Completed
Push — dev2 ( 92da4c...1cdcda )
by Gordon
03:05
created

ElasticSearchPage_Controller::index()   C

Complexity

Conditions 8
Paths 152

Size

Total Lines 77
Code Lines 40

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 77
rs 5.4212
cc 8
eloc 40
nc 152
nop 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
use Elastica\Document;
4
use Elastica\Query;
5
use \SilverStripe\Elastica\ResultList;
6
use Elastica\Query\QueryString;
7
use Elastica\Aggregation\Filter;
8
use Elastica\Filter\Term;
9
use Elastica\Filter\BoolAnd;
10
use Elastica\Aggregation\Terms;
11
use Elastica\Query\Filtered;
12
use Elastica\Query\Range;
13
use \SilverStripe\Elastica\ElasticSearcher;
14
use \SilverStripe\Elastica\Searchable;
15
use \SilverStripe\Elastica\QueryGenerator;
16
use \SilverStripe\Elastica\ElasticaUtil;
17
18
class ElasticSearchPage_Controller extends Page_Controller {
19
20
	private static $allowed_actions = array('SearchForm', 'submit', 'index', 'similar');
21
22
	public function init() {
23
		parent::init();
24
25
		Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.js');
26
		Requirements::javascript("elastica/javascript/jquery.autocomplete.js");
27
		Requirements::javascript("elastica/javascript/elastica.js");
28
		Requirements::css("elastica/css/elastica.css");
29
30
		$this->SearchPage = Controller::curr()->dataRecord;
31
	}
32
33
34
35
	/*
36
	Find DataObjects in Elasticsearch similar to the one selected.  Note that aggregations are not
37
	taken into account, merely the text of the selected document.
38
	 */
39
	public function similar() {
40
		//FIXME double check security, ie if escaping needed
41
		$class = $this->request->param('ID');
42
		$instanceID = $this->request->param('OtherID');
43
44
		$data = $this->initialiseDataArray();
45
		$es = $this->primeElasticSearcherFromRequest();
46
		$this->setMoreLikeThisParamsFromRequest($es);
47
48
		$this->addSiteTreeFilterIfRequired($es);
49
50
		// get the edited fields to search from the database for this search page
51
		// Convert this into a name => weighting array
52
		$fieldsToSearch = array();
53
		$editedSearchFields = $this->ElasticaSearchableFields()->filter(array(
54
			'Active' => true,
55
			'SimilarSearchable' => true
56
		));
57
58
		foreach($editedSearchFields->getIterator() as $searchField) {
59
			$fieldsToSearch[$searchField->Name] = $searchField->Weight;
60
		}
61
62
		// Use the standard field for more like this, ie not stemmed
63
		foreach($fieldsToSearch as $field => $value) {
64
			$fieldsToSearch[$field . '.standard'] = $value;
65
			unset($fieldsToSearch[$field]);
66
		}
67
68
		try {
69
			// Simulate server being down for testing purposes
70
			if($this->request->getVar('ServerDown')) {
71
				throw new Elastica\Exception\Connection\HttpException('Unable to reach search server');
72
			}
73
			if(class_exists($class)) {
74
				$instance = \DataObject::get_by_id($class, $instanceID);
75
76
				$paginated = $es->moreLikeThis($instance, $fieldsToSearch);
77
78
				$this->Aggregations = $es->getAggregations();
79
				$this->successfulSearch($data, $paginated);
80
				$data['SimilarTo'] = $instance;
81
82
83
				$moreLikeThisTerms = $paginated->getList()->MoreLikeThisTerms;
84
				$fieldToTerms = new ArrayList();
85
				foreach(array_keys($moreLikeThisTerms) as $fieldName) {
86
					$readableFieldName = str_replace('.standard', '', $fieldName);
87
					$fieldTerms = new ArrayList();
88
					foreach($moreLikeThisTerms[$fieldName] as $value) {
89
						$do = new DataObject();
90
						$do->Term = $value;
91
						$fieldTerms->push($do);
92
					}
93
94
					$do = new DataObject();
95
					$do->FieldName = $readableFieldName;
96
					$do->Terms = $fieldTerms;
97
					$fieldToTerms->push($do);
98
				}
99
100
				$data['SimilarSearchTerms'] = $fieldToTerms;
101
			} else {
102
				// class does not exist
103
				$data['ErrorMessage'] = "Class $class is either not found or not searchable\n";
104
			}
105
		} catch (\InvalidArgumentException $e) {
106
			$data['ErrorMessage'] = "Class $class is either not found or not searchable\n";
107
		} catch (Elastica\Exception\Connection\HttpException $e) {
108
			$data['ErrorMessage'] = 'Unable to connect to search server';
109
		}
110
111
112
		$elapsed = $this->calculateTime();
113
		$data['ElapsedTime'] = $elapsed;
114
115
116
		return $this->renderResults($data);
117
	}
118
119
120
121
	/*
122
	Display the search form. If the query parameter exists, search against Elastica
123
	and render results accordingly.
124
	 */
125
	public function index() {
126
		$data = $this->initialiseDataArray();
127
		$es = $this->primeElasticSearcherFromRequest();
128
129
		// Do not show suggestions if this flag is set
130
		$ignoreSuggestions = null !== $this->request->getVar('is');
131
132
133
		// query string
134
		$queryTextParam = $this->request->getVar('q');
135
		$queryText = !empty($queryTextParam) ? $queryTextParam : '';
136
137
		$testMode = !empty($this->request->getVar('TestMode'));
138
139
		$this->addAggregationFilters($es);
140
		$this->addSiteTreeFilterIfRequired($es);
141
142
		// set the optional aggregation manipulator
143
		// In the event of a manipulator being present, show all the results for search
144
		// Otherwise aggregations are all zero
145
		if($this->SearchHelper) {
146
			$es->setQueryResultManipulator($this->SearchHelper);
147
			$es->showResultsForEmptySearch();
148
		} else {
149
			$es->hideResultsForEmptySearch();
150
		}
151
152
		// get the edited fields to search from the database for this search page
153
		// Convert this into a name => weighting array
154
		$fieldsToSearch = array();
155
		$editedSearchFields = $this->ElasticaSearchableFields()->filter(array(
156
			'Active' => true,
157
			'Searchable' => true
158
		));
159
160
		foreach($editedSearchFields->getIterator() as $searchField) {
161
			$fieldsToSearch[$searchField->Name] = $searchField->Weight;
162
		}
163
164
		$paginated = null;
165
		try {
166
			// Simulate server being down for testing purposes
167
			if(!empty($this->request->getVar('ServerDown'))) {
168
				throw new Elastica\Exception\Connection\HttpException('Unable to reach search server');
169
			}
170
171
			// now actually perform the search using the original query
172
			$paginated = $es->search($queryText, $fieldsToSearch, $testMode);
173
174
			// This is the case of the original query having a better one suggested.  Do a
175
			// second search for the suggested query, throwing away the original
176
			if($es->hasSuggestedQuery() && !$ignoreSuggestions) {
177
				$data['SuggestedQuery'] = $es->getSuggestedQuery();
178
				$data['SuggestedQueryHighlighted'] = $es->getSuggestedQueryHighlighted();
179
				//Link for if the user really wants to try their original query
180
				$sifLink = rtrim($this->Link(), '/') . '?q=' . $queryText . '&is=1';
181
				$data['SearchInsteadForLink'] = $sifLink;
182
				$paginated = $es->search($es->getSuggestedQuery(), $fieldsToSearch);
183
184
			}
185
186
			$elapsed = $this->calculateTime();
187
			$data['ElapsedTime'] = $elapsed;
188
189
			$this->Aggregations = $es->getAggregations();
190
			$this->successfulSearch($data, $paginated);
191
192
		} catch (Elastica\Exception\Connection\HttpException $e) {
193
			$data['ErrorMessage'] = 'Unable to connect to search server';
194
		}
195
196
		$data['OriginalQuery'] = $queryText;
197
		$data['IgnoreSuggestions'] = $ignoreSuggestions;
198
199
		return $this->renderResults($data);
200
201
	}
202
203
204
	private function successfulSearch(&$data, $paginated) {
205
		$data['SearchResults'] = $paginated;
206
		$data['SearchPerformed'] = true;
207
		$data['NumberOfResults'] = $paginated->getTotalItems();
208
		$data['SearchPageLink'] = $this->SearchPage->Link();
209
	}
210
211
212
	/*
213
	Return true if the query is not empty
214
	 */
215
	public function QueryIsEmpty() {
216
		return empty($this->request->getVar('q'));
217
	}
218
219
220
	/**
221
	 * Process submission of the search form, redirecting to a URL that will render search results
222
	 * @param  array $data form data
223
	 * @param  Form $form form
224
	 */
225
	public function submit($data, $form) {
226
		$queryText = $data['q'];
227
		$url = $this->Link();
228
		$url = rtrim($url, '/');
229
		$link = rtrim($url, '/') . '?q=' . $queryText . '&sfid=' . $data['identifier'];
230
		$this->redirect($link);
231
	}
232
233
234
	/*
235
	Obtain an instance of the form
236
	*/
237
	public function SearchForm() {
238
		$form = new ElasticSearchForm($this, 'SearchForm');
239
		$fields = $form->Fields();
240
		$elasticaSearchPage = Controller::curr()->dataRecord;
241
		$identifierField = new HiddenField('identifier');
242
		$identifierField->setValue($elasticaSearchPage->Identifier);
243
244
		$fields->push($identifierField);
245
		$queryField = $fields->fieldByName('q');
246
247
		 if($this->isParamSet('q') && $this->isParamSet('sfid')) {
248
		 	$sfid = $this->request->getVar('sfid');
249
			if($sfid == $elasticaSearchPage->Identifier) {
250
251
				$queryText = $this->request->getVar('q');
252
				$queryField->setValue($queryText);
253
			}
254
255
		}
256
257
		if($this->action == 'similar') {
258
			$queryField->setDisabled(true);
259
			$actions = $form->Actions();
260
			if(!empty($actions)) {
261
				foreach($actions as $field) {
262
					$field->setDisabled(true);
263
				}
264
			}
265
266
		}
267
268
		if($this->AutoCompleteFieldID > 0) {
269
			ElasticaUtil::addAutocompleteToQueryField(
270
				$queryField,
271
				$this->ClassesToSearch,
272
				$this->SiteTreeOnly,
273
				$this->Link(),
274
				$this->AutocompleteFunction()->Slug
275
			);
276
		}
277
		return $form;
278
	}
279
280
281
	/**
282
	 * @param string $paramName
283
	 */
284
	private function isParamSet($paramName) {
285
		return !empty($this->request->getVar($paramName));
286
	}
287
288
289
	/**
290
	 * Set the start page from the request and results per page for a given searcher object
291
	 */
292
	private function primeElasticSearcherFromRequest() {
293
		$elasticSearcher = new ElasticSearcher();
294
		// start, and page length, i.e. pagination
295
		$startParam = $this->request->getVar('start');
296
		$start = isset($startParam) ? $startParam : 0;
297
		$elasticSearcher->setStart($start);
298
		$this->StartTime = microtime(true);
299
		$elasticSearcher->setPageLength($this->SearchPage->ResultsPerPage);
300
		return $elasticSearcher;
301
	}
302
303
304
	/**
305
	 * Set the admin configured similarity parameters
306
	 * @param \SilverStripe\Elastica\ElasticSearcher &$elasticSearcher ElasticaSearcher object
307
	 */
308
	private function setMoreLikeThisParamsFromRequest(&$elasticSearcher) {
309
		$elasticSearcher->setMinTermFreq($this->MinTermFreq);
310
		$elasticSearcher->setMaxTermFreq($this->MaxTermFreq);
311
		$elasticSearcher->setMinDocFreq($this->MinDocFreq);
312
		$elasticSearcher->setMaxDocFreq($this->MaxDocFreq);
313
		$elasticSearcher->setMinWordLength($this->MinWordLength);
314
		$elasticSearcher->setMaxWordLength($this->MaxWordLength);
315
		$elasticSearcher->setMinShouldMatch($this->MinShouldMatch);
316
		$elasticSearcher->setSimilarityStopWords($this->SimilarityStopWords);
317
	}
318
319
320
	private function addAggregationFilters(&$es) {
321
		$ignore = \Config::inst()->get('Elastica', 'BlackList');
322
		foreach($this->request->getVars() as $key => $value) {
323
			if(!in_array($key, $ignore)) {
324
				$es->addFilter($key, $value);
325
			}
326
		}
327
	}
328
329
330
	private function addSiteTreeFilterIfRequired(&$es) {
331
		// filter by class or site tree
332
		if($this->SearchPage->SiteTreeOnly) {
333
			$es->addFilter('IsInSiteTree', true);
334
		} else {
335
			$es->setClasses($this->SearchPage->ClassesToSearch);
336
		}
337
	}
338
339
340
	private function initialiseDataArray() {
341
		return array(
342
			'Content' => $this->Content,
343
			'Title' => $this->Title,
344
			'SearchPerformed' => false
345
		);
346
	}
347
348
349
	private function renderResults($data) {
350
		// allow the optional use of overriding the search result page, e.g. for photos, maps or facets
351
		if($this->hasExtension('PageControllerTemplateOverrideExtension')) {
352
			return $this->useTemplateOverride($data);
353
		} else {
354
			return $data;
355
		}
356
	}
357
358
359
	private function calculateTime() {
360
		$endTime = microtime(true);
361
		$elapsed = round(100 * ($endTime - $this->StartTime)) / 100;
362
		return $elapsed;
363
	}
364
}
365