Completed
Push — dev2 ( 268ac3...971ce9 )
by Gordon
03:05
created

ElasticSearchPage_Controller::similar()   F

Complexity

Conditions 12
Paths 2144

Size

Total Lines 127
Code Lines 77

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 12
eloc 77
nc 2144
nop 0
dl 0
loc 127
rs 2

How to fix   Long Method    Complexity   

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
31
32
33
	/*
34
	Find DataObjects in Elasticsearch similar to the one selected.  Note that aggregations are not
35
	taken into account, merely the text of the selected document.
36
	 */
37
	public function similar() {
38
		//FIXME double check security, ie if escaping needed
39
		$class = $this->request->param('ID');
40
		$instanceID = $this->request->param('OtherID');
41
42
		$data = array(
43
			'Content' => $this->Content,
44
			'Title' => $this->Title,
45
			'SearchPerformed' => false
46
		);
47
48
		// record the time
49
		$startTime = microtime(true);
50
51
		//instance of ElasticPage associated with this controller
52
		$ep = Controller::curr()->dataRecord;
53
54
		// use an Elastic Searcher, which needs primed from URL params
55
		$es = new ElasticSearcher();
56
57
		// start, and page length, i.e. pagination
58
		$startParam = $this->request->getVar('start');
59
		$start = isset($startParam) ? $startParam : 0;
60
		$es->setStart($start);
61
		$es->setPageLength($ep->ResultsPerPage);
62
63
64
		$es->setMinTermFreq($this->MinTermFreq);
65
		$es->setMaxTermFreq($this->MaxTermFreq);
66
		$es->setMinDocFreq($this->MinDocFreq);
67
		$es->setMaxDocFreq($this->MaxDocFreq);
68
		$es->setMinWordLength($this->MinWordLength);
69
		$es->setMaxWordLength($this->MaxWordLength);
70
		$es->setMinShouldMatch($this->MinShouldMatch);
71
		$es->setSimilarityStopWords($this->SimilarityStopWords);
72
73
74
		// filter by class or site tree
75
		if($ep->SiteTreeOnly) {
76
			T7; //FIXME test missing
77
			$es->addFilter('IsInSiteTree', true);
78
		} else {
79
			$es->setClasses($ep->ClassesToSearch);
80
		}
81
82
83
		// get the edited fields to search from the database for this search page
84
		// Convert this into a name => weighting array
85
		$fieldsToSearch = array();
86
		$editedSearchFields = $this->ElasticaSearchableFields()->filter(array(
87
			'Active' => true,
88
			'SimilarSearchable' => true
89
		));
90
91
		foreach($editedSearchFields->getIterator() as $searchField) {
92
			$fieldsToSearch[$searchField->Name] = $searchField->Weight;
93
		}
94
95
		// Use the standard field for more like this, ie not stemmed
96
		foreach($fieldsToSearch as $field => $value) {
97
			$fieldsToSearch[$field . '.standard'] = $value;
98
			unset($fieldsToSearch[$field]);
99
		}
100
101
		try {
102
			// Simulate server being down for testing purposes
103
			if($this->request->getVar('ServerDown')) {
104
				throw new Elastica\Exception\Connection\HttpException('Unable to reach search server');
105
			}
106
			if(class_exists($class)) {
107
				$instance = \DataObject::get_by_id($class, $instanceID);
108
109
				$paginated = $es->moreLikeThis($instance, $fieldsToSearch);
110
111
				$this->Aggregations = $es->getAggregations();
112
				$data['SearchResults'] = $paginated;
113
				$data['SearchPerformed'] = true;
114
				$data['SearchPageLink'] = $ep->Link();
115
				$data['SimilarTo'] = $instance;
116
				$data['NumberOfResults'] = $paginated->getTotalItems();
117
118
119
				$moreLikeThisTerms = $paginated->getList()->MoreLikeThisTerms;
120
				$fieldToTerms = new ArrayList();
121
				foreach(array_keys($moreLikeThisTerms) as $fieldName) {
122
					$readableFieldName = str_replace('.standard', '', $fieldName);
123
					$fieldTerms = new ArrayList();
124
					foreach($moreLikeThisTerms[$fieldName] as $value) {
125
						$do = new DataObject();
126
						$do->Term = $value;
127
						$fieldTerms->push($do);
128
					}
129
130
					$do = new DataObject();
131
					$do->FieldName = $readableFieldName;
132
					$do->Terms = $fieldTerms;
133
					$fieldToTerms->push($do);
134
				}
135
136
				$data['SimilarSearchTerms'] = $fieldToTerms;
137
			} else {
138
				// class does not exist
139
				$data['ErrorMessage'] = "Class $class is either not found or not searchable\n";
140
			}
141
		} catch (\InvalidArgumentException $e) {
142
			$data['ErrorMessage'] = "Class $class is either not found or not searchable\n";
143
		} catch (Elastica\Exception\Connection\HttpException $e) {
144
			$data['ErrorMessage'] = 'Unable to connect to search server';
145
			$data['SearchPerformed'] = false;
146
		}
147
148
149
		// calculate time
150
		$endTime = microtime(true);
151
		$elapsed = round(100 * ($endTime - $startTime)) / 100;
152
153
		// store variables for the template to use
154
		$data['ElapsedTime'] = $elapsed;
155
		$data['Elapsed'] = $elapsed;
156
157
		// allow the optional use of overriding the search result page, e.g. for photos, maps or facets
158
		if($this->hasExtension('PageControllerTemplateOverrideExtension')) {
159
			return $this->useTemplateOverride($data);
160
		} else {
161
			return $data;
162
		}
163
	}
