Completed
Push — dev2 ( 1d8072...a77d0b )
by Gordon
03:05
created

checkForSimulatedServerDown()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

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