Completed
Push — dev2 ( ef4631...672549 )
by Gordon
03:31
created

ElasticSearchPage_Controller::calculateTime()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 4

Duplication

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