Completed
Push — master ( 13c87d...699abc )
by Mark
06:46 queued 02:05
created
code/adapters/ShopSearchSolr.php 1 patch
Indentation   +643 added lines, -643 removed lines patch added patch discarded remove patch
@@ -1,6 +1,6 @@  discard block
 block discarded – undo
1 1
 <?php
2 2
 if (!class_exists('SolrIndex')) {
3
-    return;
3
+	return;
4 4
 }
5 5
 
6 6
 /**
@@ -12,46 +12,46 @@  discard block
 block discarded – undo
12 12
  */
13 13
 class ShopSearchSolr extends SolrIndex implements ShopSearchAdapter
14 14
 {
15
-    /** @var array - maps our names for fields to Solr's names (i.e. Title => SiteTree_Title) */
16
-    protected $fieldMap = array();
17
-
18
-    /**
19
-     * Sets up the index
20
-     */
21
-    public function init()
22
-    {
23
-        $searchables = ShopSearch::get_searchable_classes();
24
-
25
-        // Add each class to the index
26
-        foreach ($searchables as $class) {
27
-            $this->addClass($class);
28
-        }
29
-
30
-        // add the fields they've specifically asked for
31
-        $fields = $this->getFulltextSpec();
32
-        foreach ($fields as $def) {
33
-            $this->addFulltextField($def['field'], $def['type'], $def['params']);
34
-        }
35
-
36
-        // add the filters they've asked for
37
-        $filters = $this->getFilterSpec();
38
-        foreach ($filters as $filterName => $def) {
39
-            // NOTE: I'm pulling the guts out of this function so we can access Solr's full name
40
-            // for the field (SiteTree_Title for Title) and build the fieldMap in one step instead
41
-            // of two.
42
-            //$this->addFilterField($def['field'], $def['type'], $def['params']);
43
-            $singleFilter = $this->fieldData($def['field'], $def['type'], $def['params']);
44
-            $this->filterFields = array_merge($this->filterFields, $singleFilter);
45
-            foreach ($singleFilter as $solrName => $solrDef) {
46
-                if ($def['field'] == $solrDef['field']) {
47
-                    $this->fieldMap[$filterName] = $solrName;
48
-                }
49
-            }
50
-        }
15
+	/** @var array - maps our names for fields to Solr's names (i.e. Title => SiteTree_Title) */
16
+	protected $fieldMap = array();
17
+
18
+	/**
19
+	 * Sets up the index
20
+	 */
21
+	public function init()
22
+	{
23
+		$searchables = ShopSearch::get_searchable_classes();
24
+
25
+		// Add each class to the index
26
+		foreach ($searchables as $class) {
27
+			$this->addClass($class);
28
+		}
29
+
30
+		// add the fields they've specifically asked for
31
+		$fields = $this->getFulltextSpec();
32
+		foreach ($fields as $def) {
33
+			$this->addFulltextField($def['field'], $def['type'], $def['params']);
34
+		}
35
+
36
+		// add the filters they've asked for
37
+		$filters = $this->getFilterSpec();
38
+		foreach ($filters as $filterName => $def) {
39
+			// NOTE: I'm pulling the guts out of this function so we can access Solr's full name
40
+			// for the field (SiteTree_Title for Title) and build the fieldMap in one step instead
41
+			// of two.
42
+			//$this->addFilterField($def['field'], $def['type'], $def['params']);
43
+			$singleFilter = $this->fieldData($def['field'], $def['type'], $def['params']);
44
+			$this->filterFields = array_merge($this->filterFields, $singleFilter);
45
+			foreach ($singleFilter as $solrName => $solrDef) {
46
+				if ($def['field'] == $solrDef['field']) {
47
+					$this->fieldMap[$filterName] = $solrName;
48
+				}
49
+			}
50
+		}
51 51
 
52 52
 //		Debug::dump($this->filterFields);
53 53
 
54
-        // Add spellcheck fields
54
+		// Add spellcheck fields
55 55
 //		$spellFields = $cfg->get('ShopSearch', 'spellcheck_dictionary_source');
56 56
 //		if (empty($spellFields) || !is_array($spellFields)) {
57 57
 //			$spellFields = array();
@@ -65,11 +65,11 @@  discard block
 block discarded – undo
65 65
 //			$this->addCopyField($f, '_spellcheckContent');
66 66
 //		}
67 67
 
68
-        // Technically, filter and sort fields are the same in Solr/Lucene
68
+		// Technically, filter and sort fields are the same in Solr/Lucene
69 69
 //		$this->addSortField('ViewCount');
70 70
 //		$this->addSortField('LastEdited', 'SSDatetime');
71 71
 
72
-        // Aggregate fields for spelling checks
72
+		// Aggregate fields for spelling checks
73 73
 //		$this->addCopyField('Title', 'spellcheckData');
74 74
 //		$this->addCopyField('Content', 'spellcheckData');
75 75
 
@@ -82,144 +82,144 @@  discard block
 block discarded – undo
82 82
 //			)
83 83
 //		));
84 84
 
85
-        // I can't get this to work. Need a way to create the Category field that get used
85
+		// I can't get this to work. Need a way to create the Category field that get used
86 86
 //		$this->addFilterField('Category', 'Int');
87 87
 //		$this->addFilterField('Parent.ID');
88 88
 //		$this->addFilterField('ProductCategories.ID');
89 89
 //		$this->addCopyField('SiteTree_Parent_ID', 'Category');
90 90
 //		$this->addCopyField('Product_ProductCategories_ID', 'Category');
91 91
 
92
-        // These will be added in a pull request to shop module. If they're not present they'll be ignored
92
+		// These will be added in a pull request to shop module. If they're not present they'll be ignored
93 93
 //		$this->addFilterField('AllCategoryIDs', 'Int', array('multiValued' => 'true'));
94 94
 //		$this->addFilterField('AllRecursiveCategoryIDs', 'Int', array('multiValued' => 'true'));
95 95
 
96
-        // This will cause only live pages to be indexed. There are two ways to do
97
-        // this. See fulltextsearch/docs/en/index.md for more information.
98
-        // Not sure if this is really the way to go or not, but for now this is it.
99
-        $this->excludeVariantState(array('SearchVariantVersioned' => 'Stage'));
100
-    }
101
-
102
-
103
-    /**
104
-     * Transforms different formats of field list into something we can pass to solr
105
-     * @param array $in
106
-     * @return array
107
-     */
108
-    protected function scrubFieldList($in)
109
-    {
110
-        $out = array();
111
-        if (empty($in) || !is_array($in)) {
112
-            return $out;
113
-        }
114
-
115
-        foreach ($in as $name => $val) {
116
-            // supports an indexed array format of simple field names
117
-            if (is_numeric($name)) {
118
-                $name = $val;
119
-                $val = true;
120
-            }
121
-
122
-            // supports a boolean value meaning "use the default setup"
123
-            $params = !is_array($val) ? array() : array_slice($val, 0);
124
-
125
-            // build a normalized structur
126
-            $def = array(
127
-                'field'     => isset($params['field']) ? $params['field'] : $name,
128
-                'type'      => isset($params['type']) ? $params['type'] : null,
129
-                'params'    => $params,
130
-            );
131
-
132
-            if (isset($def['params']['field'])) {
133
-                unset($def['params']['field']);
134
-            }
135
-            if (isset($def['params']['type'])) {
136
-                unset($def['params']['type']);
137
-            }
138
-
139
-            $out[$name] = $def;
140
-        }
141
-
142
-        return $out;
143
-    }
144
-
145
-
146
-    /**
147
-     * @return array
148
-     */
149
-    protected function getFulltextSpec()
150
-    {
151
-        $fields = Config::inst()->get('ShopSearch', 'solr_fulltext_fields');
152
-        if (empty($fields)) {
153
-            $fields = array('Title', 'Content');
154
-        }
155
-        return $this->scrubFieldList($fields);
156
-    }
157
-
158
-
159
-    /**
160
-     *
161
-     */
162
-    protected function getFilterSpec()
163
-    {
164
-        $fields = Config::inst()->get('ShopSearch', 'solr_filter_fields');
165
-        return $this->scrubFieldList($fields);
166
-    }
167
-
168
-
169
-    /**
170
-     * @return string
171
-     */
172
-    public function getFieldDefinitions()
173
-    {
174
-        $xml = parent::getFieldDefinitions();
96
+		// This will cause only live pages to be indexed. There are two ways to do
97
+		// this. See fulltextsearch/docs/en/index.md for more information.
98
+		// Not sure if this is really the way to go or not, but for now this is it.
99
+		$this->excludeVariantState(array('SearchVariantVersioned' => 'Stage'));
100
+	}
101
+
102
+
103
+	/**
104
+	 * Transforms different formats of field list into something we can pass to solr
105
+	 * @param array $in
106
+	 * @return array
107
+	 */
108
+	protected function scrubFieldList($in)
109
+	{
110
+		$out = array();
111
+		if (empty($in) || !is_array($in)) {
112
+			return $out;
113
+		}
114
+
115
+		foreach ($in as $name => $val) {
116
+			// supports an indexed array format of simple field names
117
+			if (is_numeric($name)) {
118
+				$name = $val;
119
+				$val = true;
120
+			}
121
+
122
+			// supports a boolean value meaning "use the default setup"
123
+			$params = !is_array($val) ? array() : array_slice($val, 0);
124
+
125
+			// build a normalized structur
126
+			$def = array(
127
+				'field'     => isset($params['field']) ? $params['field'] : $name,
128
+				'type'      => isset($params['type']) ? $params['type'] : null,
129
+				'params'    => $params,
130
+			);
131
+
132
+			if (isset($def['params']['field'])) {
133
+				unset($def['params']['field']);
134
+			}
135
+			if (isset($def['params']['type'])) {
136
+				unset($def['params']['type']);
137
+			}
138
+
139
+			$out[$name] = $def;
140
+		}
141
+
142
+		return $out;
143
+	}
144
+
145
+
146
+	/**
147
+	 * @return array
148
+	 */
149
+	protected function getFulltextSpec()
150
+	{
151
+		$fields = Config::inst()->get('ShopSearch', 'solr_fulltext_fields');
152
+		if (empty($fields)) {
153
+			$fields = array('Title', 'Content');
154
+		}
155
+		return $this->scrubFieldList($fields);
156
+	}
157
+
158
+
159
+	/**
160
+	 *
161
+	 */
162
+	protected function getFilterSpec()
163
+	{
164
+		$fields = Config::inst()->get('ShopSearch', 'solr_filter_fields');
165
+		return $this->scrubFieldList($fields);
166
+	}
167
+
168
+
169
+	/**
170
+	 * @return string
171
+	 */
172
+	public function getFieldDefinitions()
173
+	{
174
+		$xml = parent::getFieldDefinitions();
175 175
 //		$xml .= "\n\t\t<field name='_spellcheckContent' type='htmltext' indexed='true' stored='false' multiValued='true' />";
176 176
 
177
-        // create a sorting column
178
-        if (isset($this->fieldMap['Title']) || ShopSearch::config()->solr_title_sort_field) {
179
-            $f = empty(ShopSearch::config()->title_sort_field) ? '_titleSort' : ShopSearch::config()->solr_title_sort_field;
180
-            $xml .= "\n\t\t" . '<field name="' . $f . '" type="alphaOnlySort" indexed="true" stored="false" required="false" multiValued="false" />';
181
-            $xml .= "\n\t\t" . '<copyField source="SiteTree_Title" dest="' . $f . '"/>';
182
-        }
183
-
184
-        // create an autocomplete column
185
-        if (ShopSearch::config()->suggest_enabled) {
186
-            $xml .= "\n\t\t<field name='_autocomplete' type='autosuggest_text' indexed='true' stored='false' multiValued='true'/>";
187
-        }
188
-
189
-        return $xml;
190
-    }
191
-
192
-
193
-    /**
194
-     * @return string
195
-     */
196
-    public function getCopyFieldDefinitions()
197
-    {
198
-        $xml = parent::getCopyFieldDefinitions();
199
-
200
-        if (ShopSearch::config()->suggest_enabled) {
201
-            foreach ($this->fulltextFields as $name => $field) {
202
-                $xml .= "\n\t<copyField source='{$name}' dest='_autocomplete' />";
203
-                //$xml .= "\n\t<copyField source='{$name}' dest='_spellcheckContent' />";
204
-            }
205
-        }
206
-
207
-        return $xml;
208
-    }
209
-
210
-
211
-        /**
212
-     * Overrides the parent to add a field for autocomplete
213
-     * @return HTMLText
214
-     */
215
-    public function getTypes()
216
-    {
217
-        $val = parent::getTypes();
218
-        if (!$val || !is_object($val)) {
219
-            return $val;
220
-        }
221
-        $xml = $val->getValue();
222
-        $xml .= <<<XML
177
+		// create a sorting column
178
+		if (isset($this->fieldMap['Title']) || ShopSearch::config()->solr_title_sort_field) {
179
+			$f = empty(ShopSearch::config()->title_sort_field) ? '_titleSort' : ShopSearch::config()->solr_title_sort_field;
180
+			$xml .= "\n\t\t" . '<field name="' . $f . '" type="alphaOnlySort" indexed="true" stored="false" required="false" multiValued="false" />';
181
+			$xml .= "\n\t\t" . '<copyField source="SiteTree_Title" dest="' . $f . '"/>';
182
+		}
183
+
184
+		// create an autocomplete column
185
+		if (ShopSearch::config()->suggest_enabled) {
186
+			$xml .= "\n\t\t<field name='_autocomplete' type='autosuggest_text' indexed='true' stored='false' multiValued='true'/>";
187
+		}
188
+
189
+		return $xml;
190
+	}
191
+
192
+
193
+	/**
194
+	 * @return string
195
+	 */
196
+	public function getCopyFieldDefinitions()
197
+	{
198
+		$xml = parent::getCopyFieldDefinitions();
199
+
200
+		if (ShopSearch::config()->suggest_enabled) {
201
+			foreach ($this->fulltextFields as $name => $field) {
202
+				$xml .= "\n\t<copyField source='{$name}' dest='_autocomplete' />";
203
+				//$xml .= "\n\t<copyField source='{$name}' dest='_spellcheckContent' />";
204
+			}
205
+		}
206
+
207
+		return $xml;
208
+	}
209
+
210
+
211
+		/**
212
+		 * Overrides the parent to add a field for autocomplete
213
+		 * @return HTMLText
214
+		 */
215
+	public function getTypes()
216
+	{
217
+		$val = parent::getTypes();
218
+		if (!$val || !is_object($val)) {
219
+			return $val;
220
+		}
221
+		$xml = $val->getValue();
222
+		$xml .= <<<XML
223 223
 
224 224
 	        <fieldType name="autosuggest_text" class="solr.TextField"
225 225
 	                   positionIncrementGap="100">
@@ -238,89 +238,89 @@  discard block
 block discarded – undo
238 238
 	        </fieldType>
239 239
 
240 240
 XML;
241
-        $val->setValue($xml);
242
-        return $val;
243
-    }
244
-
245
-    /**
246
-     * This is an intermediary to bridge the search form input
247
-     * and the SearchQuery class. It allows us to have other
248
-     * drivers that may not use the FullTextSearch module.
249
-     *
250
-     * @param string $keywords
251
-     * @param array $filters [optional]
252
-     * @param array $facetSpec [optional]
253
-     * @param int $start [optional]
254
-     * @param int $limit [optional]
255
-     * @param string $sort [optional]
256
-     * @return ArrayData
257
-     */
258
-    public function searchFromVars($keywords, array $filters=array(), array $facetSpec=array(), $start=-1, $limit=-1, $sort='score desc')
259
-    {
260
-        $query = new SearchQuery();
261
-        $params = array(
262
-            'sort'  => $sort,
263
-        );
264
-
265
-        // swap out title search
266
-        if ($params['sort'] == 'SiteTree_Title') {
267
-            $params['sort'] = '_titleSort';
268
-        }
269
-
270
-        // search by keywords
271
-        $query->search(empty($keywords) ? '*:*' : $keywords);
272
-
273
-        // search by filter
274
-        foreach ($filters as $k => $v) {
275
-            if (isset($this->fieldMap[$k])) {
276
-                if (is_string($v) && preg_match('/^RANGE\~(.+)\~(.+)$/', $v, $m)) {
277
-                    // Is it a range value?
278
-                    $range = new SearchQuery_Range($m[1], $m[2]);
279
-                    $query->filter($this->fieldMap[$k], $range);
280
-                } else {
281
-                    // Or a normal scalar value
282
-                    $query->filter($this->fieldMap[$k], $v);
283
-                }
284
-            }
285
-        }
286
-
287
-        // add facets
288
-        $facetSpec = FacetHelper::inst()->expandFacetSpec($facetSpec);
289
-        $params += $this->buildFacetParams($facetSpec);
290
-
291
-        // TODO: add spellcheck
292
-
293
-        return $this->search($query, $start, $limit, $params, $facetSpec);
294
-    }
295
-
296
-
297
-    /**
298
-     * @param string $keywords
299
-     * @param array  $filters
300
-     * @return array
301
-     */
302
-    public function suggestWithResults($keywords, array $filters = array())
303
-    {
304
-        $limit      = (int)ShopSearch::config()->sayt_limit;
305
-
306
-        // process the keywords a bit
307
-        $terms      = preg_split('/\s+/', trim(strtolower($keywords)));
308
-        $lastTerm   = count($terms) > 0 ? array_pop($terms) : '';
309
-        $prefix     = count($terms) > 0 ? implode(' ', $terms) . ' ' : '';
310
-        //$terms[]    = $lastTerm;
311
-        $terms[]    = $lastTerm . '*'; // this allows for partial words to still match
312
-
313
-        // convert that to something solr adapater can handle
314
-        $query = new SearchQuery();
315
-        $query->search('+' . implode(' +', $terms));
316
-
317
-        $params = array(
318
-            'sort'          => 'score desc',
319
-            'facet'         => 'true',
320
-            'facet.field'   => '_autocomplete',
321
-            'facet.limit'   => ShopSearch::config()->suggest_limit,
322
-            'facet.prefix'  => $lastTerm,
323
-        );
241
+		$val->setValue($xml);
242
+		return $val;
243
+	}
244
+
245
+	/**
246
+	 * This is an intermediary to bridge the search form input
247
+	 * and the SearchQuery class. It allows us to have other
248
+	 * drivers that may not use the FullTextSearch module.
249
+	 *
250
+	 * @param string $keywords
251
+	 * @param array $filters [optional]
252
+	 * @param array $facetSpec [optional]
253
+	 * @param int $start [optional]
254
+	 * @param int $limit [optional]
255
+	 * @param string $sort [optional]
256
+	 * @return ArrayData
257
+	 */
258
+	public function searchFromVars($keywords, array $filters=array(), array $facetSpec=array(), $start=-1, $limit=-1, $sort='score desc')
259
+	{
260
+		$query = new SearchQuery();
261
+		$params = array(
262
+			'sort'  => $sort,
263
+		);
264
+
265
+		// swap out title search
266
+		if ($params['sort'] == 'SiteTree_Title') {
267
+			$params['sort'] = '_titleSort';
268
+		}
269
+
270
+		// search by keywords
271
+		$query->search(empty($keywords) ? '*:*' : $keywords);
272
+
273
+		// search by filter
274
+		foreach ($filters as $k => $v) {
275
+			if (isset($this->fieldMap[$k])) {
276
+				if (is_string($v) && preg_match('/^RANGE\~(.+)\~(.+)$/', $v, $m)) {
277
+					// Is it a range value?
278
+					$range = new SearchQuery_Range($m[1], $m[2]);
279
+					$query->filter($this->fieldMap[$k], $range);
280
+				} else {
281
+					// Or a normal scalar value
282
+					$query->filter($this->fieldMap[$k], $v);
283
+				}
284
+			}
285
+		}
286
+
287
+		// add facets
288
+		$facetSpec = FacetHelper::inst()->expandFacetSpec($facetSpec);
289
+		$params += $this->buildFacetParams($facetSpec);
290
+
291
+		// TODO: add spellcheck
292
+
293
+		return $this->search($query, $start, $limit, $params, $facetSpec);
294
+	}
295
+
296
+
297
+	/**
298
+	 * @param string $keywords
299
+	 * @param array  $filters
300
+	 * @return array
301
+	 */
302
+	public function suggestWithResults($keywords, array $filters = array())
303
+	{
304
+		$limit      = (int)ShopSearch::config()->sayt_limit;
305
+
306
+		// process the keywords a bit
307
+		$terms      = preg_split('/\s+/', trim(strtolower($keywords)));
308
+		$lastTerm   = count($terms) > 0 ? array_pop($terms) : '';
309
+		$prefix     = count($terms) > 0 ? implode(' ', $terms) . ' ' : '';
310
+		//$terms[]    = $lastTerm;
311
+		$terms[]    = $lastTerm . '*'; // this allows for partial words to still match
312
+
313
+		// convert that to something solr adapater can handle
314
+		$query = new SearchQuery();
315
+		$query->search('+' . implode(' +', $terms));
316
+
317
+		$params = array(
318
+			'sort'          => 'score desc',
319
+			'facet'         => 'true',
320
+			'facet.field'   => '_autocomplete',
321
+			'facet.limit'   => ShopSearch::config()->suggest_limit,
322
+			'facet.prefix'  => $lastTerm,
323
+		);
324 324
 
325 325
 //		$facetSpec = array(
326 326
 //			'_autocomplete' => array(
@@ -339,14 +339,14 @@  discard block
 block discarded – undo
339 339
 //		$suggestsion = array();
340 340
 ////		if ($)
341 341
 
342
-        $service = $this->getService();
342
+		$service = $this->getService();
343 343
 
344
-        SearchVariant::with(count($query->classes) == 1 ? $query->classes[0]['class'] : null)->call('alterQuery', $query, $this);
344
+		SearchVariant::with(count($query->classes) == 1 ? $query->classes[0]['class'] : null)->call('alterQuery', $query, $this);
345 345
 
346
-        $q = $terms;
347
-        $fq = array();
346
+		$q = $terms;
347
+		$fq = array();
348 348
 
349
-        // Build the search itself
349
+		// Build the search itself
350 350
 //		foreach ($query->search as $search) {
351 351
 //			$text = $search['text'];
352 352
 //			preg_match_all('/"[^"]*"|\S+/', $text, $parts);
@@ -370,408 +370,408 @@  discard block
 block discarded – undo
370 370
 //			}
371 371
 //		}
372 372
 
373
-        // Filter by class if requested
374
-        $classq = array();
375
-
376
-        foreach ($query->classes as $class) {
377
-            if (!empty($class['includeSubclasses'])) {
378
-                $classq[] = 'ClassHierarchy:'.$class['class'];
379
-            } else {
380
-                $classq[] = 'ClassName:'.$class['class'];
381
-            }
382
-        }
383
-
384
-        if ($classq) {
385
-            $fq[] = '+('.implode(' ', $classq).')';
386
-        }
387
-
388
-        // Filter by filters
389
-        foreach ($query->require as $field => $values) {
390
-            $requireq = array();
391
-
392
-            foreach ($values as $value) {
393
-                if ($value === SearchQuery::$missing) {
394
-                    $requireq[] = "(*:* -{$field}:[* TO *])";
395
-                } elseif ($value === SearchQuery::$present) {
396
-                    $requireq[] = "{$field}:[* TO *]";
397
-                } elseif ($value instanceof SearchQuery_Range) {
398
-                    $start = $value->start;
399
-                    if ($start === null) {
400
-                        $start = '*';
401
-                    }
402
-                    $end = $value->end;
403
-                    if ($end === null) {
404
-                        $end = '*';
405
-                    }
406
-                    $requireq[] = "$field:[$start TO $end]";
407
-                } else {
408
-                    $requireq[] = $field.':"'.$value.'"';
409
-                }
410
-            }
411
-
412
-            $fq[] = '+('.implode(' ', $requireq).')';
413
-        }
414
-
415
-        foreach ($query->exclude as $field => $values) {
416
-            $excludeq = array();
417
-            $missing = false;
418
-
419
-            foreach ($values as $value) {
420
-                if ($value === SearchQuery::$missing) {
421
-                    $missing = true;
422
-                } elseif ($value === SearchQuery::$present) {
423
-                    $excludeq[] = "{$field}:[* TO *]";
424
-                } elseif ($value instanceof SearchQuery_Range) {
425
-                    $start = $value->start;
426
-                    if ($start === null) {
427
-                        $start = '*';
428
-                    }
429
-                    $end = $value->end;
430
-                    if ($end === null) {
431
-                        $end = '*';
432
-                    }
433
-                    $excludeq[] = "$field:[$start TO $end]";
434
-                } else {
435
-                    $excludeq[] = $field.':"'.$value.'"';
436
-                }
437
-            }
438
-
439
-            $fq[] = ($missing ? "+{$field}:[* TO *] " : '') . '-('.implode(' ', $excludeq).')';
440
-        }
373
+		// Filter by class if requested
374
+		$classq = array();
375
+
376
+		foreach ($query->classes as $class) {
377
+			if (!empty($class['includeSubclasses'])) {
378
+				$classq[] = 'ClassHierarchy:'.$class['class'];
379
+			} else {
380
+				$classq[] = 'ClassName:'.$class['class'];
381
+			}
382
+		}
383
+
384
+		if ($classq) {
385
+			$fq[] = '+('.implode(' ', $classq).')';
386
+		}
387
+
388
+		// Filter by filters
389
+		foreach ($query->require as $field => $values) {
390
+			$requireq = array();
391
+
392
+			foreach ($values as $value) {
393
+				if ($value === SearchQuery::$missing) {
394
+					$requireq[] = "(*:* -{$field}:[* TO *])";
395
+				} elseif ($value === SearchQuery::$present) {
396
+					$requireq[] = "{$field}:[* TO *]";
397
+				} elseif ($value instanceof SearchQuery_Range) {
398
+					$start = $value->start;
399
+					if ($start === null) {
400
+						$start = '*';
401
+					}
402
+					$end = $value->end;
403
+					if ($end === null) {
404
+						$end = '*';
405
+					}
406
+					$requireq[] = "$field:[$start TO $end]";
407
+				} else {
408
+					$requireq[] = $field.':"'.$value.'"';
409
+				}
410
+			}
411
+
412
+			$fq[] = '+('.implode(' ', $requireq).')';
413
+		}
414
+
415
+		foreach ($query->exclude as $field => $values) {
416
+			$excludeq = array();
417
+			$missing = false;
418
+
419
+			foreach ($values as $value) {
420
+				if ($value === SearchQuery::$missing) {
421
+					$missing = true;
422
+				} elseif ($value === SearchQuery::$present) {
423
+					$excludeq[] = "{$field}:[* TO *]";
424
+				} elseif ($value instanceof SearchQuery_Range) {
425
+					$start = $value->start;
426
+					if ($start === null) {
427
+						$start = '*';
428
+					}
429
+					$end = $value->end;
430
+					if ($end === null) {
431
+						$end = '*';
432
+					}
433
+					$excludeq[] = "$field:[$start TO $end]";
434
+				} else {
435
+					$excludeq[] = $field.':"'.$value.'"';
436
+				}
437
+			}
438
+
439
+			$fq[] = ($missing ? "+{$field}:[* TO *] " : '') . '-('.implode(' ', $excludeq).')';
440
+		}
441 441
 
442 442
 //		if(!headers_sent()) {
443 443
 //			if ($q) header('X-Query: '.implode(' ', $q));
444 444
 //			if ($fq) header('X-Filters: "'.implode('", "', $fq).'"');
445 445
 //		}
446 446
 
447
-        $params = array_merge($params, array('fq' => implode(' ', $fq)));
448
-
449
-        $res = $service->search(
450
-            implode(' ', $q),
451
-            0,
452
-            $limit,
453
-            $params,
454
-            Apache_Solr_Service::METHOD_POST
455
-        );
456
-
457
-        $results = new ArrayList();
458
-        if ($res->getHttpStatus() >= 200 && $res->getHttpStatus() < 300) {
459
-            foreach ($res->response->docs as $doc) {
460
-                $result = DataObject::get_by_id($doc->ClassName, $doc->ID);
461
-                if ($result) {
462
-                    $results->push($result);
463
-                }
464
-            }
465
-            $numFound = $res->response->numFound;
466
-        } else {
467
-            $numFound = 0;
468
-        }
469
-
470
-        $ret = array();
471
-        $ret['products'] = new PaginatedList($results);
472
-        $ret['products']->setLimitItems(false);
473
-        $ret['products']->setTotalItems($numFound);
474
-        $ret['products']->setPageStart(0);
475
-        $ret['products']->setPageLength($limit);
476
-
477
-        // Facets (this is how we're doing suggestions for now...
478
-        $ret['suggestions'] = array();
479
-        if (isset($res->facet_counts->facet_fields->_autocomplete)) {
480
-            foreach ($res->facet_counts->facet_fields->_autocomplete as $term => $count) {
481
-                $ret['suggestions'][] = $prefix . $term;
482
-            }
483
-        }
484
-
485
-        // Suggestions (requires custom setup, assumes spellcheck.collate=true)
447
+		$params = array_merge($params, array('fq' => implode(' ', $fq)));
448
+
449
+		$res = $service->search(
450
+			implode(' ', $q),
451
+			0,
452
+			$limit,
453
+			$params,
454
+			Apache_Solr_Service::METHOD_POST
455
+		);
456
+
457
+		$results = new ArrayList();
458
+		if ($res->getHttpStatus() >= 200 && $res->getHttpStatus() < 300) {
459
+			foreach ($res->response->docs as $doc) {
460
+				$result = DataObject::get_by_id($doc->ClassName, $doc->ID);
461
+				if ($result) {
462
+					$results->push($result);
463
+				}
464
+			}
465
+			$numFound = $res->response->numFound;
466
+		} else {
467
+			$numFound = 0;
468
+		}
469
+
470
+		$ret = array();
471
+		$ret['products'] = new PaginatedList($results);
472
+		$ret['products']->setLimitItems(false);
473
+		$ret['products']->setTotalItems($numFound);
474
+		$ret['products']->setPageStart(0);
475
+		$ret['products']->setPageLength($limit);
476
+
477
+		// Facets (this is how we're doing suggestions for now...
478
+		$ret['suggestions'] = array();
479
+		if (isset($res->facet_counts->facet_fields->_autocomplete)) {
480
+			foreach ($res->facet_counts->facet_fields->_autocomplete as $term => $count) {
481
+				$ret['suggestions'][] = $prefix . $term;
482
+			}
483
+		}
484
+
485
+		// Suggestions (requires custom setup, assumes spellcheck.collate=true)
486 486
 //		if(isset($res->spellcheck->suggestions->collation)) {
487 487
 //			$ret['Suggestion'] = $res->spellcheck->suggestions->collation;
488 488
 //		}
489 489
 
490
-        return $ret;
491
-    }
492
-
493
-    /**
494
-     * @param $facets
495
-     * @return array
496
-     */
497
-    protected function buildFacetParams(array $facets)
498
-    {
499
-        $params = array();
500
-
501
-        if (!empty($facets)) {
502
-            $params['facet'] = 'true';
503
-
504
-            foreach ($facets as $name => $spec) {
505
-                // With our current implementation, "range" facets aren't true facets in solr terms.
506
-                // They're just a type of filter which can be handled elsewhere.
507
-                // For the other types we just ignore the rest of the spec and let Solr do its thing
508
-                if ($spec['Type'] != ShopSearch::FACET_TYPE_RANGE && isset($this->fieldMap[$name])) {
509
-                    $params['facet.field'] = $this->fieldMap[$name];
510
-                }
511
-            }
512
-        }
513
-
514
-        return $params;
515
-    }
516
-
517
-
518
-    /**
519
-     * Fulltextsearch module doesn't yet support facets very well, so I've just copied this function here so
520
-     * we have access to the results. I'd prefer to modify it minimally so we can eventually get rid of it
521
-     * once they add faceting or hooks to get directly at the returned response.
522
-     *
523
-     * @param SearchQuery $query
524
-     * @param integer $offset
525
-     * @param integer $limit
526
-     * @param  Array $params Extra request parameters passed through to Solr
527
-     * @param array $facetSpec - Added for ShopSearch so we can process the facets
528
-     * @return ArrayData Map with the following keys:
529
-     *  - 'Matches': ArrayList of the matched object instances
530
-     */
531
-    public function search(SearchQuery $query, $offset = -1, $limit = -1, $params = array(), $facetSpec = array())
532
-    {
533
-        $service = $this->getService();
534
-
535
-        SearchVariant::with(count($query->classes) == 1 ? $query->classes[0]['class'] : null)->call('alterQuery', $query, $this);
536
-
537
-        $q = array();
538
-        $fq = array();
539
-
540
-        // Build the search itself
541
-
542
-        foreach ($query->search as $search) {
543
-            $text = $search['text'];
544
-            preg_match_all('/"[^"]*"|\S+/', $text, $parts);
545
-
546
-            $fuzzy = $search['fuzzy'] ? '~' : '';
547
-
548
-            foreach ($parts[0] as $part) {
549
-                $fields = (isset($search['fields'])) ? $search['fields'] : array();
550
-                if (isset($search['boost'])) {
551
-                    $fields = array_merge($fields, array_keys($search['boost']));
552
-                }
553
-                if ($fields) {
554
-                    $searchq = array();
555
-                    foreach ($fields as $field) {
556
-                        $boost = (isset($search['boost'][$field])) ? '^' . $search['boost'][$field] : '';
557
-                        $searchq[] = "{$field}:".$part.$fuzzy.$boost;
558
-                    }
559
-                    $q[] = '+('.implode(' OR ', $searchq).')';
560
-                } else {
561
-                    $q[] = '+'.$part.$fuzzy;
562
-                }
563
-            }
564
-        }
565
-
566
-        // Filter by class if requested
567
-
568
-        $classq = array();
569
-
570
-        foreach ($query->classes as $class) {
571
-            if (!empty($class['includeSubclasses'])) {
572
-                $classq[] = 'ClassHierarchy:'.$class['class'];
573
-            } else {
574
-                $classq[] = 'ClassName:'.$class['class'];
575
-            }
576
-        }
577
-
578
-        if ($classq) {
579
-            $fq[] = '+('.implode(' ', $classq).')';
580
-        }
581
-
582
-        // Filter by filters
583
-
584
-        foreach ($query->require as $field => $values) {
585
-            $requireq = array();
586
-
587
-            foreach ($values as $value) {
588
-                if ($value === SearchQuery::$missing) {
589
-                    $requireq[] = "(*:* -{$field}:[* TO *])";
590
-                } elseif ($value === SearchQuery::$present) {
591
-                    $requireq[] = "{$field}:[* TO *]";
592
-                } elseif ($value instanceof SearchQuery_Range) {
593
-                    $start = $value->start;
594
-                    if ($start === null) {
595
-                        $start = '*';
596
-                    }
597
-                    $end = $value->end;
598
-                    if ($end === null) {
599
-                        $end = '*';
600
-                    }
601
-                    $requireq[] = "$field:[$start TO $end]";
602
-                } else {
603
-                    $requireq[] = $field.':"'.$value.'"';
604
-                }
605
-            }
606
-
607
-            $fq[] = '+('.implode(' ', $requireq).')';
608
-        }
609
-
610
-        foreach ($query->exclude as $field => $values) {
611
-            $excludeq = array();
612
-            $missing = false;
613
-
614
-            foreach ($values as $value) {
615
-                if ($value === SearchQuery::$missing) {
616
-                    $missing = true;
617
-                } elseif ($value === SearchQuery::$present) {
618
-                    $excludeq[] = "{$field}:[* TO *]";
619
-                } elseif ($value instanceof SearchQuery_Range) {
620
-                    $start = $value->start;
621
-                    if ($start === null) {
622
-                        $start = '*';
623
-                    }
624
-                    $end = $value->end;
625
-                    if ($end === null) {
626
-                        $end = '*';
627
-                    }
628
-                    $excludeq[] = "$field:[$start TO $end]";
629
-                } else {
630
-                    $excludeq[] = $field.':"'.$value.'"';
631
-                }
632
-            }
633
-
634
-            $fq[] = ($missing ? "+{$field}:[* TO *] " : '') . '-('.implode(' ', $excludeq).')';
635
-        }
490
+		return $ret;
491
+	}
492
+
493
+	/**
494
+	 * @param $facets
495
+	 * @return array
496
+	 */
497
+	protected function buildFacetParams(array $facets)
498
+	{
499
+		$params = array();
500
+
501
+		if (!empty($facets)) {
502
+			$params['facet'] = 'true';
503
+
504
+			foreach ($facets as $name => $spec) {
505
+				// With our current implementation, "range" facets aren't true facets in solr terms.
506
+				// They're just a type of filter which can be handled elsewhere.
507
+				// For the other types we just ignore the rest of the spec and let Solr do its thing
508
+				if ($spec['Type'] != ShopSearch::FACET_TYPE_RANGE && isset($this->fieldMap[$name])) {
509
+					$params['facet.field'] = $this->fieldMap[$name];
510
+				}
511
+			}
512
+		}
513
+
514
+		return $params;
515
+	}
516
+
517
+
518
+	/**
519
+	 * Fulltextsearch module doesn't yet support facets very well, so I've just copied this function here so
520
+	 * we have access to the results. I'd prefer to modify it minimally so we can eventually get rid of it
521
+	 * once they add faceting or hooks to get directly at the returned response.
522
+	 *
523
+	 * @param SearchQuery $query
524
+	 * @param integer $offset
525
+	 * @param integer $limit
526
+	 * @param  Array $params Extra request parameters passed through to Solr
527
+	 * @param array $facetSpec - Added for ShopSearch so we can process the facets
528
+	 * @return ArrayData Map with the following keys:
529
+	 *  - 'Matches': ArrayList of the matched object instances
530
+	 */
531
+	public function search(SearchQuery $query, $offset = -1, $limit = -1, $params = array(), $facetSpec = array())
532
+	{
533
+		$service = $this->getService();
534
+
535
+		SearchVariant::with(count($query->classes) == 1 ? $query->classes[0]['class'] : null)->call('alterQuery', $query, $this);
536
+
537
+		$q = array();
538
+		$fq = array();
539
+
540
+		// Build the search itself
541
+
542
+		foreach ($query->search as $search) {
543
+			$text = $search['text'];
544
+			preg_match_all('/"[^"]*"|\S+/', $text, $parts);
545
+
546
+			$fuzzy = $search['fuzzy'] ? '~' : '';
547
+
548
+			foreach ($parts[0] as $part) {
549
+				$fields = (isset($search['fields'])) ? $search['fields'] : array();
550
+				if (isset($search['boost'])) {
551
+					$fields = array_merge($fields, array_keys($search['boost']));
552
+				}
553
+				if ($fields) {
554
+					$searchq = array();
555
+					foreach ($fields as $field) {
556
+						$boost = (isset($search['boost'][$field])) ? '^' . $search['boost'][$field] : '';
557
+						$searchq[] = "{$field}:".$part.$fuzzy.$boost;
558
+					}
559
+					$q[] = '+('.implode(' OR ', $searchq).')';
560
+				} else {
561
+					$q[] = '+'.$part.$fuzzy;
562
+				}
563
+			}
564
+		}
565
+
566
+		// Filter by class if requested
567
+
568
+		$classq = array();
569
+
570
+		foreach ($query->classes as $class) {
571
+			if (!empty($class['includeSubclasses'])) {
572
+				$classq[] = 'ClassHierarchy:'.$class['class'];
573
+			} else {
574
+				$classq[] = 'ClassName:'.$class['class'];
575
+			}
576
+		}
577
+
578
+		if ($classq) {
579
+			$fq[] = '+('.implode(' ', $classq).')';
580
+		}
581
+
582
+		// Filter by filters
583
+
584
+		foreach ($query->require as $field => $values) {
585
+			$requireq = array();
586
+
587
+			foreach ($values as $value) {
588
+				if ($value === SearchQuery::$missing) {
589
+					$requireq[] = "(*:* -{$field}:[* TO *])";
590
+				} elseif ($value === SearchQuery::$present) {
591
+					$requireq[] = "{$field}:[* TO *]";
592
+				} elseif ($value instanceof SearchQuery_Range) {
593
+					$start = $value->start;
594
+					if ($start === null) {
595
+						$start = '*';
596
+					}
597
+					$end = $value->end;
598
+					if ($end === null) {
599
+						$end = '*';
600
+					}
601
+					$requireq[] = "$field:[$start TO $end]";
602
+				} else {
603
+					$requireq[] = $field.':"'.$value.'"';
604
+				}
605
+			}
606
+
607
+			$fq[] = '+('.implode(' ', $requireq).')';
608
+		}
609
+
610
+		foreach ($query->exclude as $field => $values) {
611
+			$excludeq = array();
612
+			$missing = false;
613
+
614
+			foreach ($values as $value) {
615
+				if ($value === SearchQuery::$missing) {
616
+					$missing = true;
617
+				} elseif ($value === SearchQuery::$present) {
618
+					$excludeq[] = "{$field}:[* TO *]";
619
+				} elseif ($value instanceof SearchQuery_Range) {
620
+					$start = $value->start;
621
+					if ($start === null) {
622
+						$start = '*';
623
+					}
624
+					$end = $value->end;
625
+					if ($end === null) {
626
+						$end = '*';
627
+					}
628
+					$excludeq[] = "$field:[$start TO $end]";
629
+				} else {
630
+					$excludeq[] = $field.':"'.$value.'"';
631
+				}
632
+			}
633
+
634
+			$fq[] = ($missing ? "+{$field}:[* TO *] " : '') . '-('.implode(' ', $excludeq).')';
635
+		}
636 636
 
637 637
 //		if(!headers_sent()) {
638 638
 //			if ($q) header('X-Query: '.implode(' ', $q));
639 639
 //			if ($fq) header('X-Filters: "'.implode('", "', $fq).'"');
640 640
 //		}
641 641
 
642
-        if ($offset == -1) {
643
-            $offset = $query->start;
644
-        }
645
-        if ($limit == -1) {
646
-            $limit = $query->limit;
647
-        }
648
-        if ($limit == -1) {
649
-            $limit = SearchQuery::$default_page_size;
650
-        }
651
-
652
-        $params = array_merge($params, array('fq' => implode(' ', $fq)));
653
-
654
-        $res = $service->search(
655
-            $q ? implode(' ', $q) : '*:*',
656
-            $offset,
657
-            $limit,
658
-            $params,
659
-            Apache_Solr_Service::METHOD_POST
660
-        );
661
-        //Debug::dump($res);
662
-
663
-        $results = new ArrayList();
664
-        if ($res->getHttpStatus() >= 200 && $res->getHttpStatus() < 300) {
665
-            foreach ($res->response->docs as $doc) {
666
-                $result = DataObject::get_by_id($doc->ClassName, $doc->ID);
667
-                if ($result) {
668
-                    $results->push($result);
669
-
670
-                    // Add highlighting (optional)
671
-                    $docId = $doc->_documentid;
672
-                    if ($res->highlighting && $res->highlighting->$docId) {
673
-                        // TODO Create decorator class for search results rather than adding arbitrary object properties
674
-                        // TODO Allow specifying highlighted field, and lazy loading
675
-                        // in case the search API needs another query (similar to SphinxSearchable->buildExcerpt()).
676
-                        $combinedHighlights = array();
677
-                        foreach ($res->highlighting->$docId as $field => $highlights) {
678
-                            $combinedHighlights = array_merge($combinedHighlights, $highlights);
679
-                        }
680
-
681
-                        // Remove entity-encoded U+FFFD replacement character. It signifies non-displayable characters,
682
-                        // and shows up as an encoding error in browsers.
683
-                        $result->Excerpt = DBField::create_field(
684
-                            'HTMLText',
685
-                            str_replace(
686
-                                '&#65533;',
687
-                                '',
688
-                                implode(' ... ', $combinedHighlights)
689
-                            )
690
-                        );
691
-                    }
692
-                }
693
-            }
694
-            $numFound = $res->response->numFound;
695
-        } else {
696
-            $numFound = 0;
697
-        }
698
-
699
-        $ret = array();
700
-        $ret['Matches'] = new PaginatedList($results);
701
-        $ret['Matches']->setLimitItems(false);
702
-        // Tell PaginatedList how many results there are
703
-        $ret['Matches']->setTotalItems($numFound);
704
-        // Results for current page start at $offset
705
-        $ret['Matches']->setPageStart($offset);
706
-        // Results per page
707
-        $ret['Matches']->setPageLength($limit);
708
-
709
-        // Facets
710
-        //Debug::dump($res);
711
-        if (isset($res->facet_counts->facet_fields)) {
712
-            $ret['Facets'] = $this->buildFacetResults($res->facet_counts->facet_fields, $facetSpec);
713
-        }
714
-
715
-        // Suggestions (requires custom setup, assumes spellcheck.collate=true)
716
-        if (isset($res->spellcheck->suggestions->collation)) {
717
-            $ret['Suggestion'] = $res->spellcheck->suggestions->collation;
718
-        }
719
-
720
-        return new ArrayData($ret);
721
-    }
722
-
723
-
724
-    /**
725
-     * @param stdClass $facetFields
726
-     * @param array $facetSpec
727
-     * @return ArrayList
728
-     */
729
-    protected function buildFacetResults($facetFields, array $facetSpec)
730
-    {
731
-        $out = new ArrayList;
732
-
733
-        foreach ($facetSpec as $field => $facet) {
734
-            if ($facet['Type'] == ShopSearch::FACET_TYPE_RANGE) {
735
-                // If it's a range facet, set up the min/max
736
-                // TODO: we could probably get the real min and max with solr's range faceting if we tried
737
-                if (isset($facet['RangeMin'])) {
738
-                    $facet['MinValue'] = $facet['RangeMin'];
739
-                }
740
-                if (isset($facet['RangeMax'])) {
741
-                    $facet['MaxValue'] = $facet['RangeMax'];
742
-                }
743
-                $out->push(new ArrayData($facet));
744
-            } elseif (isset($this->fieldMap[$field])) {
745
-                // Otherwise, look through Solr's results
746
-                $mySolrName = $this->fieldMap[$field];
747
-                foreach ($facetFields as $solrName => $values) {
748
-                    if ($solrName == $mySolrName) {
749
-                        // we found a match, look through the values we were given
750
-                        foreach ($values as $val => $count) {
751
-                            if (!isset($facet['Values'][$val])) {
752
-                                // for link type facets we want to add anything
753
-                                // for checkboxes, if it's not in the provided list we leave it out
754
-                                if ($facet['Type'] != ShopSearch::FACET_TYPE_CHECKBOX && $count > 0) {
755
-                                    $facet['Values'][$val] = new ArrayData(array(
756
-                                        'Label'     => $val,
757
-                                        'Value'     => $val,
758
-                                        'Count'     => $count,
759
-                                    ));
760
-                                }
761
-                            } elseif ($facet['Values'][$val]) {
762
-                                $facet['Values'][$val]->Count = $count;
763
-                            }
764
-                        }
765
-                    }
766
-                }
767
-
768
-                // then add that to the stack
769
-                $facet['Values'] = new ArrayList($facet['Values']);
770
-                $out->push(new ArrayData($facet));
771
-            }
772
-        }
773
-
774
-        //Debug::dump($out);
775
-        return $out;
776
-    }
642
+		if ($offset == -1) {
643
+			$offset = $query->start;
644
+		}
645
+		if ($limit == -1) {
646
+			$limit = $query->limit;
647
+		}
648
+		if ($limit == -1) {
649
+			$limit = SearchQuery::$default_page_size;
650
+		}
651
+
652
+		$params = array_merge($params, array('fq' => implode(' ', $fq)));
653
+
654
+		$res = $service->search(
655
+			$q ? implode(' ', $q) : '*:*',
656
+			$offset,
657
+			$limit,
658
+			$params,
659
+			Apache_Solr_Service::METHOD_POST
660
+		);
661
+		//Debug::dump($res);
662
+
663
+		$results = new ArrayList();
664
+		if ($res->getHttpStatus() >= 200 && $res->getHttpStatus() < 300) {
665
+			foreach ($res->response->docs as $doc) {
666
+				$result = DataObject::get_by_id($doc->ClassName, $doc->ID);
667
+				if ($result) {
668
+					$results->push($result);
669
+
670
+					// Add highlighting (optional)
671
+					$docId = $doc->_documentid;
672
+					if ($res->highlighting && $res->highlighting->$docId) {
673
+						// TODO Create decorator class for search results rather than adding arbitrary object properties
674
+						// TODO Allow specifying highlighted field, and lazy loading
675
+						// in case the search API needs another query (similar to SphinxSearchable->buildExcerpt()).
676
+						$combinedHighlights = array();
677
+						foreach ($res->highlighting->$docId as $field => $highlights) {
678
+							$combinedHighlights = array_merge($combinedHighlights, $highlights);
679
+						}
680
+
681
+						// Remove entity-encoded U+FFFD replacement character. It signifies non-displayable characters,
682
+						// and shows up as an encoding error in browsers.
683
+						$result->Excerpt = DBField::create_field(
684
+							'HTMLText',
685
+							str_replace(
686
+								'&#65533;',
687
+								'',
688
+								implode(' ... ', $combinedHighlights)
689
+							)
690
+						);
691
+					}
692
+				}
693
+			}
694
+			$numFound = $res->response->numFound;
695
+		} else {
696
+			$numFound = 0;
697
+		}
698
+
699
+		$ret = array();
700
+		$ret['Matches'] = new PaginatedList($results);
701
+		$ret['Matches']->setLimitItems(false);
702
+		// Tell PaginatedList how many results there are
703
+		$ret['Matches']->setTotalItems($numFound);
704
+		// Results for current page start at $offset
705
+		$ret['Matches']->setPageStart($offset);
706
+		// Results per page
707
+		$ret['Matches']->setPageLength($limit);
708
+
709
+		// Facets
710
+		//Debug::dump($res);
711
+		if (isset($res->facet_counts->facet_fields)) {
712
+			$ret['Facets'] = $this->buildFacetResults($res->facet_counts->facet_fields, $facetSpec);
713
+		}
714
+
715
+		// Suggestions (requires custom setup, assumes spellcheck.collate=true)
716
+		if (isset($res->spellcheck->suggestions->collation)) {
717
+			$ret['Suggestion'] = $res->spellcheck->suggestions->collation;
718
+		}
719
+
720
+		return new ArrayData($ret);
721
+	}
722
+
723
+
724
+	/**
725
+	 * @param stdClass $facetFields
726
+	 * @param array $facetSpec
727
+	 * @return ArrayList
728
+	 */
729
+	protected function buildFacetResults($facetFields, array $facetSpec)
730
+	{
731
+		$out = new ArrayList;
732
+
733
+		foreach ($facetSpec as $field => $facet) {
734
+			if ($facet['Type'] == ShopSearch::FACET_TYPE_RANGE) {
735
+				// If it's a range facet, set up the min/max
736
+				// TODO: we could probably get the real min and max with solr's range faceting if we tried
737
+				if (isset($facet['RangeMin'])) {
738
+					$facet['MinValue'] = $facet['RangeMin'];
739
+				}
740
+				if (isset($facet['RangeMax'])) {
741
+					$facet['MaxValue'] = $facet['RangeMax'];
742
+				}
743
+				$out->push(new ArrayData($facet));
744
+			} elseif (isset($this->fieldMap[$field])) {
745
+				// Otherwise, look through Solr's results
746
+				$mySolrName = $this->fieldMap[$field];
747
+				foreach ($facetFields as $solrName => $values) {
748
+					if ($solrName == $mySolrName) {
749
+						// we found a match, look through the values we were given
750
+						foreach ($values as $val => $count) {
751
+							if (!isset($facet['Values'][$val])) {
752
+								// for link type facets we want to add anything
753
+								// for checkboxes, if it's not in the provided list we leave it out
754
+								if ($facet['Type'] != ShopSearch::FACET_TYPE_CHECKBOX && $count > 0) {
755
+									$facet['Values'][$val] = new ArrayData(array(
756
+										'Label'     => $val,
757
+										'Value'     => $val,
758
+										'Count'     => $count,
759
+									));
760
+								}
761
+							} elseif ($facet['Values'][$val]) {
762
+								$facet['Values'][$val]->Count = $count;
763
+							}
764
+						}
765
+					}
766
+				}
767
+
768
+				// then add that to the stack
769
+				$facet['Values'] = new ArrayList($facet['Values']);
770
+				$out->push(new ArrayData($facet));
771
+			}
772
+		}
773
+
774
+		//Debug::dump($out);
775
+		return $out;
776
+	}
777 777
 }
Please login to merge, or discard this patch.