164
165
166
	/*
167
	Display the search form. If the query parameter exists, search against Elastica
168
	and render results accordingly.
169
	 */
170
	public function index() {
171
		$data = array(
172
			'Content' => $this->Content,
173
			'Title' => $this->Title,
174
			'SearchPerformed' => false
175
		);
176
177
		// record the time
178
		$startTime = microtime(true);
179
180
		//instance of ElasticPage associated with this controller
181
		$ep = Controller::curr()->dataRecord;
182
183
		// use an Elastic Searcher, which needs primed from URL params
184
		$es = new ElasticSearcher();
185
186
		// start, and page length, i.e. pagination
187
		$startParam = $this->request->getVar('start');
188
		$start = isset($startParam) ? $startParam : 0;
189
		$es->setStart($start);
190
		$es->setPageLength($ep->ResultsPerPage);
191
192
193
		// Do not show suggestions if this flag is set
194
		$ignoreSuggestions = isset($this->request->getVar('is'));
195
196
197
		// query string
198
		$queryTextParam = $this->request->getVar('q');
199
		$queryText = isset($queryTextParam) ? $queryTextParam : '';
200
201
		$testMode = isset($this->request->getVar('TestMode'));
202
203
		// filters for aggregations
204
		$ignore = \Config::inst()->get('Elastica', 'BlackList');
205
		foreach($this->request->getVars() as $key => $value) {
206
			if(!in_array($key, $ignore)) {
207
				$es->addFilter($key, $value);
208
			}
209
		}
210
211
		// filter by class or site tree
212
		if($ep->SiteTreeOnly) {
213
			$es->addFilter('IsInSiteTree', true);
214
		} else {
215
			$es->setClasses($ep->ClassesToSearch);
216
		}
217
218
		// set the optional aggregation manipulator
219
		// In the event of a manipulator being present, show all the results for search
220
		// Otherwise aggregations are all zero
221
		if($this->SearchHelper) {
222
			$es->setQueryResultManipulator($this->SearchHelper);
223
			$es->showResultsForEmptySearch();
224
		} else {
225
			$es->hideResultsForEmptySearch();
226
		}
227
228
		// get the edited fields to search from the database for this search page
229
		// Convert this into a name => weighting array
230
		$fieldsToSearch = array();
231
		$editedSearchFields = $this->ElasticaSearchableFields()->filter(array(
232
			'Active' => true,
233
			'Searchable' => true
234
		));
235
236
		foreach($editedSearchFields->getIterator() as $searchField) {
237
			$fieldsToSearch[$searchField->Name] = $searchField->Weight;
238
		}
239
240
		$paginated = null;
241
		try {
242
			// Simulate server being down for testing purposes
243
			if(isset($this->request->getVar('ServerDown')) {
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected '{'
Loading history...
Bug introduced by
Avoid IF statements that are always true or false
Loading history...
244
				throw new Elastica\Exception\Connection\HttpException('Unable to reach search server');
245
			}
246
247
			// now actually perform the search using the original query
248
			$paginated = $es->search($queryText, $fieldsToSearch, $testMode);
249
250
			// This is the case of the original query having a better one suggested.  Do a
251
			// second search for the suggested query, throwing away the original
252
			if($es->hasSuggestedQuery() && !$ignoreSuggestions) {
253
				$data['SuggestedQuery'] = $es->getSuggestedQuery();
254
				$data['SuggestedQueryHighlighted'] = $es->getSuggestedQueryHighlighted();
255
				//Link for if the user really wants to try their original query
256
				$sifLink = rtrim($this->Link(), '/') . '?q=' . $queryText . '&is=1';
257
				$data['SearchInsteadForLink'] = $sifLink;
258
				$paginated = $es->search($es->getSuggestedQuery(), $fieldsToSearch);
259
260
			}
261
262
			// calculate time
263
			$endTime = microtime(true);
264
			$elapsed = round(100 * ($endTime - $startTime)) / 100;
265
266
			// store variables for the template to use
267
			$data['ElapsedTime'] = $elapsed;
268
			$this->Aggregations = $es->getAggregations();
269
			$data['SearchResults'] = $paginated;
270
			$data['SearchPerformed'] = true;
271
			$data['NumberOfResults'] = $paginated->getTotalItems();
272
273
		} catch (Elastica\Exception\Connection\HttpException $e) {
274
			$data['ErrorMessage'] = 'Unable to connect to search server';
275
			$data['SearchPerformed'] = false;
276
		}
277
278
		$data['OriginalQuery'] = $queryText;
279
		$data['IgnoreSuggestions'] = $ignoreSuggestions;
280
281
		if($this->has_extension('PageControllerTemplateOverrideExtension')) {
282
			return $this->useTemplateOverride($data);
283
		} else {
284
			return $data;
285
		}
286
	}
287
288
289
290
	/*
291
	Return true if the query is not empty
292
	 */
293
	public function QueryIsEmpty() {
294
		return empty($this->request->getVar('q'));
295
	}
296
297
298
	/**
299
	 * Process submission of the search form, redirecting to a URL that will render search results
300
	 * @param  array $data form data
301
	 * @param  Form $form form
302
	 */
303
	public function submit($data, $form) {
304
		$queryText = $data['q'];
305
		$url = $this->Link();
306
		$url = rtrim($url, '/');
307
		$link = rtrim($url, '/') . '?q=' . $queryText . '&sfid=' . $data['identifier'];
308
		$this->redirect($link);
309
	}
310
311
	/*
312
	Obtain an instance of the form
313
	*/
314
315
	public function SearchForm() {
316
		$form = new ElasticSearchForm($this, 'SearchForm');
317
		$fields = $form->Fields();
318
		$ep = Controller::curr()->dataRecord;
319
		$identifierField = new HiddenField('identifier');
320
		$identifierField->setValue($ep->Identifier);
321
		$fields->push($identifierField);
322
		$queryField = $fields->fieldByName('q');
323
324
		 if(isset($_GET['q']) && isset($_GET['sfid'])) {
325
			if($_GET['sfid'] == $ep->Identifier) {
326
				$queryField->setValue($_GET['q']);
327
			}
328
329
		}
330
331
		if($this->action == 'similar') {
332
			$queryField->setDisabled(true);
333
			$actions = $form->Actions();
334
			foreach($actions as $field) {
335
				$field->setDisabled(true);
336
			}
337
		}
338
339
		/*
340
		A field needs to be chosen for autocompletion, if not no autocomplete
341
		 */
342
		if($this->AutoCompleteFieldID > 0) {
343
			$queryField->setAttribute('data-autocomplete', 'true');
344
			$queryField->setAttribute('data-autocomplete-field', 'Title');
345
			$queryField->setAttribute('data-autocomplete-classes', $this->ClassesToSearch);
346
			$queryField->setAttribute('data-autocomplete-sitetree', $this->SiteTreeOnly);
347
			$queryField->setAttribute('data-autocomplete-source', $this->Link());
348
			$queryField->setAttribute('data-autocomplete-function',
349
			$this->AutocompleteFunction()->Slug);
350
		}
351
352
		return $form;
353
	}
354
355
}
356