├── ASolrConnection.php ├── ASolrCriteria.php ├── ASolrDataProvider.php ├── ASolrDocument.php ├── ASolrFacet.php ├── ASolrGroup.php ├── ASolrLoadBalancer.php ├── ASolrQueryResponse.php ├── ASolrResultList.php ├── ASolrSearchable.php ├── ASolrSort.php ├── IASolrConnection.php ├── IASolrDocument.php ├── README.md └── tests ├── ASolrConnectionTest.php ├── ASolrCriteriaTest.php ├── ASolrDataProviderTest.php ├── ASolrDocumentTest.php ├── ASolrQueryResponseTest.php ├── ASolrSearchableTest.php ├── common.php └── test.db /ASolrConnection.php: -------------------------------------------------------------------------------- 1 | _client = $client; 36 | } 37 | 38 | /** 39 | * Gets the solr client instance 40 | * @return SolrClient the solr client 41 | */ 42 | public function getClient() { 43 | if ($this->_client === null) { 44 | $this->_client = new SolrClient($this->getClientOptions()->toArray()); 45 | } 46 | return $this->_client; 47 | } 48 | 49 | /** 50 | * Sets the solr client options 51 | * @param array|CAttributeCollection $clientOptions the client options 52 | */ 53 | public function setClientOptions($clientOptions) { 54 | if (!($clientOptions instanceof CAttributeCollection)) { 55 | $data = $clientOptions; 56 | $clientOptions = new CAttributeCollection(); 57 | $clientOptions->caseSensitive = true; 58 | foreach($data as $key => $value) { 59 | $clientOptions->add($key,$value); 60 | } 61 | } 62 | else { 63 | $clientOptions->caseSensitive = true; 64 | } 65 | $this->_clientOptions = $clientOptions; 66 | } 67 | 68 | /** 69 | * Gets the solr client options 70 | * @return CAttributeCollection the client options 71 | */ 72 | public function getClientOptions() { 73 | if ($this->_clientOptions === null) { 74 | $clientOptions = new CAttributeCollection(); 75 | $clientOptions->caseSensitive = true; 76 | if ($this->_client !== null) { 77 | foreach($this->_client->getOptions() as $key => $value) { 78 | $clientOptions->add($key,$value); 79 | } 80 | } 81 | $this->_clientOptions = $clientOptions; 82 | } 83 | return $this->_clientOptions; 84 | } 85 | /** 86 | * Adds a document to the solr index 87 | * @param ASolrDocument|SolrInputDocument $document the document to add to the index 88 | * @param integer $commitWithin the number of milliseconds to commit within after indexing the document 89 | * @return boolean true if the document was indexed successfully 90 | */ 91 | public function index($document, $commitWithin = null) { 92 | // When we add documents we want to overwrite existing documents and avoid duplicates (several documents with the same ID). 93 | $overwrite = true; 94 | if (version_compare(solr_get_version(), '2.0.0', '<')) { 95 | // PECL Solr < 2.0 $allowDups was used instead of $overwrite, which does the same functionality with exact opposite bool flag. 96 | // See http://www.php.net/manual/en/solrclient.adddocument.php 97 | $overwrite = false; // Equivalent of $allowDups = false; 98 | } 99 | if ($document instanceof IASolrDocument) { 100 | if ($commitWithin === null && $document->getCommitWithin() > 0) { 101 | $commitWithin = $document->getCommitWithin(); 102 | } 103 | $document = $document->getInputDocument(); 104 | } 105 | elseif (is_array($document) || $document instanceof Traversable) { 106 | if ($commitWithin === null) { 107 | $commitWithin = 0; 108 | } 109 | $document = (array) $document; 110 | foreach($document as $key => $value) { 111 | if ($value instanceof IASolrDocument) { 112 | $document[$key] = $value->getInputDocument(); 113 | } 114 | } 115 | Yii::trace('Adding '.count($document)." documents to the solr index",'packages.solr.ASolrConnection'); 116 | return $this->getClient()->addDocuments($document, $overwrite, $commitWithin)->success(); 117 | } 118 | if ($commitWithin === null) { 119 | $commitWithin = 0; 120 | } 121 | Yii::trace('Adding 1 document to the solr index','packages.solr.ASolrConnection'); 122 | $response = $this->getClient()->addDocument($document, $overwrite, $commitWithin); 123 | return $response->success(); 124 | } 125 | 126 | 127 | /** 128 | * Deletes a document from the solr index 129 | * @param mixed $document the document to remove from the index, this can be the an id or an instance of ASoldDocument, an array of multiple values can also be used 130 | * @return boolean true if the document was deleted successfully 131 | */ 132 | public function delete($document) { 133 | if ($document instanceof IASolrDocument) { 134 | $document = $document->getPrimaryKey(); 135 | } 136 | elseif (is_array($document) || $document instanceof Traversable) { 137 | $document = (array) $document; 138 | foreach($document as $key => $value) { 139 | if ($value instanceof IASolrDocument) { 140 | $document[$key] = $value->getPrimaryKey(); 141 | } 142 | } 143 | Yii::trace('Deleting From Solr IDs: '.implode(", ",$document),'packages.solr.ASolrConnection'); 144 | return $this->getClient()->deleteByIds($document)->success(); 145 | } 146 | Yii::trace('Deleting From Solr ID: '.$document,'packages.solr.ASolrConnection'); 147 | return $this->getClient()->deleteById($document)->success(); 148 | } 149 | /** 150 | * Sends a commit command to solr. 151 | * @return boolean true if the commit was successful 152 | */ 153 | public function commit() { 154 | return $this->getClient()->commit()->success(); 155 | } 156 | /** 157 | * Makes a solr search request 158 | * @param ASolrCriteria $criteria the search criteria 159 | * @param string $modelClass the name of the model to use when instantiating results 160 | * @return ASolrQueryResponse the response from solr 161 | */ 162 | public function search(ASolrCriteria $criteria, $modelClass = "ASolrDocument") { 163 | if (is_object($modelClass)) { 164 | $modelClass = get_class($modelClass); 165 | } 166 | $c = new ASolrCriteria(); 167 | $c->mergeWith($criteria); 168 | 169 | Yii::trace('Querying Solr: '.((string) $c),'packages.solr.ASolrConnection'); 170 | $this->_lastQueryResponse = new ASolrQueryResponse($this->rawSearch($c),$c,$modelClass); 171 | return $this->_lastQueryResponse; 172 | } 173 | 174 | /** 175 | * Counts the number of rows that match the given criteria 176 | * @param ASolrCriteria $criteria the search criteria 177 | * @return integer the number of matching rows 178 | */ 179 | public function count(ASolrCriteria $criteria) { 180 | $c = new ASolrCriteria(); 181 | $c->mergeWith($criteria); 182 | $c->setLimit(0); 183 | Yii::trace('Counting Results from Solr: '.((string) $c),'packages.solr.ASolrConnection'); 184 | return $this->rawSearch($c)->response->numFound; 185 | } 186 | 187 | /** 188 | * Makes a search query with the given criteria and returns the raw solr object. 189 | * Usually you should use the search() method instead. 190 | * @param ASolrCriteria $criteria the search criteria 191 | * @return SolrObject the response from solr 192 | */ 193 | protected function rawSearch(ASolrCriteria $criteria) { 194 | if ($this->enableProfiling) { 195 | $profileTag = "packages.solr.AConnection.rawSearch(".$criteria->__toString().")"; 196 | Yii::beginProfile($profileTag); 197 | } 198 | $this->resetClient(); // solr client is not safely reusable, reset before every request 199 | $response = $this->getClient()->query($criteria)->getResponse(); 200 | if ($this->enableProfiling) 201 | Yii::endProfile($profileTag); 202 | 203 | return $response; 204 | } 205 | /** 206 | * Gets the last received solr query response 207 | * @return ASolrQueryResponse the last query response, or null if there are no responses yet 208 | */ 209 | public function getLastQueryResponse() { 210 | return $this->_lastQueryResponse; 211 | } 212 | 213 | /** 214 | * Reset the solr client 215 | */ 216 | public function resetClient() { 217 | $this->_client = new SolrClient($this->getClientOptions()->toArray()); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /ASolrCriteria.php: -------------------------------------------------------------------------------- 1 | $value) { 17 | $this->{$key} = $value; 18 | } 19 | } 20 | } 21 | /** 22 | * Return the scores along with the results 23 | * @return ASolrCriteria $this with the score field added 24 | */ 25 | public function withScores() { 26 | $this->addField("score"); 27 | return $this; 28 | } 29 | 30 | /** 31 | * Returns a property value based on its name. 32 | * Do not call this method. This is a PHP magic method that we override 33 | * to allow using the following syntax to read a property 34 | *
35 | * $value=$component->propertyName; 36 | *37 | * @param string $name the property name 38 | * @return mixed the property value 39 | * @throws CException if the property is not defined 40 | * @see __set 41 | */ 42 | public function __get($name) { 43 | $getter='get'.$name; 44 | if(method_exists($this,$getter)) { 45 | return $this->$getter(); 46 | } 47 | throw new CException(Yii::t('yii','Property "{class}.{property}" is not defined.', 48 | array('{class}'=>get_class($this), '{property}'=>$name))); 49 | } 50 | 51 | /** 52 | * Sets value of a component property. 53 | * Do not call this method. This is a PHP magic method that we override 54 | * to allow using the following syntax to set a property 55 | *
56 | * $this->propertyName=$value; 57 | *58 | * @param string $name the property name 59 | * @param mixed $value the property value 60 | * @return mixed 61 | * @throws CException if the property is not defined or the property is read only. 62 | * @see __get 63 | */ 64 | public function __set($name,$value) 65 | { 66 | $setter='set'.$name; 67 | if ($name == "query" && empty($value)) 68 | $value = "*:*"; 69 | if(method_exists($this,$setter)) { 70 | return $this->$setter($value); 71 | } 72 | if(method_exists($this,'get'.$name)) 73 | throw new CException(Yii::t('yii','Property "{class}.{property}" is read only.', 74 | array('{class}'=>get_class($this), '{property}'=>$name))); 75 | else 76 | throw new CException(Yii::t('yii','Property "{class}.{property}" is not defined.', 77 | array('{class}'=>get_class($this), '{property}'=>$name))); 78 | } 79 | 80 | /** 81 | * Checks if a property value is null. 82 | * Do not call this method. This is a PHP magic method that we override 83 | * to allow using isset() to detect if a component property is set or not. 84 | * @param string $name the property name 85 | * @return boolean 86 | */ 87 | public function __isset($name) 88 | { 89 | $getter='get'.$name; 90 | return method_exists($this,$getter); 91 | } 92 | 93 | /** 94 | * Sets a component property to be null. 95 | * Do not call this method. This is a PHP magic method that we override 96 | * to allow using unset() to set a component property to be null. 97 | * @param string $name the property name or the event name 98 | * @throws CException if the property is read only. 99 | * @return mixed 100 | * @since 1.0.1 101 | */ 102 | public function __unset($name) 103 | { 104 | $setter='set'.$name; 105 | if(method_exists($this,$setter)) 106 | $this->$setter(null); 107 | else if(method_exists($this,'get'.$name)) 108 | throw new CException(Yii::t('yii','Property "{class}.{property}" is read only.', 109 | array('{class}'=>get_class($this), '{property}'=>$name))); 110 | } 111 | 112 | /** 113 | * Gets the number of items to return. 114 | * This method is required for compatibility with pagination 115 | * @return integer the number of items to return 116 | */ 117 | public function getLimit() { 118 | return $this->getRows(); 119 | } 120 | /** 121 | * Sets the number of items to return. 122 | * This method is required for compatibility with pagination 123 | * @param integer $value the number of items to return 124 | */ 125 | public function setLimit($value) { 126 | $this->setRows($value); 127 | } 128 | 129 | /** 130 | * Gets the starting offset when returning results 131 | * This method is required for compatibility with pagination 132 | * @return integer the starting offset 133 | */ 134 | public function getOffset() { 135 | return $this->getStart(); 136 | } 137 | /** 138 | * Sets the starting offset when returning results 139 | * This method is required for compatibility with pagination 140 | * @param integer $value the starting offset 141 | */ 142 | public function setOffset($value) { 143 | return $this->setStart($value); 144 | } 145 | 146 | /** 147 | * Gets the sort order 148 | * @return string the sort order 149 | */ 150 | public function getOrder() { 151 | return $this->getParam("sort"); 152 | } 153 | 154 | /** 155 | * Sets the sort order 156 | * @param string $value the new sort order 157 | */ 158 | public function setOrder($value) { 159 | $this->setParam("sort",$value); 160 | } 161 | 162 | /** 163 | * Appends a condition to the existing {@link query}. 164 | * The new condition and the existing condition will be concatenated via the specified operator 165 | * which defaults to 'AND'. 166 | * The new condition can also be an array. In this case, all elements in the array 167 | * will be concatenated together via the operator. 168 | * This method handles the case when the existing condition is empty. 169 | * After calling this method, the {@link query} property will be modified. 170 | * @param mixed $condition the new condition. It can be either a string or an array of strings. 171 | * @param string $operator the operator to join different conditions. Defaults to 'AND'. 172 | * @return ASolrCriteria the criteria object itself 173 | */ 174 | public function addCondition($condition,$operator='AND') 175 | { 176 | if(is_array($condition)) 177 | { 178 | if($condition===array()) 179 | return $this; 180 | $condition='('.implode(') '.$operator.' (',$condition).')'; 181 | } 182 | $this->addFilterQuery($condition); 183 | return $this; 184 | } 185 | 186 | /** 187 | * Adds a between condition to the {@link query} 188 | * 189 | * The new between condition and the existing condition will be concatenated via 190 | * the specified operator which defaults to 'AND'. 191 | * If one or both values are empty then the condition is not added to the existing condition. 192 | * This method handles the case when the existing condition is empty. 193 | * After calling this method, the {@link condition} property will be modified. 194 | * @param string $column the name of the column to search between. 195 | * @param string $valueStart the beginning value to start the between search. 196 | * @param string $valueEnd the ending value to end the between search. 197 | * Defaults to 'AND'. 198 | * @return ASolrCriteria the criteria object itself 199 | */ 200 | public function addBetweenCondition($column,$valueStart,$valueEnd) 201 | { 202 | if($valueStart==='' || $valueEnd==='') 203 | return $this; 204 | 205 | $this->addFilterQuery($column.":[".$valueStart." TO ".$valueEnd."]"); 206 | return $this; 207 | } 208 | 209 | /** 210 | * Appends an IN condition to the existing {@link query}. 211 | * The IN condition and the existing condition will be concatenated via the specified operator 212 | * which defaults to 'AND'. 213 | * 214 | * @param string $column the column name 215 | * @param array $values list of values that the column value should be in 216 | * @param string $operator the operator used to concatenate the new condition with the existing one. 217 | * Defaults to 'AND'. 218 | * @return ASolrCriteria the criteria object itself 219 | */ 220 | public function addInCondition($column,$values,$operator='AND') 221 | { 222 | if(($n=count($values))<1) 223 | return $this; 224 | if($n===1) 225 | { 226 | $value=reset($values); 227 | 228 | $condition=$column.':'.$value; 229 | } 230 | else 231 | { 232 | $params=array(); 233 | foreach($values as $value) 234 | { 235 | $params[]=$value; 236 | } 237 | $condition=$column.':('.implode(' ',$params).')'; 238 | } 239 | return $this->addCondition($condition,$operator); 240 | } 241 | 242 | /** 243 | * Appends an NOT IN condition to the existing {@link query}. 244 | * The NOT IN condition and the existing condition will be concatenated via the specified operator 245 | * which defaults to 'AND'. 246 | * @param string $column the column name (or a valid SQL expression) 247 | * @param array $values list of values that the column value should not be in 248 | * @param string $operator the operator used to concatenate the new condition with the existing one. 249 | * Defaults to 'AND'. 250 | * @return ASolrCriteria the criteria object itself 251 | */ 252 | public function addNotInCondition($column,$values,$operator='AND') 253 | { 254 | if(($n=count($values))<1) 255 | return $this; 256 | if($n===1) 257 | { 258 | $value=reset($values); 259 | 260 | $condition=$column.':!'.$value; 261 | } 262 | else 263 | { 264 | $params=array(); 265 | foreach($values as $value) 266 | { 267 | $params[]="!".$value; 268 | } 269 | $condition=$column.':('.implode(' AND ',$params).')'; 270 | } 271 | return $this->addCondition($condition,$operator); 272 | } 273 | 274 | 275 | /** 276 | * Merges this criteria with another 277 | * @param ASolrCriteria $criteria the criteria to merge with 278 | * @return ASolrCriteria the merged criteria 279 | */ 280 | public function mergeWith(ASolrCriteria $criteria) { 281 | foreach($criteria->getParams() as $name => $value) { 282 | if ($value === null) { 283 | continue; 284 | } 285 | if ($name == "q" && (($query = $this->getQuery()) != "")) { 286 | 287 | $value = "(".$query.") AND (".$criteria->getQuery().")"; 288 | } 289 | if (!is_array($value)) { 290 | $this->setParam($name,$value); 291 | } 292 | else { 293 | foreach($value as $key => $val) { 294 | $this->addParam($name,$val); 295 | } 296 | } 297 | } 298 | return $this; 299 | } 300 | 301 | /** 302 | * Escape a string and remove solr special characters 303 | * @param string $string the string to escape 304 | * @return string the escaped string 305 | */ 306 | public function escape($string) { 307 | return SolrUtils::escapeQueryChars($string); 308 | } 309 | } -------------------------------------------------------------------------------- /ASolrDataProvider.php: -------------------------------------------------------------------------------- 1 | Post::model(),
Post::model()->published()
).
40 | * @param array $config configuration (name=>value) to be applied as the initial property values of this class.
41 | */
42 | public function __construct($modelClass,$config=array())
43 | {
44 | if($modelClass instanceof IASolrDocument || $modelClass instanceof CActiveRecord) {
45 | $this->modelClass=get_class($modelClass);
46 | $this->model=$modelClass;
47 | }
48 | else {
49 | $this->modelClass=$modelClass;
50 | $this->model=ASolrDocument::model($this->modelClass);
51 | }
52 | $this->setId($this->modelClass);
53 | foreach($config as $key=>$value) {
54 | $this->$key=$value;
55 | }
56 | }
57 | /**
58 | * Returns the query criteria.
59 | * @return ASolrCriteria the query criteria
60 | */
61 | public function getCriteria()
62 | {
63 | if($this->_criteria===null)
64 | $this->_criteria=new ASolrCriteria();
65 | return $this->_criteria;
66 | }
67 |
68 | /**
69 | * Sets the query criteria.
70 | * @param mixed $value the query criteria. This can be either a ASolrCriteria object or an array
71 | * representing the query criteria.
72 | */
73 | public function setCriteria($value)
74 | {
75 | $this->_criteria=$value instanceof ASolrCriteria ? $value : new ASolrCriteria($value);
76 | }
77 |
78 | /**
79 | * Returns the sort object.
80 | * @return CSort the sorting object. If this is false, it means the sorting is disabled.
81 | */
82 | public function getSort()
83 | {
84 | if($this->_sort===null)
85 | {
86 | $this->_sort=new ASolrSort;
87 | if(($id=$this->getId())!='')
88 | $this->_sort->sortVar=$id.'_sort';
89 | $this->_sort->modelClass=$this->modelClass;
90 | }
91 | return $this->_sort;
92 | }
93 |
94 | /**
95 | * Sets the sorting for this data provider.
96 | * @param mixed $value the sorting to be used by this data provider. This could be a {@link CSort} object
97 | * or an array used to configure the sorting object. If this is false, it means the sorting should be disabled.
98 | */
99 | public function setSort($value)
100 | {
101 | if(is_array($value))
102 | {
103 | $sort=$this->getSort();
104 | foreach($value as $k=>$v)
105 | $sort->$k=$v;
106 | }
107 | else
108 | $this->_sort=$value;
109 | }
110 |
111 | /**
112 | * Fetches the data from the persistent data storage.
113 | * @return array list of data items
114 | */
115 | protected function fetchData()
116 | {
117 | $criteria=new ASolrCriteria();
118 | $criteria->mergeWith($this->getCriteria());
119 |
120 | if(($pagination=$this->getPagination())!==false)
121 | {
122 | $pagination->setItemCount(999999999); // set to an unreasonably high value to save an extra request
123 | $pagination->applyLimit($criteria);
124 | }
125 |
126 | if(($sort=$this->getSort())!==false)
127 | $sort->applyOrder($criteria);
128 |
129 | if ($this->model instanceof CActiveRecord) {
130 | // this should be a model with ASolrSearchable attached
131 | if ($this->loadFromDB) {
132 | $results = $this->model->getSolrDocument()->findAll($criteria);
133 | $this->_solrQueryResponse = $this->model->getSolrDocument()->getSolrConnection()->getLastQueryResponse();
134 | $ids = array();
135 | foreach($results as $n => $item /* @var ASolrDocument $item */) {
136 | $ids[$n] = $item->getPrimaryKey();
137 | }
138 | if (!empty($ids)){
139 | $c = new CDbCriteria();
140 | $fields = $ids;
141 | array_unshift($fields,$this->model->getTableAlias().'.'.$this->model->getMetaData()->tableSchema->primaryKey);
142 | $c->order = 'FIELD('.implode(',',$fields).')';// keep the order of objects as it is from solr's results
143 | $data = $this->model->findAllByPk($ids,$c);
144 | $ids = array_flip($ids);
145 | foreach($data as $n => $model) {
146 | $model->setSolrDocument($results[$ids[$model->getPrimaryKey()]]);
147 | }
148 | }else {
149 | $data = array(); // prevent any errors
150 | }
151 | }
152 | else {
153 | $data = $this->model->findAllBySolr($criteria);
154 | $this->_solrQueryResponse = $this->model->getSolrDocument()->getSolrConnection()->getLastQueryResponse();
155 | }
156 | }
157 | else {
158 | $data=$this->model->findAll($criteria);
159 | $this->_solrQueryResponse = $this->model->getSolrConnection()->getLastQueryResponse();
160 | }
161 | if ($pagination) {
162 | $pagination->setItemCount($this->_solrQueryResponse->getResults()->total);
163 | }
164 |
165 | return $data;
166 | }
167 |
168 | /**
169 | * Calculates the total number of data items.
170 | * @return integer the total number of data items.
171 | */
172 | protected function calculateTotalItemCount()
173 | {
174 | if ($this->model instanceof CActiveRecord) {
175 | // this should be a model with ASolrSearchable attached
176 | return $this->model->getSolrDocument()->count($this->getCriteria());
177 | }
178 | else {
179 | return $this->model->count($this->getCriteria());
180 | }
181 | }
182 |
183 | /**
184 | * Gets an array of date facets that belong to this query response
185 | * @return ASolrFacet[]
186 | */
187 | public function getDateFacets()
188 | {
189 | if ($this->_solrQueryResponse === null) {
190 | $this->getData();
191 | }
192 | return $this->_solrQueryResponse->getDateFacets();
193 | }
194 |
195 | /**
196 | * Gets an array of field facets that belong to this query response
197 | * @return ASolrFacet[]
198 | */
199 | public function getFieldFacets()
200 | {
201 | if ($this->_solrQueryResponse === null) {
202 | $this->getData();
203 | }
204 | return $this->_solrQueryResponse->getFieldFacets();
205 | }
206 | /**
207 | * Gets an array of query facets that belong to this query response
208 | * @return ASolrFacet[]
209 | */
210 | public function getQueryFacets()
211 | {
212 | if ($this->_solrQueryResponse === null) {
213 | $this->getData();
214 | }
215 | return $this->_solrQueryResponse->getQueryFacets();
216 | }
217 | /**
218 | * Gets an array of range facets that belong to this query response
219 | * @return ASolrFacet[]
220 | */
221 | public function getRangeFacets()
222 | {
223 | if ($this->_solrQueryResponse === null) {
224 | $this->getData();
225 | }
226 | return $this->_solrQueryResponse->getRangeFacets();
227 | }
228 |
229 | /**
230 | * Gets the solr query response
231 | * @return ASolrQueryResponse the response from solr
232 | */
233 | public function getSolrQueryResponse()
234 | {
235 | return $this->_solrQueryResponse;
236 | }
237 | }
238 |
--------------------------------------------------------------------------------
/ASolrDocument.php:
--------------------------------------------------------------------------------
1 |
5 | * $document = ASolrDocument::model()->findByPk(1); // loads a document with a unique id of 1
6 | * $document->name = "a test name"; // sets the name attribute on the document
7 | * $document->save(); // saves the document to solr
8 | * $document->delete(); // deletes the document from solr
9 | *
10 | * @author Charles Pick / PeoplePerHour.com
11 | * @package packages.solr
12 | */
13 | class ASolrDocument extends CFormModel implements IASolrDocument{
14 | /**
15 | * The solr connection
16 | * @var ASolrConnection
17 | */
18 | public static $solr;
19 |
20 | /**
21 | * The number of milliseconds to commit saves and updates within.
22 | * @var integer
23 | */
24 | protected $_commitWithin = 0;
25 |
26 | /**
27 | * The document attributes.
28 | * @var CAttributeCollection
29 | */
30 | protected $_attributes;
31 | /**
32 | * Holds the solr query response after a find call
33 | * @var ASolrQueryResponse
34 | */
35 | protected $_solrResponse;
36 |
37 | /**
38 | * The connection to solr
39 | * @var ASolrConnection
40 | */
41 | protected $_connection;
42 | /**
43 | * Holds the solr criteria
44 | * @var ASolrCriteria
45 | */
46 | protected $_solrCriteria;
47 |
48 | /**
49 | * The position in the search results
50 | * @var integer
51 | */
52 | protected $_position;
53 |
54 | /**
55 | * Holds the document score
56 | * @var integer
57 | */
58 | protected $_score;
59 |
60 | /**
61 | * The solr input document
62 | * @var SolrInputDocument
63 | */
64 | protected $_inputDocument;
65 |
66 | /**
67 | * The old primary key value
68 | * @var mixed
69 | */
70 | private $_pk;
71 | /**
72 | * Whether this is a new document or not
73 | * @var boolean
74 | */
75 | private $_new = true;
76 |
77 | /**
78 | * @var array the highlights for this record if highlighting is enabled
79 | */
80 | private $_highlights;
81 |
82 | /**
83 | * An array of static model instances, clas name => model
84 | * @var array
85 | */
86 | private static $_models=array();
87 |
88 |
89 | /**
90 | * Sets the document score
91 | * @param integer $score
92 | */
93 | public function setScore($score) {
94 | $this->_score = $score;
95 | }
96 |
97 | /**
98 | * Gets the score for this document
99 | * @return integer
100 | */
101 | public function getScore() {
102 | return $this->_score;
103 | }
104 |
105 | /**
106 | * Sets the position in the search results
107 | * @param integer $position
108 | */
109 | public function setPosition($position) {
110 | $this->_position = $position;
111 | }
112 |
113 | /**
114 | * Gets the position in the search results
115 | * @return integer the position in the search results
116 | */
117 | public function getPosition() {
118 | return $this->_position;
119 | }
120 | /**
121 | * Gets a list of attribute names on the model
122 | * @return array the list of attribute names
123 | */
124 | public function attributeNames()
125 | {
126 | return CMap::mergeArray(
127 | array_keys($this->_attributes->toArray()),
128 | parent::attributeNames()
129 | );
130 | }
131 | /**
132 | * An array of attribute default values
133 | * @return array
134 | */
135 | public function attributeDefaults() {
136 | return array();
137 | }
138 | /**
139 | * Checks whether this AR has the named attribute
140 | * @param string $name attribute name
141 | * @return boolean whether this AR has the named attribute (table column).
142 | */
143 | public function hasAttribute($name)
144 | {
145 | return in_array($name, $this->attributeNames());
146 | }
147 | /**
148 | * Constructor.
149 | * @param string $scenario the scenario name
150 | * See {@link CModel::scenario} on how scenario is used by models.
151 | * @see getScenario
152 | */
153 | public function __construct($scenario = "insert")
154 | {
155 | if ($scenario === null) {
156 | return;
157 | }
158 | $this->init();
159 | $this->attachBehaviors($this->behaviors());
160 | $this->afterConstruct();
161 | }
162 |
163 | /**
164 | * Returns the static model of the specified solr document class.
165 | * The model returned is a static instance of the solr document class.
166 | * It is provided for invoking class-level methods (something similar to static class methods.)
167 | *
168 | * EVERY derived solr document class must override this method as follows,
169 | * 170 | * public static function model($className=__CLASS__) 171 | * { 172 | * return parent::model($className); 173 | * } 174 | *175 | * 176 | * @param string $className solr document class name. 177 | * @return ASolrDocument solr document model instance. 178 | */ 179 | public static function model($className=__CLASS__) 180 | { 181 | if(isset(self::$_models[$className])) 182 | return self::$_models[$className]; 183 | else 184 | { 185 | $model=self::$_models[$className]=new $className(null); 186 | $model->attachBehaviors($model->behaviors()); 187 | return $model; 188 | } 189 | } 190 | 191 | /** 192 | * Returns the solr connection used by solr document. 193 | * By default, the "solr" application component is used as the solr connection. 194 | * You may override this method if you want to use a different solr connection. 195 | * @return ASolrConnection the solr connection used by solr document. 196 | */ 197 | public function getSolrConnection() 198 | { 199 | if ($this->_connection !== null) { 200 | return $this->_connection; 201 | } 202 | elseif(self::$solr!==null) { 203 | return self::$solr; 204 | } 205 | else 206 | { 207 | self::$solr=Yii::app()->solr; 208 | if(self::$solr instanceof IASolrConnection) 209 | return self::$solr; 210 | else 211 | throw new CException(Yii::t('yii','Solr Document requires a "solr" ASolrConnection application component.')); 212 | } 213 | } 214 | /** 215 | * Sets the solr connection used by this solr document 216 | * @param ASolrConnection $connection the solr connection to use for this document 217 | */ 218 | public function setSolrConnection(ASolrConnection $connection) { 219 | $this->_connection = $connection; 220 | } 221 | /** 222 | * Returns the solr query criteria associated with this model. 223 | * @param boolean $createIfNull whether to create a criteria instance if it does not exist. Defaults to true. 224 | * @return ASolrCriteria the query criteria that is associated with this model. 225 | * This criteria is mainly used by {@link scopes named scope} feature to accumulate 226 | * different criteria specifications. 227 | */ 228 | public function getSolrCriteria($createIfNull=true) 229 | { 230 | if($this->_solrCriteria===null) 231 | { 232 | if(($c=$this->defaultScope())!==array() || $createIfNull) { 233 | $this->_solrCriteria=new ASolrCriteria($c); 234 | 235 | } 236 | } 237 | return $this->_solrCriteria; 238 | } 239 | 240 | /** 241 | * Sets the solr query criteria for the current model. 242 | * @param ASolrCriteria $criteria the query criteria 243 | */ 244 | public function setSolrCriteria(ASolrCriteria $criteria) 245 | { 246 | $this->_solrCriteria=$criteria; 247 | } 248 | 249 | /** 250 | * Returns the default named scope that should be implicitly applied to all queries for this model. 251 | * The default implementation simply returns an empty array. You may override this method 252 | * if the model needs to be queried with some default criteria 253 | * @return array the query criteria. This will be used as the parameter to the constructor 254 | * of {@link ASolrCriteria}. 255 | */ 256 | public function defaultScope() 257 | { 258 | return array(); 259 | } 260 | 261 | /** 262 | * Resets all scopes and criterias applied including default scope. 263 | * 264 | * @return ASolrDocument 265 | */ 266 | public function resetScope() 267 | { 268 | $this->_solrCriteria = new ASolrCriteria(); 269 | return $this; 270 | } 271 | 272 | /** 273 | * Gets the solr input document 274 | * @return SolrInputDocument the solr document 275 | */ 276 | public function getInputDocument() { 277 | if ($this->_inputDocument !== null) { 278 | return $this->_inputDocument; 279 | } 280 | $this->_inputDocument = new SolrInputDocument(); 281 | foreach($this->attributeNames() as $attribute) { 282 | if ($this->{$attribute} !== null) { 283 | if (is_array($this->{$attribute})) { 284 | foreach($this->{$attribute} as $value) { 285 | $this->_inputDocument->addField($attribute,$value); 286 | } 287 | } 288 | else { 289 | $this->_inputDocument->addField($attribute,$this->prepareAttribute($attribute)); 290 | } 291 | } 292 | } 293 | return $this->_inputDocument; 294 | } 295 | /** 296 | * Gets an array of mapping rules for this solr index. 297 | * Child classes should override this method to specify solr types for individual fields. 298 | * E.g. 299 | *
300 | * array( 301 | * "fieldName" => "string", 302 | * "someDateField" => "date", 303 | * ); 304 | *305 | * @return array an array of mapping rules for this solr index 306 | */ 307 | public function attributeMapping() { 308 | return array(); 309 | } 310 | /** 311 | * Prepares an attribute to be inputted into solr 312 | * @param string $attribute the name of the attribute 313 | * @return mixed the prepared value to insert into solr 314 | */ 315 | protected function prepareAttribute($attribute) { 316 | $mapping = $this->attributeMapping(); 317 | if (!isset($mapping[$attribute])) { 318 | return $this->{$attribute}; 319 | } 320 | if (is_array($mapping[$attribute])) { 321 | if (!isset($mapping[$attribute]['type'])) { 322 | return $this->{$attribute}; 323 | } 324 | $type = $mapping[$attribute]['type']; 325 | } 326 | else { 327 | $type = $mapping[$attribute]; 328 | } 329 | switch (strtolower($type)) { 330 | case "date": 331 | $date = $this->{$attribute}; 332 | if (substr($date,0,4) == "0000") { 333 | $date = 0; 334 | } 335 | $date = $this->createSolrDateTime(is_int($date) ? $date : strtotime($date)); 336 | return $date; 337 | break; 338 | default: 339 | return $this->{$attribute}; 340 | } 341 | } 342 | /** 343 | * Formats a unix timestamp in solr date format 344 | * @param integer $time the unix timestamp 345 | * @return string the solr formatted timestamp 346 | */ 347 | public function createSolrDateTime($time) { 348 | $defaultTZ = date_default_timezone_get(); 349 | date_default_timezone_set("UTC"); 350 | $date = date("Y-m-d\TH:i:s\Z",$time); 351 | date_default_timezone_set($defaultTZ); 352 | return $date; 353 | } 354 | 355 | /** 356 | * Parse a solr date time into unix time 357 | * @param string $time the time from solr 358 | * @return integer the unix time 359 | */ 360 | public function parseSolrDateTime($time) { 361 | if (substr($time,0,7) == "2-11-30") // this is a broken date / time in solr 362 | return null; 363 | return strtotime($time); 364 | } 365 | 366 | 367 | /** 368 | * Returns a property value or an event handler list by property or event name. 369 | * This method overrides the parent implementation by returning 370 | * a key value if the key exists in the collection. 371 | * @param string $name the property name or the event name 372 | * @return mixed the property value or the event handler list 373 | * @throws CException if the property/event is not defined. 374 | */ 375 | public function __get($name) 376 | { 377 | if($this->_attributes->contains($name)) 378 | return $this->_attributes->itemAt($name); 379 | else 380 | return parent::__get($name); 381 | } 382 | 383 | /** 384 | * Sets value of a component property. 385 | * This method overrides the parent implementation by adding a new key value 386 | * to the collection. 387 | * @param string $name the property name or event name 388 | * @param mixed $value the property value or event handler 389 | * @throws CException If the property is not defined or read-only. 390 | */ 391 | public function __set($name,$value) 392 | { 393 | $setter = "set".$name; 394 | if (method_exists($this,$setter)) { 395 | return $this->{$setter}($value); 396 | } 397 | $this->add($name,$value); 398 | } 399 | 400 | /** 401 | * Checks if a property value is null. 402 | * This method overrides the parent implementation by checking 403 | * if the key exists in the collection and contains a non-null value. 404 | * @param string $name the property name or the event name 405 | * @return boolean whether the property value is null 406 | * @since 1.0.1 407 | */ 408 | public function __isset($name) 409 | { 410 | if($this->_attributes->contains($name)) 411 | return $this->_attributes->itemAt($name)!==null; 412 | else 413 | return parent::__isset($name); 414 | } 415 | 416 | /** 417 | * Sets a component property to be null. 418 | * This method overrides the parent implementation by clearing 419 | * the specified key value. 420 | * @param string $name the property name or the event name 421 | * @since 1.0.1 422 | */ 423 | public function __unset($name) 424 | { 425 | $this->_attributes->remove($name); 426 | } 427 | /** 428 | * Initializes this model. 429 | * This method is invoked in the constructor right after {@link scenario} is set. 430 | * You may override this method to provide code that is needed to initialize the model (e.g. setting 431 | * initial property values.) 432 | * @since 1.0.8 433 | */ 434 | public function init() 435 | { 436 | $this->_attributes = new CAttributeCollection(); 437 | $this->_attributes->caseSensitive = true; 438 | } 439 | 440 | /** 441 | * Creates a solr document instance. 442 | * This method is called by {@link populateRecord} and {@link populateRecords}. 443 | * You may override this method if the instance being created 444 | * depends the attributes that are to be populated to the record. 445 | * For example, by creating a record based on the value of a column, 446 | * you may implement the so-called single-table inheritance mapping. 447 | * @param array $attributes list of attribute values for the solr document. 448 | * @return ASolrDocument the active record 449 | */ 450 | protected function instantiate($attributes) 451 | { 452 | $class=get_class($this); 453 | $model=new $class(null); 454 | return $model; 455 | } 456 | 457 | /** 458 | * Creates a solr document with the given attributes. 459 | * This method is internally used by the find methods. 460 | * @param array $attributes attribute values (column name=>column value) 461 | * @param boolean $callAfterFind whether to call {@link afterFind} after the record is populated. 462 | * @return ASolrDocument the newly created solr document. The class of the object is the same as the model class. 463 | * Null is returned if the input data is false. 464 | */ 465 | public function populateRecord($attributes,$callAfterFind=true) 466 | { 467 | if($attributes!==false) 468 | { 469 | $record=$this->instantiate($attributes); 470 | $record->setScenario('update'); 471 | $record->init(); 472 | $mapping = $this->attributeMapping(); 473 | foreach($attributes as $name=>$value) { 474 | if (isset($mapping[$name])) { 475 | switch ($mapping[$name]) { 476 | case "date"; 477 | $value = $this->parseSolrDateTime($value); 478 | if ($value !== null) 479 | $value = date("Y-m-d H:i:s", $value); 480 | break; 481 | } 482 | } 483 | $record->$name=$value; 484 | } 485 | $record->_pk=$record->getPrimaryKey(); 486 | $record->attachBehaviors($record->behaviors()); 487 | if($callAfterFind) { 488 | $record->afterFind(); 489 | } 490 | return $record; 491 | } 492 | else { 493 | return null; 494 | } 495 | } 496 | 497 | /** 498 | * Creates a list of solr documents based on the input data. 499 | * This method is internally used by the find methods. 500 | * @param array $data list of attribute values for the solr documents. 501 | * @param boolean $callAfterFind whether to call {@link afterFind} after each record is populated. 502 | * @param string $index the name of the attribute whose value will be used as indexes of the query result array. 503 | * If null, it means the array will be indexed by zero-based integers. 504 | * @return array list of solr documents. 505 | */ 506 | public function populateRecords($data,$callAfterFind=true,$index=null) 507 | { 508 | $records=array(); 509 | foreach($data as $attributes) 510 | { 511 | if(($record=$this->populateRecord($attributes,$callAfterFind))!==null) 512 | { 513 | if($index===null) 514 | $records[]=$record; 515 | else 516 | $records[$record->$index]=$record; 517 | } 518 | } 519 | return $records; 520 | } 521 | 522 | /** 523 | * Returns the name of the primary key of the associated solr index. 524 | * Child classes should override this if the primary key is anything other than "id" 525 | * @return mixed the primary key attribute name(s). Defaults to "id" 526 | */ 527 | public function primaryKey() 528 | { 529 | return "id"; 530 | } 531 | 532 | /** 533 | * Returns the primary key value. 534 | * @return mixed the primary key value. An array (column name=>column value) is returned if the primary key is composite. 535 | * If primary key is not defined, null will be returned. 536 | */ 537 | public function getPrimaryKey() 538 | { 539 | $attribute = $this->primaryKey(); 540 | if (!is_array($attribute)) { 541 | return $this->{$attribute}; 542 | } 543 | $pk = array(); 544 | foreach($attribute as $field) { 545 | $pk[$field] = $this->{$attribute}; 546 | } 547 | return $pk; 548 | } 549 | 550 | /** 551 | * Sets the primary key value. 552 | * After calling this method, the old primary key value can be obtained from {@link oldPrimaryKey}. 553 | * @param mixed $value the new primary key value. If the primary key is composite, the new value 554 | * should be provided as an array (column name=>column value). 555 | */ 556 | public function setPrimaryKey($value) 557 | { 558 | $this->_pk=$this->getPrimaryKey(); 559 | $attribute = $this->primaryKey(); 560 | if (!is_array($attribute)) { 561 | return $this->{$attribute} = $value; 562 | } 563 | foreach($value as $attribute => $attributeValue) { 564 | $this->{$attribute} = $attributeValue; 565 | } 566 | return $value; 567 | } 568 | /** 569 | * Returns the old primary key value. 570 | * This refers to the primary key value that is populated into the record 571 | * after executing a find method (e.g. find(), findAll()). 572 | * The value remains unchanged even if the primary key attribute is manually assigned with a different value. 573 | * @return mixed the old primary key value. An array (column name=>column value) is returned if the primary key is composite. 574 | * If primary key is not defined, null will be returned. 575 | * @since 1.1.0 576 | */ 577 | public function getOldPrimaryKey() 578 | { 579 | return $this->_pk; 580 | } 581 | 582 | /** 583 | * Sets the old primary key value. 584 | * @param mixed $value the old primary key value. 585 | * @since 1.1.3 586 | */ 587 | public function setOldPrimaryKey($value) 588 | { 589 | $this->_pk=$value; 590 | } 591 | /** 592 | * Adds an item into the map. 593 | * If the item is an array, it will be converted to an instance of ASolrCriteria 594 | * @param mixed $key key 595 | * @param mixed $value value 596 | */ 597 | public function add($key,$value) { 598 | if (property_exists($this,$key)) { 599 | $this->{$key} = $value; 600 | } 601 | else { 602 | $this->_attributes->add($key,$value); 603 | } 604 | } 605 | 606 | /** 607 | * Returns an array containing the attributes 608 | * @return array the list of items in array 609 | */ 610 | public function toArray() { 611 | $data = array(); 612 | foreach($this->attributeNames() as $attribute) { 613 | $data[$attribute] = $this->{$attribute}; 614 | } 615 | return $data; 616 | } 617 | 618 | /** 619 | * Returns an array containing the attributes 620 | * @see toArray() 621 | */ 622 | public function __toArray() { 623 | return $this->toArray(); 624 | } 625 | 626 | /** 627 | * Sets the attribute values in a massive way. 628 | * @param array $values attribute values (name=>value) to be set. 629 | * @param boolean $safeOnly whether the assignments should only be done to the safe attributes. 630 | * A safe attribute is one that is associated with a validation rule in the current {@link scenario}. 631 | * @see getSafeAttributeNames 632 | * @see attributeNames 633 | */ 634 | public function setAttributes($values,$safeOnly=true) 635 | { 636 | if ($safeOnly) { 637 | return parent::setAttributes($values,true); 638 | } 639 | foreach($values as $attribute => $value) { 640 | $this->{$attribute} = $value; 641 | } 642 | } 643 | 644 | /** 645 | * Saves the solr document 646 | * @param boolean $runValidation whether to run validation or not, defaults to true 647 | * @return boolean whether the save succeeded or not 648 | */ 649 | public function save($runValidation = true) { 650 | if ($runValidation && !$this->validate()) { 651 | return false; 652 | } 653 | if (!$this->beforeSave()) { 654 | return false; 655 | } 656 | $connection = $this->getSolrConnection(); 657 | if (!$connection->index($this)) { 658 | return false; 659 | } 660 | $this->afterSave(); 661 | return true; 662 | } 663 | /** 664 | * Deletes the solr document 665 | * @return boolean whether the delete succeeded or not 666 | */ 667 | public function delete() { 668 | if (!$this->beforeDelete() || !$this->getSolrConnection()->delete($this)){ 669 | return false; 670 | } 671 | $this->afterDelete(); 672 | return true; 673 | } 674 | 675 | /** 676 | * Returns the number of documents matching specified criteria. 677 | * @param ASolrCriteria $criteria solr query criteria. 678 | * @return integer the number of rows found 679 | */ 680 | public function count(ASolrCriteria $criteria = null) 681 | { 682 | Yii::trace(get_class($this).'.count()','packages.solr.ASolrDocument'); 683 | if ($criteria === null) { 684 | $criteria = new ASolrCriteria(); 685 | } 686 | $this->applyScopes($criteria); 687 | return $this->getSolrConnection()->count($criteria); 688 | } 689 | 690 | /** 691 | * Finds a single solr document according to the specified criteria. 692 | * @param ASolrCriteria $criteria solr query criteria. 693 | * @return ASolrDocument the document found. Null if none is found. 694 | */ 695 | public function find(ASolrCriteria $criteria = null) 696 | { 697 | Yii::trace(get_class($this).'.find()','packages.solr.ASolrDocument'); 698 | if ($criteria === null) { 699 | $criteria = new ASolrCriteria(); 700 | } 701 | return $this->query($criteria); 702 | } 703 | 704 | /** 705 | * Finds multiple solr documents according to the specified criteria. 706 | * @param ASolrCriteria $criteria solr query criteria. 707 | * @return ASolrDocument[] the documents found. 708 | */ 709 | public function findAll(ASolrCriteria $criteria = null) 710 | { 711 | Yii::trace(get_class($this).'.findAll()','packages.solr.ASolrDocument'); 712 | if ($criteria === null) { 713 | $criteria = new ASolrCriteria(); 714 | } 715 | return $this->query($criteria,true); 716 | } 717 | 718 | /** 719 | * Finds a single solr document with the specified primary key. 720 | * @param mixed $pk primary key value(s). Use array for multiple primary keys. For composite key, each key value must be an array (column name=>column value). 721 | * @param ASolrCriteria $criteria solr query criteria. 722 | * @return ASolrDocument the document found. Null if none is found. 723 | */ 724 | public function findByPk($pk,ASolrCriteria $criteria = null) 725 | { 726 | Yii::trace(get_class($this).'.findByPk()','packages.solr.ASolrDocument'); 727 | if ($criteria === null) { 728 | $criteria = new ASolrCriteria(); 729 | } 730 | if (is_array($pk)) { 731 | $query = array(); 732 | foreach($pk as $attribute => $value) { 733 | $query[] = $attribute.":".$value; 734 | } 735 | $criteria->setQuery(implode(" AND ",$query)); 736 | } 737 | else { 738 | $criteria->setQuery($this->primaryKey().":".$pk); 739 | } 740 | return $this->query($criteria); 741 | } 742 | 743 | /** 744 | * Finds all solr documents with the specified primary keys. 745 | * @param mixed $pk primary key value(s). Use array for multiple primary keys. For composite key, each key value must be an array (column name=>column value). 746 | * @param ASolrCriteria $criteria solr query criteria. 747 | * @return ASolrDocument the document found. Null if none is found. 748 | */ 749 | public function findAllByPk($pk,ASolrCriteria $criteria = null) 750 | { 751 | Yii::trace(get_class($this).'.findByPk()','packages.solr.ASolrDocument'); 752 | if ($criteria === null) { 753 | $criteria = new ASolrCriteria(); 754 | } 755 | $query = array(); 756 | foreach($pk as $key) { 757 | if (is_array($key)) { 758 | $item = array(); 759 | foreach($key as $attribute => $value) { 760 | $item[] = $attribute.":".$value; 761 | } 762 | $query[] = implode(" AND ",$item); 763 | } 764 | else { 765 | $query[] = $this->primaryKey().":".$key; 766 | } 767 | } 768 | $criteria->setQuery(implode(" OR ",$query)); 769 | return $this->query($criteria,true); 770 | } 771 | 772 | /** 773 | * Finds a single solr document that has the specified attribute values. 774 | * @param array $attributes list of attribute values (indexed by attribute names) that the solr documents should match. 775 | * @param ASolrCriteria $criteria The solr search criteria 776 | * @return ASolrDocument the document found. Null if none is found. 777 | */ 778 | public function findByAttributes($attributes,$criteria = null) 779 | { 780 | Yii::trace(get_class($this).'.findByAttributes()','packages.solr.ASolrDocument'); 781 | if ($criteria === null) { 782 | $criteria = new ASolrCriteria(); 783 | } 784 | $query = array(); 785 | foreach($attributes as $attribute => $value) { 786 | $query[] = $attribute.":".$value; 787 | } 788 | $criteria->setQuery(implode(" AND ",$query)); 789 | return $this->query($criteria); 790 | } 791 | 792 | /** 793 | * Finds multiple solr document that have the specified attribute values. 794 | * @param array $attributes list of attribute values (indexed by attribute names) that the solr documents should match. 795 | * @param ASolrCriteria $criteria The solr search criteria 796 | * @return ASolrDocument[] the documents found. 797 | */ 798 | public function findAllByAttributes($attributes,$criteria = null) 799 | { 800 | Yii::trace(get_class($this).'.findAllByAttributes()','packages.solr.ASolrDocument'); 801 | if ($criteria === null) { 802 | $criteria = new ASolrCriteria(); 803 | } 804 | $query = array(); 805 | foreach($attributes as $attribute => $value) { 806 | $query[] = $attribute.":".$value; 807 | } 808 | $criteria->setQuery(implode(" AND ",$query)); 809 | return $this->query($criteria,true); 810 | } 811 | 812 | /** 813 | * Performs the actual solr query and populates the solr document objects with the query result. 814 | * This method is mainly internally used by other solr document query methods. 815 | * @param ASolrCriteria $criteria the query criteria 816 | * @param boolean $all whether to return all data 817 | * @return mixed the solr document objects populated with the query result 818 | */ 819 | protected function query($criteria,$all=false) 820 | { 821 | $this->beforeFind(); 822 | $this->applyScopes($criteria); 823 | 824 | if(!$all) { 825 | $criteria->setLimit(1); 826 | } 827 | 828 | $response = $this->getSolrConnection()->search($criteria,$this); 829 | $results = $response->getResults()->toArray(); 830 | if (!count($results) && $criteria->getParam("group")) { 831 | // this is the result of a group by query 832 | $groups = $response->getGroups()->toArray(); 833 | $group = array_shift($groups); 834 | if ($group) 835 | $results = $group->toArray(); 836 | } 837 | if ($all) { 838 | return $results; 839 | } 840 | else { 841 | 842 | return array_shift($results); 843 | } 844 | 845 | } 846 | /** 847 | * Returns the declaration of named scopes. 848 | * A named scope represents a query criteria that can be chained together with 849 | * other named scopes and applied to a query. This method should be overridden 850 | * by child classes to declare named scopes for the particular solr document classes. 851 | * @return array the scope definition 852 | */ 853 | public function scopes() 854 | { 855 | return array(); 856 | } 857 | 858 | /** 859 | * Applies the query scopes to the given criteria. 860 | * This method merges {@link solrCriteria} with the given criteria parameter. 861 | * It then resets {@link solrCriteria} to be null. 862 | * @param ASolrCriteria $criteria the query criteria. This parameter may be modified by merging {@link solrCriteria}. 863 | */ 864 | public function applyScopes(&$criteria) 865 | { 866 | if(!empty($criteria->scopes)) 867 | { 868 | $scs=$this->scopes(); 869 | $c=$this->getSolrCriteria(); 870 | foreach((array)$criteria->scopes as $k=>$v) 871 | { 872 | if(is_integer($k)) 873 | { 874 | if(is_string($v)) 875 | { 876 | if(isset($scs[$v])) 877 | { 878 | $c->mergeWith($scs[$v],true); 879 | continue; 880 | } 881 | $scope=$v; 882 | $params=array(); 883 | } 884 | else if(is_array($v)) 885 | { 886 | $scope=key($v); 887 | $params=current($v); 888 | } 889 | } 890 | else if(is_string($k)) 891 | { 892 | $scope=$k; 893 | $params=$v; 894 | } 895 | 896 | call_user_func_array(array($this,$scope),(array)$params); 897 | } 898 | } 899 | 900 | if(isset($c) || ($c=$this->getSolrCriteria(false))!==null) 901 | { 902 | $c->mergeWith($criteria); 903 | $criteria=$c; 904 | $this->_solrCriteria=null; 905 | } 906 | } 907 | 908 | 909 | /** 910 | * Returns whether there is an element at the specified offset. 911 | * This method is required by the interface ArrayAccess. 912 | * @param mixed $offset the offset to check on 913 | * @return boolean 914 | */ 915 | public function offsetExists($offset) 916 | { 917 | return $this->_attributes->contains($offset); 918 | } 919 | 920 | /** 921 | * Returns the element at the specified offset. 922 | * This method is required by the interface ArrayAccess. 923 | * @param integer $offset the offset to retrieve element. 924 | * @return mixed the element at the offset, null if no element is found at the offset 925 | */ 926 | public function offsetGet($offset) 927 | { 928 | return $this->_attributes->itemAt($offset); 929 | } 930 | 931 | /** 932 | * Sets the element at the specified offset. 933 | * This method is required by the interface ArrayAccess. 934 | * @param integer $offset the offset to set element 935 | * @param mixed $item the element value 936 | */ 937 | public function offsetSet($offset,$item) 938 | { 939 | $this->_attributes->add($offset,$item); 940 | } 941 | 942 | /** 943 | * Unsets the element at the specified offset. 944 | * This method is required by the interface ArrayAccess. 945 | * @param mixed $offset the offset to unset element 946 | */ 947 | public function offsetUnset($offset) 948 | { 949 | $this->_attributes->remove($offset); 950 | } 951 | 952 | /** 953 | * Returns if the current document is new. 954 | * @return boolean whether the record is new and should be inserted when calling {@link save}. 955 | * This property is automatically set in constructor and {@link populateRecord}. 956 | * Defaults to false, but it will be set to true if the instance is created using 957 | * the new operator. 958 | */ 959 | public function getIsNewRecord() 960 | { 961 | return $this->_new; 962 | } 963 | 964 | /** 965 | * Sets if the record is new. 966 | * @param boolean $value whether the document is new and should be inserted when calling {@link save}. 967 | * @see getIsNewRecord 968 | */ 969 | public function setIsNewRecord($value) 970 | { 971 | $this->_new=$value; 972 | } 973 | 974 | /** 975 | * This event is raised before the document is saved. 976 | * By setting {@link CModelEvent::isValid} to be false, the normal {@link save()} process will be stopped. 977 | * @param CModelEvent $event the event parameter 978 | */ 979 | public function onBeforeSave($event) 980 | { 981 | $this->raiseEvent('onBeforeSave',$event); 982 | } 983 | 984 | /** 985 | * This event is raised after the document is saved. 986 | * @param CEvent $event the event parameter 987 | */ 988 | public function onAfterSave($event) 989 | { 990 | $this->raiseEvent('onAfterSave',$event); 991 | } 992 | 993 | /** 994 | * This event is raised before the document is deleted. 995 | * By setting {@link CModelEvent::isValid} to be false, the normal {@link delete()} process will be stopped. 996 | * @param CModelEvent $event the event parameter 997 | */ 998 | public function onBeforeDelete($event) 999 | { 1000 | $this->raiseEvent('onBeforeDelete',$event); 1001 | } 1002 | 1003 | /** 1004 | * This event is raised after the record is deleted. 1005 | * @param CEvent $event the event parameter 1006 | */ 1007 | public function onAfterDelete($event) 1008 | { 1009 | $this->raiseEvent('onAfterDelete',$event); 1010 | } 1011 | 1012 | /** 1013 | * This event is raised before a find call. 1014 | * In this event, the {@link CModelEvent::criteria} property contains the query criteria 1015 | * passed as parameters to those find methods. If you want to access 1016 | * the query criteria specified in scopes, please use {@link getDbCriteria()}. 1017 | * You can modify either criteria to customize them based on needs. 1018 | * @param CModelEvent $event the event parameter 1019 | * @see beforeFind 1020 | */ 1021 | public function onBeforeFind($event) 1022 | { 1023 | $this->raiseEvent('onBeforeFind',$event); 1024 | } 1025 | 1026 | /** 1027 | * This event is raised after the document is instantiated by a find method. 1028 | * @param CEvent $event the event parameter 1029 | */ 1030 | public function onAfterFind($event) 1031 | { 1032 | $this->raiseEvent('onAfterFind',$event); 1033 | } 1034 | 1035 | /** 1036 | * This method is invoked before saving a document (after validation, if any). 1037 | * The default implementation raises the {@link onBeforeSave} event. 1038 | * You may override this method to do any preparation work for document saving. 1039 | * Use {@link isNewRecord} to determine whether the saving is 1040 | * for inserting or updating document. 1041 | * Make sure you call the parent implementation so that the event is raised properly. 1042 | * @return boolean whether the saving should be executed. Defaults to true. 1043 | */ 1044 | protected function beforeSave() 1045 | { 1046 | if($this->hasEventHandler('onBeforeSave')) 1047 | { 1048 | $event=new CModelEvent($this); 1049 | $this->onBeforeSave($event); 1050 | return $event->isValid; 1051 | } 1052 | else 1053 | return true; 1054 | } 1055 | 1056 | /** 1057 | * This method is invoked after saving a document successfully. 1058 | * The default implementation raises the {@link onAfterSave} event. 1059 | * You may override this method to do postprocessing after document saving. 1060 | * Make sure you call the parent implementation so that the event is raised properly. 1061 | */ 1062 | protected function afterSave() 1063 | { 1064 | if($this->hasEventHandler('onAfterSave')) 1065 | $this->onAfterSave(new CEvent($this)); 1066 | } 1067 | 1068 | /** 1069 | * This method is invoked before deleting a document. 1070 | * The default implementation raises the {@link onBeforeDelete} event. 1071 | * You may override this method to do any preparation work for document deletion. 1072 | * Make sure you call the parent implementation so that the event is raised properly. 1073 | * @return boolean whether the document should be deleted. Defaults to true. 1074 | */ 1075 | protected function beforeDelete() 1076 | { 1077 | if($this->hasEventHandler('onBeforeDelete')) 1078 | { 1079 | $event=new CModelEvent($this); 1080 | $this->onBeforeDelete($event); 1081 | return $event->isValid; 1082 | } 1083 | else 1084 | return true; 1085 | } 1086 | 1087 | /** 1088 | * This method is invoked after deleting a document. 1089 | * The default implementation raises the {@link onAfterDelete} event. 1090 | * You may override this method to do postprocessing after the document is deleted. 1091 | * Make sure you call the parent implementation so that the event is raised properly. 1092 | */ 1093 | protected function afterDelete() 1094 | { 1095 | if($this->hasEventHandler('onAfterDelete')) 1096 | $this->onAfterDelete(new CEvent($this)); 1097 | } 1098 | 1099 | /** 1100 | * This method is invoked before a find call. 1101 | * The find calls include {@link find}, {@link findAll}, {@link findByPk}, 1102 | * {@link findAllByPk}, {@link findByAttributes} and {@link findAllByAttributes}. 1103 | * The default implementation raises the {@link onBeforeFind} event. 1104 | * If you override this method, make sure you call the parent implementation 1105 | * so that the event is raised properly. 1106 | */ 1107 | protected function beforeFind() 1108 | { 1109 | if($this->hasEventHandler('onBeforeFind')) 1110 | { 1111 | $event=new CModelEvent($this); 1112 | // for backward compatibility 1113 | $event->criteria=func_num_args()>0 ? func_get_arg(0) : null; 1114 | $this->onBeforeFind($event); 1115 | } 1116 | } 1117 | 1118 | /** 1119 | * This method is invoked after each record is instantiated by a find method. 1120 | * The default implementation raises the {@link onAfterFind} event. 1121 | * You may override this method to do postprocessing after each newly found record is instantiated. 1122 | * Make sure you call the parent implementation so that the event is raised properly. 1123 | */ 1124 | protected function afterFind() 1125 | { 1126 | if($this->hasEventHandler('onAfterFind')) 1127 | $this->onAfterFind(new CEvent($this)); 1128 | } 1129 | 1130 | /** 1131 | * Calls {@link beforeFind}. 1132 | * This method is internally used. 1133 | * @since 1.0.11 1134 | */ 1135 | public function beforeFindInternal() 1136 | { 1137 | $this->beforeFind(); 1138 | } 1139 | 1140 | /** 1141 | * Calls {@link afterFind}. 1142 | * This method is internally used. 1143 | * @since 1.0.3 1144 | */ 1145 | public function afterFindInternal() 1146 | { 1147 | $this->afterFind(); 1148 | } 1149 | 1150 | /** 1151 | * Sets the solr query response. 1152 | * @param ASolrQueryResponse $solrResponse the response from solr that this model belongs to 1153 | */ 1154 | public function setSolrResponse($solrResponse) 1155 | { 1156 | $this->_solrResponse = $solrResponse; 1157 | } 1158 | 1159 | /** 1160 | * Gets the response from solr that this model belongs to 1161 | * @return ASolrQueryResponse the solr query response 1162 | */ 1163 | public function getSolrResponse() 1164 | { 1165 | return $this->_solrResponse; 1166 | } 1167 | 1168 | /** 1169 | * Sets the number of milliseconds to commit inserts and updates within. 1170 | * @param integer $commitWithin the number of milliseconds to commit within 1171 | */ 1172 | public function setCommitWithin($commitWithin) 1173 | { 1174 | $this->_commitWithin = $commitWithin; 1175 | } 1176 | 1177 | /** 1178 | * Gets the number of milliseconds to commit inserts and updates within. 1179 | * @return integer the number of milliseconds to commit within 1180 | */ 1181 | public function getCommitWithin() 1182 | { 1183 | return $this->_commitWithin; 1184 | } 1185 | 1186 | /** 1187 | * Sets the highlights for this record 1188 | * @param array $highlights the highlights, attribute => highlights 1189 | */ 1190 | public function setHighlights($highlights) 1191 | { 1192 | $this->_highlights = $highlights; 1193 | } 1194 | 1195 | /** 1196 | * Gets the highlights if highlighting is enabled 1197 | * @param string|null $attribute the attribute to get highlights for, if null all attributes will be returned 1198 | * @return array|boolean the highlighted results 1199 | */ 1200 | public function getHighlights($attribute = null) 1201 | { 1202 | if ($attribute === null) 1203 | return $this->_highlights; 1204 | if (!isset($this->_highlights[$attribute])) 1205 | return false; 1206 | return $this->_highlights[$attribute]; 1207 | } 1208 | /** 1209 | * Return the score analysis, from the debug section of the response if debugQuery was on, 1210 | * for the current document. 1211 | * @return string The score analysis as returned by Solr. 1212 | */ 1213 | public function getScoreAnalysis() { 1214 | return $this->getSolrResponse()->getScoreAnalysis($this->getPrimaryKey()); 1215 | } 1216 | } 1217 | 1218 | -------------------------------------------------------------------------------- /ASolrFacet.php: -------------------------------------------------------------------------------- 1 | 7 | * foreach($facet as $key => $value) { 8 | * echo $value." result(s) for ".$key."\n"; 9 | * 10 | * @author Charles Pick / PeoplePerHour.com 11 | * @package packages.solr 12 | */ 13 | class ASolrFacet extends CMap { 14 | const TYPE_QUERY = "facet_queries"; 15 | const TYPE_FIELD = "facet_fields"; 16 | const TYPE_DATE = "facet_dates"; 17 | const TYPE_RANGE = "facet_ranges"; 18 | /** 19 | * The name of the facet 20 | * @var string 21 | */ 22 | public $name; 23 | 24 | /** 25 | * The type of the facet 26 | * @var string 27 | */ 28 | public $type; 29 | 30 | /** 31 | * The solr query response this facet belongs to 32 | * @var ASolrQueryResponse 33 | */ 34 | public $response; 35 | } -------------------------------------------------------------------------------- /ASolrGroup.php: -------------------------------------------------------------------------------- 1 | 7 | * 'components' => array( 8 | * 'yourIndex' => array( 9 | * 'class' => 'packages.solr.ASolrLoadBalancer', 10 | * 'readConnection' => array( 11 | * 'clientOptions' => array( 12 | * 'hostname' => 'firstserver', 13 | * 'port' => '8080', 14 | * 'path' => '/solr/core_something' 15 | * ), 16 | * ), 17 | * 'writeConnection' => array( 18 | * 'clientOptions' => array( 19 | * 'hostname' => 'secondserver', 20 | * 'port' => '8080', 21 | * 'path' => '/solr/core_something' 22 | * ), 23 | * ) 24 | * ), 25 | * ), 26 | * 27 | * @package packages.solr 28 | * @author Charles Pick / PeoplePerHour.com 29 | */ 30 | class ASolrLoadBalancer extends CApplicationComponent implements IASolrConnection 31 | { 32 | /** 33 | * The connection used for reading from solr 34 | * @var ASolrConnection 35 | */ 36 | protected $_readConnection; 37 | 38 | /** 39 | * The connection used for writing to solr 40 | * @var ASolrConnection 41 | */ 42 | protected $_writeConnection; 43 | 44 | /** 45 | * Gets the solr client instance 46 | * @return SolrClient the solr client 47 | */ 48 | public function getClient() 49 | { 50 | if ($this->_writeConnection === null) 51 | if ($this->_readConnection === null) 52 | return null; 53 | else 54 | return $this->_readConnection->getClient(); 55 | else 56 | return $this->_writeConnection->getClient(); 57 | } 58 | 59 | /** 60 | * Adds a document to the solr index 61 | * @param ASolrDocument|SolrInputDocument $document the document to add to the index 62 | * @param integer $commitWithin the number of milliseconds to commit within after indexing the document 63 | * @return boolean true if the document was indexed successfully 64 | */ 65 | public function index($document, $commitWithin = null) 66 | { 67 | return $this->getWriteConnection()->index($document, $commitWithin); 68 | } 69 | 70 | /** 71 | * Deletes a document from the solr index 72 | * @param mixed $document the document to remove from the index, this can be the an id or an instance of ASoldDocument, an array of multiple values can also be used 73 | * @return boolean true if the document was deleted successfully 74 | */ 75 | public function delete($document) 76 | { 77 | return $this->getWriteConnection()->delete($document); 78 | } 79 | 80 | /** 81 | * Sends a commit command to solr. 82 | * @return boolean true if the commit was successful 83 | */ 84 | public function commit() 85 | { 86 | return $this->getWriteConnection()->commit(); 87 | } 88 | 89 | /** 90 | * Makes a solr search request 91 | * @param ASolrCriteria $criteria the search criteria 92 | * @param string $modelClass the name of the model to use when instantiating results 93 | * @return ASolrQueryResponse the response from solr 94 | */ 95 | public function search(ASolrCriteria $criteria, $modelClass = "ASolrDocument") 96 | { 97 | return $this->getReadConnection()->search($criteria, $modelClass); 98 | } 99 | 100 | /** 101 | * Counts the number of rows that match the given criteria 102 | * @param ASolrCriteria $criteria the search criteria 103 | * @return integer the number of matching rows 104 | */ 105 | public function count(ASolrCriteria $criteria) 106 | { 107 | return $this->getReadConnection()->count($criteria); 108 | } 109 | 110 | /** 111 | * Gets the last received solr query response 112 | * @return ASolrQueryResponse the last query response, or null if there are no responses yet 113 | */ 114 | public function getLastQueryResponse() 115 | { 116 | return $this->getReadConnection()->getLastQueryResponse(); 117 | } 118 | 119 | /** 120 | * Reset the solr client 121 | */ 122 | public function resetClient() 123 | { 124 | $this->getWriteConnection()->resetClient(); 125 | $this->getReadConnection()->resetClient(); 126 | } 127 | 128 | /** 129 | * Sets the connection used for reading from solr 130 | * @param ASolrConnection|array $readConnection the solr connection or config 131 | */ 132 | public function setReadConnection($readConnection) 133 | { 134 | if (!($readConnection instanceof IASolrConnection)) { 135 | $attributes = $readConnection; 136 | $readConnection = new ASolrConnection(); 137 | foreach($attributes as $attribute => $value) 138 | $readConnection->{$attribute} = $value; 139 | } 140 | $this->_readConnection = $readConnection; 141 | } 142 | 143 | /** 144 | * Gets the connection used for reading from solr 145 | * @return ASolrConnection the solr connection 146 | */ 147 | public function getReadConnection() 148 | { 149 | return $this->_readConnection; 150 | } 151 | 152 | /** 153 | * Sets the connection used for writing to solr 154 | * @param ASolrConnection $writeConnection the solr connection or config 155 | */ 156 | public function setWriteConnection($writeConnection) 157 | { 158 | if (!($writeConnection instanceof IASolrConnection)) { 159 | $attributes = $writeConnection; 160 | $writeConnection = new ASolrConnection(); 161 | foreach($attributes as $attribute => $value) 162 | $writeConnection->{$attribute} = $value; 163 | } 164 | $this->_writeConnection = $writeConnection; 165 | } 166 | 167 | /** 168 | * Gets the connection used for writing to solr 169 | * @return ASolrConnection 170 | */ 171 | public function getWriteConnection() 172 | { 173 | if ($this->_writeConnection === null) 174 | return $this->_readConnection; 175 | return $this->_writeConnection; 176 | } 177 | 178 | } -------------------------------------------------------------------------------- /ASolrQueryResponse.php: -------------------------------------------------------------------------------- 1 | _solrObject = $solrObject; 65 | $this->_criteria = $criteria; 66 | $this->_modelClass = $modelClass; 67 | } 68 | 69 | /** 70 | * Gets a list of groups in the search results, if this is 71 | * a group query. 72 | * @return ASolrGroup[] a list of groups 73 | */ 74 | public function getGroups() { 75 | if ($this->_groups === null) 76 | $this->loadGroups(); 77 | return $this->_groups; 78 | } 79 | 80 | /** 81 | * Loads the groups in the result set, if any 82 | * @return boolean true if the groups were loaded successfully 83 | */ 84 | protected function loadGroups() 85 | { 86 | $this->_groups = new CAttributeCollection; 87 | if (!isset($this->getSolrObject()->grouped)) 88 | return false; 89 | foreach($this->getSolrObject()->grouped as $name => $rawGroup) { 90 | $group = new ASolrGroup(); 91 | $group->name = $name; 92 | $group->total = $rawGroup->matches; 93 | $this->processResults($rawGroup->doclist->docs, $group); 94 | $this->_groups->{$name} = $group; 95 | } 96 | return true; 97 | } 98 | /** 99 | * Gets an array of date facets that belong to this query response 100 | * @return ASolrFacet[] 101 | */ 102 | public function getDateFacets() 103 | { 104 | if ($this->_dateFacets === null) 105 | $this->loadFacets(); 106 | 107 | return $this->_dateFacets; 108 | } 109 | 110 | /** 111 | * Gets an array of field facets that belong to this query response 112 | * @return ASolrFacet[] 113 | */ 114 | public function getFieldFacets() 115 | { 116 | if ($this->_fieldFacets === null) 117 | $this->loadFacets(); 118 | 119 | return $this->_fieldFacets; 120 | } 121 | /** 122 | * Gets an array of query facets that belong to this query response 123 | * @return ASolrFacet[] 124 | */ 125 | public function getQueryFacets() 126 | { 127 | if ($this->_queryFacets === null) 128 | $this->loadFacets(); 129 | 130 | return $this->_queryFacets; 131 | } 132 | /** 133 | * Gets an array of range facets that belong to this query response 134 | * @return ASolrFacet[] 135 | */ 136 | public function getRangeFacets() 137 | { 138 | if ($this->_rangeFacets === null) 139 | $this->loadFacets(); 140 | 141 | return $this->_rangeFacets; 142 | } 143 | /** 144 | * Loads the facet objects 145 | * @return boolean true if facets were loaded 146 | */ 147 | protected function loadFacets() { 148 | $this->_dateFacets = new CAttributeCollection(); 149 | $this->_dateFacets->caseSensitive = true; 150 | $this->_fieldFacets = new CAttributeCollection(); 151 | $this->_fieldFacets->caseSensitive = true; 152 | $this->_queryFacets = new CAttributeCollection(); 153 | $this->_queryFacets->caseSensitive = true; 154 | $this->_rangeFacets = new CAttributeCollection(); 155 | $this->_rangeFacets->caseSensitive = true; 156 | if (!isset($this->getSolrObject()->facet_counts)) 157 | return false; 158 | 159 | foreach($this->getSolrObject()->facet_counts as $facetType => $item) { 160 | foreach($item as $facetName => $values) { 161 | /* 162 | * Using @ to suppress weird errors occactionally coming 163 | * from the solr pecl extension: 164 | * e.g. "Illegal member variable name" 165 | */ 166 | if (@is_object($values)) 167 | $values = (array) $values; 168 | elseif (!@is_array($values)) 169 | $values = array("value" => $values); 170 | 171 | $facet = new ASolrFacet($values); 172 | $facet->name = $facetName; 173 | $facet->type = $facetType; 174 | switch ($facetType) { 175 | case ASolrFacet::TYPE_DATE: 176 | $this->_dateFacets[$facet->name] = $facet; 177 | break; 178 | case ASolrFacet::TYPE_FIELD: 179 | $this->_fieldFacets[$facet->name] = $facet; 180 | break; 181 | case ASolrFacet::TYPE_QUERY: 182 | $this->_queryFacets[$facet->name] = $facet; 183 | break; 184 | case ASolrFacet::TYPE_RANGE: 185 | $this->_rangeFacets[$facet->name] = $facet; 186 | break; 187 | } 188 | } 189 | } 190 | return true; 191 | } 192 | 193 | 194 | /** 195 | * Gets the SolrObject object wrapped by this class 196 | * @return SolrObject the solr query response object 197 | */ 198 | public function getSolrObject() 199 | { 200 | return $this->_solrObject; 201 | } 202 | 203 | /** 204 | * Gets the list of search results 205 | * @return ASolrResultList the solr results 206 | */ 207 | public function getResults() 208 | { 209 | if ($this->_results === null) { 210 | $this->_results = new ASolrResultList; 211 | $this->_results->total = isset($this->_solrObject->response->numFound) ? $this->_solrObject->response->numFound : 0; 212 | if ($this->_results->total) 213 | $this->processResults($this->_solrObject->response->docs,$this->_results); 214 | } 215 | return $this->_results; 216 | } 217 | 218 | /** 219 | * Processes a list of results 220 | * @param SolrObject $rawResults the raw results to process 221 | * @param ASolrResultList $list the list of results 222 | */ 223 | protected function processResults($rawResults, $list = null) { 224 | if ($list === null) 225 | $list = new ASolrResultList; 226 | $modelClass = $this->_modelClass; 227 | $highlighting = isset($this->_solrObject->highlighting); 228 | if ($highlighting) 229 | $highlights = array_values((array) $this->_solrObject->highlighting); 230 | if ($rawResults) { 231 | foreach($rawResults as $n => $row) { 232 | $result = $modelClass::model()->populateRecord($row); /* @var ASolrDocument $result */ 233 | $result->setPosition($n + $this->_criteria->getOffset()); 234 | $result->setSolrResponse($this); 235 | if ($highlighting && isset($highlights[$n])) 236 | $result->setHighlights($highlights[$n]); 237 | 238 | $list->add($result); 239 | } 240 | } 241 | return $list; 242 | } 243 | public function getScoreAnalysis($id) { 244 | return isset($this->getSolrObject()->debug['explain'][$id]) ? 245 | $this->getSolrObject()->debug['explain'][$id] : 246 | null; 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /ASolrResultList.php: -------------------------------------------------------------------------------- 1 | 7 | * $model = User::model()->find(); 8 | * $behavior = new ASolrSearchable(); 9 | * $behavior->attributes = array( 10 | * "name", "skills", "country.name" 11 | * ); 12 | * $model->attachBehavior("ASolrSearchable", $behavior); 13 | * $model->index(); // adds the user to the solr index 14 | * 15 | * $model->name = "Test User"; 16 | * $model->save(); // document is automatically reindexed 17 | * $model->address = "123 Fake Street"; 18 | * $model->save(); // document is not reindexed because we don't care about the address field 19 | * 20 | * $criteria = new ASolrCriteria; 21 | * $criteria->query = "name:'Test User'"; 22 | * $users = $model->findAllBySolr($criteria); // all the users with the name "Test User" 23 | * 24 | * $model->delete(); // document is automatically deleted from solr after the model is deleted 25 | * 26 | * @package packages.solr 27 | * @author Charles Pick / PeoplePerHour.com 28 | */ 29 | class ASolrSearchable extends CActiveRecordBehavior { 30 | /** 31 | * The class name of the solr document to instantiate 32 | * @var string 33 | */ 34 | public $documentClass = "ASolrDocument"; 35 | 36 | /** 37 | * Whether to automatically index or reindex the document when it changes. 38 | * Defaults to true. 39 | * You can also pass a method that will be evaluated in order to decide if 40 | * the current model should be indexed or not 41 | * eg: autoIndex => function() { return true; }, 42 | * while in the definition of attributes in the models behaviors() method 43 | * @var boolean | callable 44 | */ 45 | public $autoIndex = true; 46 | 47 | /** 48 | * Whether to be smart about when to reindex documents. 49 | * If this is true, changes will be pushed to solr only if attributes that 50 | * we care about have changed. 51 | * @var boolean 52 | */ 53 | public $smartIndex = true; 54 | 55 | /** 56 | * The configuration for the associated ASolrDocument class 57 | * @var array 58 | */ 59 | public $solrDocumentConfig = array(); 60 | /** 61 | * The solr document associated with this model instance 62 | * @var ASolrDocument 63 | */ 64 | protected $_solrDocument; 65 | 66 | /** 67 | * The solr criteria associated with this model instance 68 | * @var ASolrCriteria 69 | */ 70 | protected $_solrCriteria; 71 | 72 | /** 73 | * The attributes that should be indexed in solr 74 | * @var array 75 | */ 76 | protected $_attributes; 77 | 78 | /** 79 | * Stores the attributes of the model after it is found. 80 | * Used to determine whether any of the attributes we care about have changed or not 81 | * @var array 82 | */ 83 | protected $_oldAttributes = array(); 84 | 85 | /** 86 | * Sets the attributes that should be indexed in solr 87 | * @param array $attributes 88 | */ 89 | public function setAttributes($attributes) 90 | { 91 | $a = array(); 92 | foreach($attributes as $key => $value) { 93 | if (is_integer($key)) { 94 | $key = $value; 95 | } 96 | $a[$key] = $value; 97 | } 98 | $this->_attributes = $a; 99 | } 100 | 101 | /** 102 | * Gets the attributes that should be indexed in solr 103 | * @return array 104 | */ 105 | public function getAttributes() 106 | { 107 | if ($this->_attributes === null) { 108 | $names = $this->getOwner()->attributeNames(); 109 | $this->_attributes = array_combine($names,$names); 110 | } 111 | return $this->_attributes; 112 | } 113 | /** 114 | * Gets a list of objects and attributes that 115 | * @return array a multidimensional array of objects and attributes 116 | */ 117 | protected function resolveAttributes() { 118 | $names = array(); 119 | foreach($this->getAttributes() as $modelAttribute => $docAttribute) { 120 | if (!strstr($modelAttribute,".")) { 121 | $names[$modelAttribute] = array($this->getOwner(),$modelAttribute); 122 | continue; 123 | } 124 | $reference = $this->getOwner(); /* @var CActiveRecord $reference */ 125 | $pointers = explode(".",$modelAttribute); 126 | $lastItem = array_pop($pointers); 127 | foreach($pointers as $pointer) { 128 | $reference = $reference->{$pointer}; 129 | } 130 | $names[$modelAttribute] = array($reference, $lastItem); 131 | } 132 | return $names; 133 | } 134 | 135 | /** 136 | * Resolves the attribute name to the field name on solr. 137 | * Default implementation replaces "." with "__" (double underscore) 138 | *
139 | * echo $behavior->resolveAttributeName("name"); // "name" 140 | * echo $behavior->resolveAttributeName("country.name"); // "country__name" 141 | *142 | * @param $attributeName 143 | * @return mixed 144 | */ 145 | protected function resolveAttributeName($attributeName) { 146 | $attributes = $this->getAttributes(); 147 | $attributeName = $attributes[$attributeName]; 148 | return str_replace(".","__",$attributeName); 149 | } 150 | 151 | /** 152 | * Resolves the value of an attribute on an owner object. 153 | * 154 | * @param mixed $owner the object or array of objects that the attribute belongs to 155 | * @param string $attribute the name of the attribute to get the value for 156 | * @return mixed the attribute value 157 | */ 158 | protected function resolveAttributeValue($owner, $attribute) { 159 | if (is_array($owner)) { 160 | $value = array(); 161 | foreach($owner as $item) 162 | if (is_array($item) && isset($item[$attribute])) 163 | $value[] = $item[$attribute]; 164 | else if (is_object($item) && isset($item->{$attribute})) 165 | $value[] = $item->{$attribute}; 166 | return $value; 167 | } 168 | return isset($owner->{$attribute}) ? $owner->{$attribute} : null; 169 | } 170 | 171 | /** 172 | * Sets the solr document associated with this model instance 173 | * @param ASolrDocument $solrDocument the solr document 174 | */ 175 | public function setSolrDocument($solrDocument) 176 | { 177 | $this->_solrDocument = $solrDocument; 178 | } 179 | 180 | /** 181 | * Gets the solr document associated with this model instance. 182 | * @param boolean $refresh whether to refresh the document, defaults to false 183 | * @return ASolrDocument the solr document 184 | */ 185 | public function getSolrDocument($refresh = false) 186 | { 187 | if ($this->_solrDocument === null || $refresh) { 188 | $config = $this->solrDocumentConfig; 189 | $config['class'] = $this->documentClass; 190 | $this->_solrDocument = Yii::createComponent($config); 191 | 192 | foreach($this->resolveAttributes() as $attribute => $item) { 193 | list($object, $property) = $item; 194 | $resolvedAttributeName = $this->resolveAttributeName($attribute); 195 | $this->_solrDocument->{$resolvedAttributeName} = $this->resolveAttributeValue($object, $property); 196 | } 197 | } 198 | return $this->_solrDocument; 199 | } 200 | 201 | /** 202 | * Adds the solr document to the index 203 | * @return boolean true if the document was indexed successfully 204 | */ 205 | public function index() { 206 | if (!$this->isIndexable()) 207 | return true; 208 | $document = $this->getSolrDocument(true); 209 | if (!$document->save()) { 210 | return false; 211 | } 212 | if ($this->smartIndex) { 213 | $this->_oldAttributes = array(); 214 | foreach($this->resolveAttributes() as $key => $item) { 215 | list($object, $property) = $item; 216 | $this->_oldAttributes[$key] = $this->resolveAttributeValue($object, $property); 217 | } 218 | } 219 | return true; 220 | } 221 | /** 222 | * Triggered after the attached model is found. 223 | * Stores the current state of attributes we care about to see if they have changed. 224 | * @param CEvent $event the event raised 225 | */ 226 | public function afterFind($event) { 227 | if ($this->smartIndex) { 228 | $this->_oldAttributes = array(); 229 | foreach($this->resolveAttributes() as $key => $item) { 230 | list($object, $property) = $item; 231 | $this->_oldAttributes[$key] = $this->resolveAttributeValue($object, $property); 232 | } 233 | } 234 | } 235 | 236 | /** 237 | * Deletes the relevant document from the solr index after the model is deleted 238 | * @param CEvent $event the event raised 239 | */ 240 | public function afterDelete($event) { 241 | $this->getSolrDocument()->delete(); 242 | } 243 | /** 244 | * Adds the relevant document to the solr index after the model is saved if $this->autoIndex is true. 245 | * For existing records, the document will only be re-indexed if attributes we care about have changed. 246 | * @param CEvent $event the event raised 247 | */ 248 | public function afterSave($event) { 249 | if (!$this->isIndexable() || !$this->getIsModified()) { 250 | return; 251 | } 252 | $this->index(); 253 | } 254 | /** 255 | * Finds an active record that matches the given criteria using solr 256 | * @param ASolrCriteria $criteria the solr criteria to use for searching 257 | * @return CActiveRecord|null the found record, or null if nothing was found 258 | */ 259 | public function findBySolr($criteria = null) { 260 | $c = new ASolrCriteria(); 261 | $c->mergeWith($this->getSolrCriteria()); 262 | if ($criteria !== null) { 263 | $c->mergeWith($criteria); 264 | } 265 | if ($c->getQuery() == "") { 266 | $c->setQuery("*:*"); 267 | } 268 | $document = $this->getSolrDocument()->find($c); 269 | if (!is_object($document)) { 270 | return null; 271 | } 272 | return $this->populateFromSolr($document,false); 273 | } 274 | 275 | /** 276 | * Finds all active records that matches the given criteria using solr 277 | * @param ASolrCriteria $criteria the solr criteria to use for searching 278 | * @return CActiveRecord[] an array of results 279 | */ 280 | public function findAllBySolr($criteria = null) { 281 | $c = new ASolrCriteria(); 282 | $c->mergeWith($this->getSolrCriteria()); 283 | if ($criteria !== null) { 284 | $c->mergeWith($criteria); 285 | } 286 | if ($c->getQuery() == "") { 287 | $c->setQuery("*:*"); 288 | } 289 | return $this->populateFromSolr($this->getSolrDocument()->findAll($c),true); 290 | 291 | } 292 | 293 | /** 294 | * Populates active record objects from solr 295 | * @param ASolrDocument|array $document the document(s) to populate the records from 296 | * @param boolean $all whether to populate a list of records instead of just one, defaults to false 297 | * @return CActiveRecord|array the active record(s) populated from solr 298 | */ 299 | public function populateFromSolr($document, $all = false) { 300 | if ($all) { 301 | $results = array(); 302 | foreach($document as $doc) { 303 | $results[] = $this->populateFromSolr($doc,false); 304 | } 305 | return $results; 306 | } 307 | $relations = $this->getOwner()->getMetaData()->relations; 308 | $attributes = array(); 309 | $relationAttributes = array(); 310 | foreach($this->getAttributes() as $modelAttribute => $docAttribute) { 311 | $resolved = $this->resolveAttributeName($modelAttribute); 312 | if (!strstr($modelAttribute,".")) { 313 | $attributes[$modelAttribute] = $document->{$resolved}; 314 | continue; 315 | } 316 | $reference = &$relationAttributes; 317 | $pointers = explode(".",$modelAttribute); 318 | $last = array_pop($pointers); 319 | foreach($pointers as $pointer) { 320 | if (!isset($reference[$pointer])) { 321 | $reference[$pointer] = array(); 322 | } 323 | $reference =& $reference[$pointer]; 324 | } 325 | $reference[$last] = $document->{$resolved}; 326 | } 327 | $modelClass = get_class($this->getOwner()); 328 | $model = $modelClass::model()->populateRecord($attributes); 329 | if (count($relationAttributes)) { 330 | foreach($relationAttributes as $relationName => $attributes) { 331 | $relationClass = $relations[$relationName]->className; 332 | $model->{$relationName} = $relationClass::model()->populateRecord($attributes); 333 | } 334 | } 335 | return $model; 336 | } 337 | 338 | /** 339 | * Determines whether any attributes that we care about on the model have been modified or not. 340 | * @return boolean true if the item has been modified, otherwise false 341 | */ 342 | public function getIsModified() { 343 | if (!$this->smartIndex || count($this->_oldAttributes) == 0) { 344 | return true; 345 | } 346 | foreach($this->resolveAttributes() as $key => $item) { 347 | if (!isset($this->_oldAttributes[$key])) { 348 | return true; 349 | } 350 | list($object, $property) = $item; 351 | 352 | if ($this->_oldAttributes[$key] != $this->resolveAttributeValue($object, $property)) { 353 | return true; 354 | } 355 | } 356 | return false; 357 | } 358 | 359 | /** 360 | * Resets the scope 361 | * @return ASolrSearchable $this with the scope reset 362 | */ 363 | public function resetScope() { 364 | $this->_solrCriteria = null; 365 | return $this; 366 | } 367 | 368 | /** 369 | * Sets the solr criteria associated with this model 370 | * @param ASolrCriteria $solrCriteria the solr criteria 371 | */ 372 | public function setSolrCriteria($solrCriteria) { 373 | $this->_solrCriteria = $solrCriteria; 374 | } 375 | 376 | /** 377 | * Gets the solr criteria associated with this model 378 | * @return ASolrCriteria the solr criteria 379 | */ 380 | public function getSolrCriteria() { 381 | if ($this->_solrCriteria === null) { 382 | $this->_solrCriteria = new ASolrCriteria(); 383 | } 384 | return $this->_solrCriteria; 385 | } 386 | 387 | /** 388 | * Checks whether the current model should be indexed or not. 389 | * @return bool 390 | */ 391 | protected function isIndexable() { 392 | return is_callable($this->autoIndex) ? call_user_func($this->autoIndex) : $this->autoIndex; 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /ASolrSort.php: -------------------------------------------------------------------------------- 1 | array( 11 | * 'asc'=>'created_at asc', 12 | * 'desc'=>'created_at desc', 13 | * ), 14 | * 15 | * // Solr parameters can be defined like this: 16 | * 'keyword'=>array( 17 | * 'asc'=>array( 18 | * // Each of these parameters is set through setParam() in Solr. 19 | * // Make sure you also have a 'sort' parameter here. 20 | * 'sortingQ' =>'{!edismax qf="title^8.0"} '.$keyword, 21 | * 'sort' =>'product(query($sortingQ),scale(stars,1,5)) asc', 22 | * ), 23 | * 'desc'=>array( 24 | * 'sort' =>'product(query($sortingQ),scale(stars,1,5)) desc', 25 | * 'sortingQ' =>'{!edismax qf="title^8.0"} '.$keyword, 26 | * ), 27 | * ), 28 | * ) 29 | * Represents information relevant to solr sorting 30 | * @author Charles Pick / PeoplePerHour.com 31 | * @package packages.solr 32 | */ 33 | class ASolrSort extends CSort 34 | { 35 | /** 36 | * @var mixed default order as string or array of solr params 37 | */ 38 | public $defaultOrder; 39 | 40 | public function applyOrder($criteria) 41 | { 42 | $directions=$this->getDirections(); 43 | if(empty($directions)) 44 | { 45 | if(!empty($this->defaultOrder)) 46 | $criteria->order=$this->defaultOrder; 47 | } 48 | elseif(isset($directions['sort'])) 49 | { 50 | // Special case: getDirections returned defaultOrder and it was an array of Solr params 51 | foreach($directions as $name=>$value) 52 | $criteria->setParam($name,$value); 53 | } 54 | else 55 | { 56 | $orders=array(); 57 | foreach($directions as $attribute=>$descending) 58 | { 59 | $definition=$this->resolveAttribute($attribute); 60 | if(is_array($definition)) 61 | { 62 | $dir=$descending ? 'desc' : 'asc'; 63 | if(isset($definition[$dir]) && is_array($definition[$dir])) 64 | foreach($definition[$dir] as $name=>$value) 65 | { 66 | $criteria->setParam($name,$value); 67 | } 68 | else 69 | $orders[]=isset($definition[$dir]) ? $definition[$dir] : $attribute.($descending ? ' DESC':''); 70 | } 71 | else if($definition!==false) 72 | { 73 | $attribute=$definition; 74 | $orders[]=$descending?$attribute.' DESC':$attribute; 75 | } 76 | } 77 | if($orders!==array()) 78 | $criteria->order=implode(', ',$orders); 79 | } 80 | } 81 | 82 | /** 83 | * Solr does not support ORDER BY 84 | */ 85 | public function getOrderBy($criteria = NULL) 86 | { 87 | throw new CException('Solr sorting does not support ORDER BY'); 88 | } 89 | 90 | /** 91 | * Returns the real definition of an attribute given its name. 92 | * 93 | * The resolution is based on {@link attributes} and {@link CActiveRecord::attributeNames}. 94 | *
10 | pecl install solr 11 |12 | If this command fails, you may find it easier to compile the pecl solr extension from source. 13 | 14 | 15 | If you do not already have a packages directory and alias set up, first create a directory called "packages" in your application folder. 16 | Then add an alias to your main config, e.g. 17 |
18 | "aliases" => array( 19 | "packages" => dirname(__DIR__)."/packages/", 20 | ), 21 | ... 22 |23 | 24 | Now extract the files to packages/solr 25 | 26 | #### Running unit tests 27 | 28 | The unit tests depend on sqlite to provide the test data, please ensure the php PDO sqlite module is installed before continuing. 29 | 30 | The unit tests also depend on the example index that ships with solr. 31 | Go to your solr installation directory and in the "example" folder run 32 |
33 | java -jar start.jar 34 |35 | This should start the solr server running on port 8983 by default. If you're using a different port, please configure it in the packages/solr/tests/common.php file. 36 | 37 | Now go to your application tests directory, usually protected/tests and run the following command: 38 | 39 |
40 | phpunit --verbose ../packages/solr/tests 41 |42 | 43 | This will run the unit tests, if all went well they should all pass, otherwise please check your configuration. 44 | 45 | #### Configuring your solr connection 46 | 47 | Before we can use solr in our application, we must configure a connection to use. 48 | In the application config, add the following 49 |
50 | "components" => array( 51 | ... 52 | "solr" => array( 53 | "class" => "packages.solr.ASolrConnection", 54 | "clientOptions" => array( 55 | "hostname" => "localhost", 56 | "port" => 8983, 57 | ), 58 | ), 59 | ), 60 |61 | 62 | This will configure an application component called "solr". 63 | If you're dealing with more than one index, define a new solr connection for each one, giving each a unique name. 64 | 65 | 66 | #### Indexing a document with solr 67 | 68 | 69 | To add a document to solr we use the {@link ASolrDocument} class. 70 | Example: 71 |
72 | $doc = new ASolrDocument; 73 | $doc->id = 123; 74 | $doc->name = "test document"; 75 | $doc->save(); // adds the document to solr 76 |77 | Remember - Your changes won't appear in solr until a commit occurs. 78 | If you need your data to appear immediately, use the following syntax: 79 |
80 | $doc->getSolrConnection()->commit(); 81 |82 | If you need to deal with multiple solr indexes, it's often best to define a model for 83 | each index you're dealing with. To do this we extend ASolrDocument in the same way that we would extend CActiveRecord when defining a model 84 | For example: 85 |
86 | class Job extends ASolrDocument { 87 | /** 88 | * Required for all ASolrDocument sub classes 89 | * @see ASolrDocument::model() 90 | */ 91 | public static function model($className = __CLASS__) { 92 | return parent::model($className); 93 | } 94 | /** 95 | * @return ASolrConnection the solr connection to use for this model 96 | */ 97 | public function getSolrConnection() { 98 | return Yii::app()->yourCustomSolrConnection; 99 | } 100 | } 101 |102 | 103 | #### Searching solr 104 | 105 | To find documents in solr, we use the following methods: 106 |
118 | $criteria = new ASolrCriteria; 119 | $criteria->query = "name:test"; // lucene query syntax 120 | $docs = ASolrDocument::model()->findAll($criteria); 121 |122 | Alternative method: 123 |
124 | $docs = ASolrDocument::model()->findAllByAttributes(array("name" => "test")); 125 |126 | 127 | 128 | Example: Find a job with the unique id of 123 129 |
130 | $job = Job::model()->findByPk(123); 131 |132 | Example: Find the total number of jobs in the index 133 |
134 | $criteria = new ASolrCriteria; 135 | $criteria->query = "*"; // match everything 136 | $total = Job::model()->count($criteria); // the total number of jobs in the index 137 |138 | 139 | #### Using data providers 140 | 141 | Often we need to use a data provider to retrieve paginated lists of results. 142 | Example: 143 |
144 | $dataProvider = new ASolrDataProvider(Job::model()); 145 | $dataProvider->criteria->query = "*"; 146 | foreach($dataProvider->getData() as $job) { 147 | echo $job->title."\n"; 148 | } 149 |150 | 151 | 152 | #### Removing items from the index 153 | To remove an item from the index, use the following syntax: 154 |
155 | $job = Job::model()->findByPk(234); 156 | $job->delete(); 157 |-------------------------------------------------------------------------------- /tests/ASolrConnectionTest.php: -------------------------------------------------------------------------------- 1 | clientOptions->hostname = SOLR_HOSTNAME; 16 | $connection->clientOptions->port = SOLR_PORT; 17 | $connection->clientOptions->path = SOLR_PATH; 18 | $client = $connection->getClient(); 19 | $this->assertTrue($client instanceof SolrClient); 20 | $connection->clientOptions = array(); 21 | $connection->setClient(null); 22 | $connection->getClient(); // should throw an exception 23 | } 24 | /** 25 | * Tests adding and removing a document from solr 26 | */ 27 | public function testIndexAndDelete() { 28 | $connection = new ASolrConnection(); 29 | $connection->clientOptions->hostname = SOLR_HOSTNAME; 30 | $connection->clientOptions->port = SOLR_PORT; 31 | $connection->clientOptions->path = SOLR_PATH; 32 | $doc = new SolrInputDocument(); 33 | 34 | $doc->addField('id', 334455); 35 | $doc->addField('cat', 'Software'); 36 | $doc->addField('cat', 'Lucene'); 37 | $this->assertTrue($connection->index($doc)); 38 | $this->assertTrue($connection->delete(334455)); 39 | 40 | // now test bulk actions 41 | $docList = array(); 42 | for($i = 0; $i < 10; $i++) { 43 | $item = clone $doc; 44 | $item->getField("id")->values[0] = 999999 + $i; 45 | $docList[] = $item; 46 | } 47 | $this->assertTrue($connection->index($docList)); 48 | $this->assertTrue($connection->delete(array_map( 49 | function($a) { 50 | return $a->getField("id")->values[0]; 51 | }, 52 | $docList))); 53 | $this->assertTrue($connection->commit()); 54 | } 55 | /** 56 | * Tests the faceted search functions 57 | */ 58 | public function testFacetedSearch() { 59 | $connection = new ASolrConnection(); 60 | $connection->clientOptions->hostname = SOLR_HOSTNAME; 61 | $connection->clientOptions->port = SOLR_PORT; 62 | $connection->clientOptions->path = SOLR_PATH; 63 | $doc = new SolrInputDocument(); 64 | 65 | $doc->addField('id', 334455); 66 | $doc->addField('cat', 'Software'); 67 | $doc->addField('cat', 'Lucene'); 68 | $doc->addField("popularity",20); 69 | $doc->addField("incubationdate_dt",date("Y-m-d\TH:i:s\Z")); 70 | $this->assertTrue($connection->index($doc)); 71 | $this->assertTrue($connection->commit()); 72 | $criteria = new ASolrCriteria; 73 | $criteria->query = "lucene"; 74 | $criteria->offset = 0; 75 | $criteria->limit = 10; 76 | $criteria->facet = true; 77 | $criteria->addFacetField("cat")->addFacetField("name"); 78 | $criteria->addFacetDateField("incubationdate_dt"); 79 | $criteria->facetDateStart = "2005-10-10T00:00:00Z"; 80 | $criteria->facetDateEnd = "2015-10-10T00:00:00Z"; 81 | $criteria->facetDateGap = "+12MONTH"; 82 | $criteria->addFacetQuery("popularity:[* TO 10]"); 83 | $criteria->addFacetQuery("popularity:[10 TO 20]"); 84 | $criteria->addFacetQuery("popularity:[20 TO *]"); 85 | 86 | $response = $connection->search($criteria); 87 | $this->assertTrue($response instanceof ASolrQueryResponse); 88 | $this->assertTrue(isset($response->getDateFacets()->incubationdate_dt)); 89 | $this->assertTrue($response->getDateFacets()->incubationdate_dt instanceof ASolrFacet); 90 | 91 | $results = $response->getResults(); 92 | $this->assertTrue(count($results) > 0); 93 | foreach($results as $n => $result) { 94 | $this->assertEquals($n, $result->getPosition()); 95 | } 96 | $this->assertTrue($connection->delete(334455)); 97 | $this->assertTrue($connection->commit()); 98 | } 99 | 100 | /** 101 | * Tests performing a facetted search without getting results at the same time 102 | */ 103 | public function testFacetsWithoutResults() { 104 | $connection = new ASolrConnection(); 105 | $connection->clientOptions->hostname = SOLR_HOSTNAME; 106 | $connection->clientOptions->port = SOLR_PORT; 107 | $connection->clientOptions->path = SOLR_PATH; 108 | $doc = new SolrInputDocument(); 109 | 110 | $doc->addField('id', 334455); 111 | $doc->addField('cat', 'Software'); 112 | $doc->addField('cat', 'Lucene'); 113 | $doc->addField("popularity",20); 114 | $doc->addField("incubationdate_dt",date("Y-m-d\TH:i:s\Z")); 115 | $this->assertTrue($connection->index($doc)); 116 | $this->assertTrue($connection->commit()); 117 | $criteria = new ASolrCriteria; 118 | $criteria->query = "lucene"; 119 | $criteria->offset = 0; 120 | $criteria->limit = 0; 121 | $criteria->facet = true; 122 | $criteria->addFacetField("cat")->addFacetField("name"); 123 | $criteria->addFacetDateField("incubationdate_dt"); 124 | $criteria->facetDateStart = "2005-10-10T00:00:00Z"; 125 | $criteria->facetDateEnd = "2015-10-10T00:00:00Z"; 126 | $criteria->facetDateGap = "+12MONTH"; 127 | $criteria->addFacetQuery("popularity:[* TO 10]"); 128 | $criteria->addFacetQuery("popularity:[10 TO 20]"); 129 | $criteria->addFacetQuery("popularity:[20 TO *]"); 130 | 131 | $response = $connection->search($criteria); 132 | $this->assertTrue($response instanceof ASolrQueryResponse); 133 | $this->assertTrue(isset($response->getDateFacets()->incubationdate_dt)); 134 | $this->assertTrue($response->getDateFacets()->incubationdate_dt instanceof ASolrFacet); 135 | 136 | $results = $response->getResults(); 137 | $this->assertEquals(0, count($results)); 138 | $this->assertTrue($connection->delete(334455)); 139 | $this->assertTrue($connection->commit()); 140 | } 141 | 142 | } -------------------------------------------------------------------------------- /tests/ASolrCriteriaTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(isset($criteria->fields)); 16 | $this->assertTrue(isset($criteria->filterQueries)); 17 | } 18 | /** 19 | * Tests the limit and offset magic getters / setters that are required for compatibility with pagination 20 | */ 21 | public function testLimitAndOffset() { 22 | $criteria = new ASolrCriteria(); 23 | $this->assertEquals(null,$criteria->limit); 24 | $criteria->limit = 100; 25 | $this->assertEquals(100,$criteria->getRows()); 26 | $this->assertEquals(null, $criteria->offset); 27 | $criteria->offset = 20; 28 | $this->assertEquals(20,$criteria->getStart()); 29 | } 30 | 31 | /** 32 | * Gets the getOrder() and setOrder() methods 33 | */ 34 | public function testOrder() { 35 | $criteria = new ASolrCriteria(); 36 | $criteria->order = "id DESC, other_field ASC, somethingElse"; 37 | $this->assertEquals("id DESC, other_field ASC, somethingElse",$criteria->getParam("sort")); 38 | } 39 | 40 | /** 41 | * Tests the mergeWith() method 42 | */ 43 | public function testMergeWith() { 44 | $criteria = new ASolrCriteria(); 45 | $criteria->addField("id"); 46 | $criteria->query = "id:1"; 47 | $criteria2 = new ASolrCriteria(); 48 | $criteria2->addField("name"); 49 | $criteria2->query = "id:2"; 50 | $criteria->addFilterQuery("test:1"); 51 | $criteria2->addFilterQuery("test:2"); 52 | $criteria->mergeWith($criteria2); 53 | $this->assertTrue(in_array("name",$criteria->getFields())); 54 | $this->assertEquals("(id:1) AND (id:2)",$criteria->query); 55 | } 56 | } -------------------------------------------------------------------------------- /tests/ASolrDataProviderTest.php: -------------------------------------------------------------------------------- 1 | getConnection(); 14 | foreach($this->fixtureData() as $attributes) { 15 | $doc = new ASolrDocument(); 16 | $doc->setAttributes($attributes,false); 17 | $this->assertTrue($doc->save()); 18 | } 19 | $this->getConnection()->commit(); 20 | } 21 | /** 22 | * Tests the basic functionality 23 | */ 24 | public function testBasics() { 25 | $dataProvider = new ASolrDataProvider("ASolrDocument"); 26 | $dataProvider->getCriteria()->query = "name:test"; 27 | $data = $dataProvider->getData(); 28 | $this->assertEquals(10,count($data)); 29 | $this->assertGreaterThan(54,$dataProvider->getTotalItemCount()); 30 | } 31 | /** 32 | * Tests the facet functions 33 | */ 34 | public function testFacets() { 35 | $dataProvider = new ASolrDataProvider("ASolrDocument"); 36 | $criteria = $dataProvider->getCriteria(); 37 | $criteria->setQuery("name:test"); 38 | $criteria->facet = true; 39 | $criteria->addFacetField("name"); 40 | $criteria->addFacetQuery("popularity:[* TO 10]"); 41 | $criteria->addFacetQuery("popularity:[10 TO 20]"); 42 | $criteria->addFacetQuery("popularity:[20 TO *]"); 43 | $this->assertGreaterThan(54, $dataProvider->getTotalItemCount()); 44 | $fieldFacets = $dataProvider->getFieldFacets(); 45 | $this->assertTrue(isset($fieldFacets->name)); 46 | $this->assertTrue($fieldFacets->name instanceof ASolrFacet); 47 | $this->assertTrue(isset($fieldFacets->name['test'])); 48 | $this->assertGreaterThan(54, $fieldFacets->name['test']); 49 | 50 | $queryFacets = $dataProvider->getQueryFacets(); 51 | $this->assertTrue(isset($queryFacets["popularity:[* TO 10]"])); 52 | $this->assertGreaterThan(10, $queryFacets["popularity:[* TO 10]"]['value']); 53 | } 54 | 55 | /** 56 | * Destroys the test data at the end of the test 57 | */ 58 | public function tearDown() { 59 | foreach($this->fixtureData() as $attributes) { 60 | $doc = ASolrDocument::model()->findByAttributes($attributes); 61 | if (is_object($doc)) { 62 | $this->assertTrue($doc->delete()); 63 | } 64 | } 65 | $this->getConnection()->commit(); 66 | } 67 | 68 | /** 69 | * Gets the solr connection 70 | * @return ASolrConnection the connection to use for this test 71 | */ 72 | protected function getConnection() { 73 | static $connection; 74 | if ($connection === null) { 75 | $connection = new ASolrConnection(); 76 | $connection->clientOptions->hostname = SOLR_HOSTNAME; 77 | $connection->clientOptions->port = SOLR_PORT; 78 | $connection->clientOptions->path = SOLR_PATH; 79 | ASolrDocument::$solr = $connection; 80 | } 81 | return $connection; 82 | } 83 | /** 84 | * Generates 55 arrays of attributes for fixtures 85 | * @return array the fixture data 86 | */ 87 | protected function fixtureData() { 88 | $rows = array(); 89 | for($i = 0; $i < 55; $i++) { 90 | $rows[] = array( 91 | "id" => 400 + $i, 92 | "name" => "Test Item ".$i, 93 | "popularity" => $i 94 | ); 95 | } 96 | return $rows; 97 | } 98 | } -------------------------------------------------------------------------------- /tests/ASolrDocumentTest.php: -------------------------------------------------------------------------------- 1 | assertFalse(isset($doc->name)); 15 | $doc->name = "test name"; 16 | $this->assertTrue(isset($doc->name)); 17 | $this->assertEquals("test name", $doc->name); 18 | } 19 | /** 20 | * Tests the attributeNames() method 21 | */ 22 | public function testAttributeNames() { 23 | $doc = new ASolrDocument(); 24 | $doc->name = "test item"; 25 | $this->assertEquals(array("name"),$doc->attributeNames()); 26 | $doc = new ExampleExtendedSolrDocument(); 27 | $this->assertEquals(array("incubationdate_dt"),$doc->attributeNames()); 28 | $doc->name = "test item"; 29 | $this->assertEquals(array("name","incubationdate_dt"),$doc->attributeNames()); 30 | } 31 | /** 32 | * Tests the primary key methods 33 | */ 34 | public function testPrimaryKey() { 35 | $doc = new ASolrDocument(); 36 | $doc->name = "test"; 37 | $doc->id = 12; 38 | $this->assertEquals("id", $doc->primaryKey()); 39 | $this->assertEquals(12, $doc->getPrimaryKey()); 40 | } 41 | /** 42 | * Tests the save method 43 | */ 44 | public function testSave() { 45 | $connection = new ASolrConnection(); 46 | $connection->clientOptions->hostname = SOLR_HOSTNAME; 47 | $connection->clientOptions->port = SOLR_PORT; 48 | $connection->clientOptions->path = SOLR_PATH; 49 | ASolrDocument::$solr = $connection; 50 | foreach($this->fixtureData() as $attributes) { 51 | 52 | $doc = new ASolrDocument(); 53 | $doc->setAttributes($attributes); // should fail because of massive assignment on unsafe attributes 54 | $this->assertEquals(array(),$doc->getAttributes()); 55 | $doc->setAttributes($attributes,false); 56 | $this->assertEquals(4,count($doc->getAttributes())); 57 | $this->assertTrue($doc->save()); 58 | } 59 | $connection->commit(); 60 | } 61 | /** 62 | * Tests the named scope features 63 | */ 64 | public function testNamedScopes() { 65 | $model = ExampleExtendedSolrDocument::model(); 66 | $model->exampleScope(); // apply the scope 67 | $models = $model->findAll(); 68 | $this->assertGreaterThan(49, count($models)); 69 | } 70 | /** 71 | * Tests the find methods 72 | */ 73 | public function testFind() { 74 | $connection = new ASolrConnection(); 75 | $connection->clientOptions->hostname = SOLR_HOSTNAME; 76 | $connection->clientOptions->port = SOLR_PORT; 77 | $connection->clientOptions->path = SOLR_PATH; 78 | ASolrDocument::$solr = $connection; 79 | $pkList = array(); 80 | foreach($this->fixtureData() as $attributes) { 81 | $criteria = new ASolrCriteria; 82 | $criteria->query = "id:".$attributes['id']; 83 | $doc = ASolrDocument::model()->find($criteria); 84 | $this->assertTrue(is_object($doc)); 85 | $this->assertTrue($doc instanceof IASolrDocument); 86 | foreach($attributes as $attribute => $value) { 87 | $this->assertEquals($value,$doc->{$attribute}); 88 | } 89 | $pkList[$doc->getPrimaryKey()] = $attributes; 90 | } 91 | $criteria = new ASolrCriteria(); 92 | $criteria->limit = 100; 93 | $criteria->query = "*:*"; 94 | $criteria->addInCondition("id",array_keys($pkList)); 95 | $models = ASolrDocument::model()->findAll($criteria); 96 | $this->assertEquals(count($pkList),count($models)); 97 | foreach($models as $doc) { 98 | foreach($pkList[$doc->getPrimaryKey()] as $attribute => $value) { 99 | $this->assertEquals($value,$doc->{$attribute}); 100 | } 101 | } 102 | } 103 | 104 | 105 | /** 106 | * Tests the find methods, returning scores 107 | */ 108 | public function testFindWithScores() { 109 | $connection = new ASolrConnection(); 110 | $connection->clientOptions->hostname = SOLR_HOSTNAME; 111 | $connection->clientOptions->port = SOLR_PORT; 112 | $connection->clientOptions->path = SOLR_PATH; 113 | ASolrDocument::$solr = $connection; 114 | $pkList = array(); 115 | foreach($this->fixtureData() as $attributes) { 116 | $criteria = new ASolrCriteria; 117 | $criteria->query = "id:".$attributes['id']; 118 | $criteria->withScores(); 119 | $doc = ASolrDocument::model()->find($criteria); 120 | $this->assertTrue(is_object($doc)); 121 | $this->assertTrue($doc instanceof IASolrDocument); 122 | $this->assertGreaterThan(0, $doc->getScore()); 123 | foreach($attributes as $attribute => $value) { 124 | $this->assertEquals($value,$doc->{$attribute}); 125 | } 126 | $pkList[$doc->getPrimaryKey()] = $attributes; 127 | } 128 | $criteria = new ASolrCriteria(); 129 | $criteria->query = "*:*"; 130 | $criteria->limit = 100; 131 | $criteria->addInCondition("id",array_keys($pkList)); 132 | $criteria->withScores(); 133 | $models = ASolrDocument::model()->findAll($criteria); 134 | $this->assertEquals(count($pkList),count($models)); 135 | foreach($models as $doc) { 136 | $this->assertGreaterThan(0, $doc->getScore()); 137 | foreach($pkList[$doc->getPrimaryKey()] as $attribute => $value) { 138 | $this->assertEquals($value,$doc->{$attribute}); 139 | } 140 | } 141 | } 142 | 143 | 144 | /** 145 | * Tests the find by pk methods 146 | */ 147 | public function testFindByPk() { 148 | $connection = new ASolrConnection(); 149 | $connection->clientOptions->hostname = SOLR_HOSTNAME; 150 | $connection->clientOptions->port = SOLR_PORT; 151 | $connection->clientOptions->path = SOLR_PATH; 152 | ASolrDocument::$solr = $connection; 153 | $pkList = array(); 154 | foreach($this->fixtureData() as $attributes) { 155 | $doc = ASolrDocument::model()->findByPk($attributes['id']); 156 | $this->assertTrue(is_object($doc)); 157 | $this->assertTrue($doc instanceof IASolrDocument); 158 | foreach($attributes as $attribute => $value) { 159 | $this->assertEquals($value,$doc->{$attribute}); 160 | } 161 | $pkList[$doc->getPrimaryKey()] = $attributes; 162 | } 163 | $criteria = new ASolrCriteria(); 164 | $criteria->limit = 100; 165 | $models = ASolrDocument::model()->findAllByPk(array_keys($pkList),$criteria); 166 | $this->assertEquals(count($pkList),count($models)); 167 | foreach($models as $doc) { 168 | foreach($pkList[$doc->getPrimaryKey()] as $attribute => $value) { 169 | $this->assertEquals($value,$doc->{$attribute}); 170 | } 171 | } 172 | } 173 | 174 | /** 175 | * Tests the find by attributes methods 176 | */ 177 | public function testFindByAttributes() { 178 | $connection = new ASolrConnection(); 179 | $connection->clientOptions->hostname = SOLR_HOSTNAME; 180 | $connection->clientOptions->port = SOLR_HOSTNAME; 181 | $connection->clientOptions->path = SOLR_PATH; 182 | ASolrDocument::$solr = $connection; 183 | foreach($this->fixtureData() as $attributes) { 184 | $attr = $attributes; 185 | unset($attr['links']); // cannot search with arrays 186 | $doc = ASolrDocument::model()->findByAttributes($attr); 187 | $this->assertTrue(is_object($doc)); 188 | $this->assertTrue($doc instanceof IASolrDocument); 189 | foreach($attributes as $attribute => $value) { 190 | $this->assertEquals($value,$doc->{$attribute}); 191 | } 192 | } 193 | foreach($this->fixtureData() as $attributes) { 194 | $attr = $attributes; 195 | unset($attr['links']); // cannot search with arrays 196 | $models = ASolrDocument::model()->findAllByAttributes($attr); 197 | $this->assertEquals(1,count($models)); 198 | $doc = array_shift($models); 199 | $this->assertTrue(is_object($doc)); 200 | $this->assertTrue($doc instanceof IASolrDocument); 201 | foreach($attributes as $attribute => $value) { 202 | $this->assertEquals($value,$doc->{$attribute}); 203 | } 204 | } 205 | } 206 | /** 207 | * Tests the delete method 208 | */ 209 | public function testDelete() { 210 | $connection = new ASolrConnection(); 211 | $connection->clientOptions->hostname = SOLR_HOSTNAME; 212 | $connection->clientOptions->port = SOLR_PORT; 213 | $connection->clientOptions->path = SOLR_PATH; 214 | ASolrDocument::$solr = $connection; 215 | foreach($this->fixtureData() as $attributes) { 216 | $doc = ASolrDocument::model()->findByPk($attributes['id']); 217 | $this->assertTrue(is_object($doc)); 218 | $this->assertTrue($doc->delete()); 219 | } 220 | $connection->commit(); 221 | // now check if they were really deleted 222 | foreach($this->fixtureData() as $attributes) { 223 | $doc = ASolrDocument::model()->findByPk($attributes['id']); 224 | $this->assertFalse(is_object($doc)); 225 | } 226 | } 227 | 228 | 229 | /** 230 | * Generates 50 arrays of attributes for fixtures 231 | * @return array the fixture data 232 | */ 233 | protected function fixtureData() { 234 | $rows = array(); 235 | for($i = 0; $i < 50; $i++) { 236 | $rows[] = array( 237 | "id" => 400 + $i, 238 | "name" => "Test Item ".$i, 239 | "popularity" => $i, 240 | "links" => array( 241 | "http://www.google.com/", 242 | "http://www.youtube.com/", 243 | ) 244 | ); 245 | } 246 | return $rows; 247 | } 248 | } 249 | /** 250 | * An example of an extended solr document 251 | * @author Charles Pick/PeoplePerHour.com 252 | * @package packages.solr.tests 253 | */ 254 | class ExampleExtendedSolrDocument extends ASolrDocument { 255 | /** 256 | * An example of a class property that will be saved to solr 257 | * @var string 258 | */ 259 | public $incubationdate_dt; 260 | 261 | /** 262 | * An example of a solr named scope 263 | * @return ExampleExtendedSolrDocument $this with the scope applied 264 | */ 265 | public function exampleScope() { 266 | $criteria = new ASolrCriteria(); 267 | $criteria->setLimit(100); 268 | $criteria->setQuery('name:test'); 269 | $this->getSolrCriteria()->mergeWith($criteria); 270 | return $this; 271 | } 272 | /** 273 | * Gets the static model 274 | * @param string $className the model class to instantiate 275 | * @return ExampleExtendedSolrDocument the nidek 276 | */ 277 | public static function model($className = __CLASS__) { 278 | return parent::model($className); 279 | } 280 | } -------------------------------------------------------------------------------- /tests/ASolrQueryResponseTest.php: -------------------------------------------------------------------------------- 1 | mockSolrObject(), $this->mockSolrCriteria()); 15 | $facets = $queryResponse->getFieldFacets(); 16 | $this->assertEquals(1, count($facets)); 17 | $this->assertTrue(isset($facets->name)); 18 | $this->assertTrue($facets->name instanceof ASolrFacet); 19 | 20 | } 21 | /** 22 | * Tests the results functions 23 | */ 24 | public function testResults() { 25 | $queryResponse = new ASolrQueryResponse($this->mockSolrObject(), $this->mockSolrCriteria()); 26 | $results = $queryResponse->getResults(); 27 | $this->assertEquals(3,$results->count()); 28 | foreach($results as $n => $result) { 29 | $this->assertEquals("test item ".($n + 1),$result->name); 30 | $this->assertEquals(1.0, $result->getScore()); 31 | } 32 | } 33 | /** 34 | * Creates a mock solr criteria for use in testing 35 | * @return ASolrCriteria the mock criteria 36 | */ 37 | protected function mockSolrCriteria() { 38 | $criteria = new ASolrCriteria; 39 | $criteria->query = "test item"; 40 | $criteria->offset = 0; 41 | $criteria->limit = 10; 42 | return $criteria; 43 | } 44 | /** 45 | * Creates a mock solr object for use in testing 46 | * @return object the mock solr object 47 | */ 48 | protected function mockSolrObject() { 49 | $mock = (object) array( 50 | "responseHeader" => (object) array(), 51 | "response" => (object) array( 52 | "numFound" => 2, 53 | "start" => 0, 54 | "docs" => array( 55 | (object) array( 56 | "id" => 1, 57 | "name" => "test item 1", 58 | "popularity" => 1, 59 | "score" => 1.0, 60 | ), 61 | (object) array( 62 | "id" => 2, 63 | "name" => "test item 2", 64 | "popularity" => 2, 65 | "score" => 1.0, 66 | ), 67 | (object) array( 68 | "id" => 3, 69 | "name" => "test item 3", 70 | "popularity" => 3, 71 | "score" => 1.0, 72 | ) 73 | ) 74 | ), 75 | "facet_counts" => (object) array( 76 | "facet_queries" => (object) array(), 77 | "facet_fields" => (object) array( 78 | "name" => (object) array( 79 | "test item 1" => 1, 80 | "test item 2" => 1, 81 | "test item 3" => 1, 82 | ) 83 | ), 84 | ), 85 | ); 86 | return $mock; 87 | } 88 | 89 | } -------------------------------------------------------------------------------- /tests/ASolrSearchableTest.php: -------------------------------------------------------------------------------- 1 | find(); 14 | $behavior = $model->asa("ASolrSearchable"); 15 | $attributeNames = array("id","name","author","popularity","description"); 16 | $this->assertEquals(array_combine($attributeNames,$attributeNames),$behavior->getAttributes()); 17 | $solrDocument = $behavior->getSolrDocument(); 18 | $this->assertTrue($solrDocument instanceof IASolrDocument); 19 | foreach($attributeNames as $attribute) { 20 | $this->assertTrue(isset($solrDocument->{$attribute})); 21 | $this->assertEquals($model->{$attribute},$solrDocument->{$attribute}); 22 | } 23 | $this->assertTrue($model->index()); 24 | $this->assertFalse($behavior->getIsModified()); 25 | $model->name = "Test User ".uniqid(); 26 | $this->assertTrue($behavior->getIsModified()); 27 | $this->assertTrue($model->save()); 28 | $this->getConnection()->commit(); 29 | $criteria = new ASolrCriteria(); 30 | $criteria->query = "*:*"; 31 | $fromSolr = $model->findBySolr($criteria); 32 | $this->assertTrue($fromSolr instanceof ExampleSolrActiveRecord); 33 | } 34 | 35 | public function testGetIsModified() { 36 | $model = ExampleSolrActiveRecord::model()->find(); 37 | $behavior = $model->asa("ASolrSearchable"); 38 | $behavior->setAttributes(array("id","name","popularity","description")); // specify the attribute names we're interested in saving to solr 39 | $this->assertFalse($model->getIsModified()); 40 | $model->author = "an author"; 41 | $this->assertFalse($model->getIsModified()); // a field we don't care about 42 | $model->name = "hello"; 43 | $this->assertTrue($model->getIsModified()); 44 | } 45 | /** 46 | * Tests the find() and findAll() methods 47 | */ 48 | public function testFind() { 49 | $model = ExampleSolrActiveRecord::model()->find(); 50 | 51 | $behavior = $model->asa("ASolrSearchable"); /* @var ASolrSearchable $behavior */ 52 | $model->index(); 53 | $behavior->getSolrDocument()->getSolrConnection()->commit(); 54 | $criteria = new ASolrCriteria(); 55 | $criteria->query = "id:".$model->id; 56 | $fromSolr = $behavior->findBySolr($criteria); 57 | $this->assertTrue($fromSolr instanceof ExampleSolrActiveRecord); 58 | foreach($fromSolr->attributeNames() as $attribute) { 59 | $this->assertEquals($model->{$attribute}, $fromSolr->{$attribute}); 60 | } 61 | 62 | $criteria = new ASolrCriteria; 63 | $criteria->setLimit(10); 64 | $results = $model->findAllBySolr($criteria); 65 | $this->assertEquals(10, count($results)); 66 | foreach($results as $result) { 67 | $this->assertTrue($result instanceof ExampleSolrActiveRecord); 68 | } 69 | } 70 | 71 | /** 72 | * Tests the delete events 73 | */ 74 | public function testDelete() { 75 | $model = ExampleSolrActiveRecord::model()->find(); 76 | 77 | $behavior = $model->asa("ASolrSearchable"); /* @var ASolrSearchable $behavior */ 78 | $model->index(); 79 | $connection = $behavior->getSolrDocument()->getSolrConnection() /* @var ASolrConnection $connection */; 80 | $connection->commit(); 81 | $criteria = new ASolrCriteria(); 82 | $criteria->query = "id:".$model->id; 83 | $this->assertTrue(is_object($connection->search($criteria))); 84 | $this->assertEquals(1,$connection->count($criteria)); 85 | $model->delete(); 86 | $connection->commit(); 87 | 88 | $this->assertEquals(0,$connection->count($criteria)); 89 | 90 | } 91 | 92 | 93 | /** 94 | * Tests populating active record objects directly from solr 95 | */ 96 | public function testPopulateFromSolr() { 97 | $model = ExampleSolrActiveRecord::model()->find(); 98 | $model->getMetaData()->addRelation("testRelation",array( 99 | CActiveRecord::HAS_ONE, 100 | "ExampleSolrActiveRecord", 101 | "id" 102 | )); 103 | $behavior = $model->asa("ASolrSearchable"); /* @var ASolrSearchable $behavior */ 104 | $behavior->setAttributes(CMap::mergeArray($behavior->getAttributes(),array("testRelation.name"))); 105 | $criteria = new ASolrCriteria(); 106 | $criteria->query = "*:*"; 107 | $document = $behavior->getSolrDocument()->find($criteria); 108 | $document->testRelation__name = "test relation name"; 109 | $fromSolr = $behavior->populateFromSolr($document, false); 110 | $this->assertEquals($document->name, $fromSolr->name); 111 | $this->assertTrue(is_object($fromSolr->testRelation)); 112 | $this->assertEquals("test relation name", $fromSolr->testRelation->name); 113 | } 114 | /** 115 | * Adds the required data to the test database 116 | */ 117 | public function setUp() { 118 | $this->getConnection(); 119 | foreach($this->fixtureData() as $row) { 120 | $record = new ExampleSolrActiveRecord(); 121 | foreach($row as $attribute => $value) { 122 | $record->{$attribute} = $value; 123 | } 124 | $this->assertTrue($record->save()); 125 | } 126 | } 127 | /** 128 | * Deletes the data from the test database 129 | */ 130 | public function tearDown() { 131 | $sql = "DELETE FROM solrexample WHERE 1=1"; 132 | ExampleSolrActiveRecord::model()->getDbConnection()->createCommand($sql)->execute(); 133 | } 134 | 135 | /** 136 | * Gets the solr connection 137 | * @return ASolrConnection the connection to use for this test 138 | */ 139 | protected function getConnection() { 140 | static $connection; 141 | if ($connection === null) { 142 | $connection = new ASolrConnection(); 143 | $connection->clientOptions->hostname = SOLR_HOSTNAME; 144 | $connection->clientOptions->port = SOLR_PORT; 145 | $connection->clientOptions->path = SOLR_PATH; 146 | ASolrDocument::$solr = $connection; 147 | } 148 | return $connection; 149 | } 150 | 151 | 152 | /** 153 | * Generates 50 arrays of attributes for fixtures 154 | * @return array the fixture data 155 | */ 156 | protected function fixtureData() { 157 | $rows = array(); 158 | for($i = 0; $i < 50; $i++) { 159 | $rows[] = array( 160 | "name" => "Test Item ".$i, 161 | "popularity" => $i, 162 | "author" => "Test Author ".$i, 163 | "description" => str_repeat("lorem ipsum dolor est ",rand(3,20)), 164 | 165 | ); 166 | } 167 | return $rows; 168 | } 169 | } 170 | 171 | /** 172 | * An example active record that can be populated from a database or from solr 173 | * @author Charles Pick / PeoplePerHour.com 174 | * @package packages.solr.tests 175 | * 176 | * @propert integer $id the id field (pk) 177 | * @property string $name the name field 178 | * @property string $author the author field 179 | * @property integer $popularity the popularity field 180 | * @property string $description the description field 181 | */ 182 | class ExampleSolrActiveRecord extends CActiveRecord { 183 | /** 184 | * Holds the database connection to use with this model 185 | * @var CDbConnection 186 | */ 187 | protected $_db; 188 | 189 | public function behaviors() { 190 | return array( 191 | "ASolrSearchable" => array( 192 | "class" => "packages.solr.ASolrSearchable", 193 | ) 194 | ); 195 | } 196 | /** 197 | * Gets the database connection to use with this model. 198 | * We use an sqlite connection for the test data. 199 | * @return CDbConnection the database connection 200 | */ 201 | public function getDbConnection() { 202 | if ($this->_db === null) { 203 | $dsn = 'sqlite2:'.__DIR__.'/test.db'; 204 | $this->_db = new CDbConnection($dsn); 205 | } 206 | return $this->_db; 207 | } 208 | /** 209 | * Gets the table name to use for this model 210 | * @return string the table name 211 | */ 212 | public function tableName() { 213 | return "solrexample"; 214 | } 215 | /** 216 | * Gets the static model instance 217 | * @param string $className the class to instantiate 218 | * @return ExampleSolrActiveRecord the static model instance 219 | */ 220 | public static function model($className = __CLASS__) { 221 | return parent::model($className); 222 | } 223 | 224 | } -------------------------------------------------------------------------------- /tests/common.php: -------------------------------------------------------------------------------- 1 |