Completed
Push — dev2 ( 17a3b8...48fafe )
by Gordon
03:14
created

setMoreLikeThisParamsFromRequest()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 9

Duplication

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