ElasticSearchPage::onAfterWrite()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 57
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20
Metric Value
dl 0
loc 57
ccs 0
cts 33
cp 0
rs 9.031
cc 4
eloc 30
nc 4
nop 0
crap 20

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
* Only show a page with login when not logged in
4
*/
5
use Elastica\Document;
6
use Elastica\Query;
7
use \SilverStripe\Elastica\ResultList;
8
use Elastica\Query\QueryString;
9
use Elastica\Aggregation\Filter;
10
use Elastica\Filter\Term;
11
use Elastica\Filter\BoolAnd;
12
use Elastica\Aggregation\Terms;
13
use Elastica\Query\Filtered;
14
use Elastica\Query\Range;
15
use \SilverStripe\Elastica\ElasticSearcher;
16
use \SilverStripe\Elastica\Searchable;
17
use \SilverStripe\Elastica\QueryGenerator;
18
use \SilverStripe\Elastica\ElasticaUtil;
19
20
//FIXME namespace
21
22
23
class ElasticSearchPage extends Page {
24
	private static $defaults = array(
25
		'ShowInMenus' => 0,
26
		'ShowInSearch' => 0,
27
		'ClassesToSearch' => '',
28
		'ResultsPerPage' => 10,
29
		'SiteTreeOnly' => true,
30
		'MinTermFreq' => 2,
31
		'MaxTermFreq' => 25,
32
		'MinWordLength' => 3,
33
		'MinDocFreq' => 2,
34
		'MaxDocFreq' => 0,
35
		'MinWordLength' => 0,
36
		'MaxWordLength' => 0,
37
		'MinShouldMatch' => '30%'
38
	);
39
40
	private static $db = array(
41
		'ClassesToSearch' => 'Text',
42
		// unique identifier used to find correct search page for results
43
		// e.g. a separate search page for blog, pictures etc
44
		'Identifier' => 'Varchar',
45
		'ResultsPerPage' => 'Int',
46
		'SearchHelper' => 'Varchar',
47
		'SiteTreeOnly' => 'Boolean',
48
		'ContentForEmptySearch' => 'HTMLText',
49
		'MinTermFreq' => 'Int',
50
		'MaxTermFreq' => 'Int',
51
		'MinWordLength' => 'Int',
52
		'MinDocFreq' => 'Int',
53
		'MaxDocFreq' => 'Int',
54
		'MinWordLength' => 'Int',
55
		'MaxWordLength' => 'Int',
56
		'MinShouldMatch' => 'Varchar',
57
		'SimilarityStopWords' => 'Text'
58
	);
59
60
	private static $many_many = array(
61
		'ElasticaSearchableFields' => 'SearchableField'
62
	);
63
64
	private static $many_many_extraFields = array(
65
		'ElasticaSearchableFields' => array(
66
		'Searchable' => 'Boolean', // allows the option of turning off a single field for searching
67
		'SimilarSearchable' => 'Boolean', // allows field to be used in more like this queries.
68
		'Active' => 'Boolean', // preserve previous edits of weighting when classes changed
69
		'EnableAutocomplete' => 'Boolean', // whether or not to show autocomplete search for this field
70
		'Weight' => 'Int' // Weight to apply to this field in a search
71
		)
72
  	);
73
74
75
  	private static $has_one = array(
76
  		'AutoCompleteFunction' => 'AutoCompleteOption',
77
  		'AutoCompleteField' => 'SearchableField'
78
  	);
79
80
81
	/*
82
	Add a tab with details of what to search
83
	 */
84
	public function getCMSFields() {
85
		Requirements::javascript('elastica/javascript/elasticaedit.js');
86
		$fields = parent::getCMSFields();
87
88
89
		$fields->addFieldToTab("Root", new TabSet('Search',
90
			new Tab('SearchFor'),
91
			new Tab('Fields'),
92
			new Tab('AutoComplete'),
93
			new Tab('Aggregations'),
94
			new Tab('Similarity')
95
		));
96
97
98
		// ---- similarity tab ----
99
		$html = '<button class="ui-button-text-alternate ui-button-text"
100
		id="MoreLikeThisDefaultsButton"
101
		style="display: block;float: right;">Restore Defaults</button>';
102
		$defaultsButton = new LiteralField('DefaultsButton', $html);
103
				$fields->addFieldToTab("Root.Search.Similarity", $defaultsButton);
104
		$sortedWords = $this->SimilarityStopWords;
105
106
		$stopwordsField = StringTagField::create(
107
			'SimilarityStopWords',
108
			'Stop Words for Similar Search',
109
			explode(',', $sortedWords),
110
			$sortedWords
111
		);
112
113
		$stopwordsField->setShouldLazyLoad(true); // tags should be lazy loaded
114
115
		$fields->addFieldToTab("Root.Search.Similarity", $stopwordsField);
116
117
		$lf = new LiteralField('SimilarityNotes', _t('Elastica.SIMILARITY_NOTES',
118
			'Default values are those used by Elastica'));
119
		$fields->addFieldToTab("Root.Search.Similarity", $lf);
120
		$fields->addFieldToTab("Root.Search.Similarity", new TextField('MinTermFreq',
121
			'The minimum term frequency below which the terms will be ignored from the input ' .
122
			'document. Defaults to 2.'));
123
		$fields->addFieldToTab("Root.Search.Similarity", new TextField('MaxTermFreq',
124
			'The maximum number of query terms that will be selected. Increasing this value gives ' .
125
			'greater accuracy at the expense of query execution speed. Defaults to 25.'));
126
		$fields->addFieldToTab("Root.Search.Similarity", new TextField('MinWordLength',
127
			'The minimum word length below which the terms will be ignored.  Defaults to 0.'));
128
		$fields->addFieldToTab("Root.Search.Similarity", new TextField('MinDocFreq',
129
			'The minimum document frequency below which the terms will be ignored from the input ' .
130
			'document. Defaults to 5.'));
131
		$fields->addFieldToTab("Root.Search.Similarity", new TextField('MaxDocFreq',
132
			'The maximum document frequency above which the terms will be ignored from the input ' .
133
			'document. This could be useful in order to ignore highly frequent words such as stop ' .
134
			'words. Defaults to unbounded (0).'));
135
		$fields->addFieldToTab("Root.Search.Similarity", new TextField('MinWordLength',
136
			'The minimum word length below which the terms will be ignored. The old name min_' .
137
			'word_len is deprecated. Defaults to 0.'));
138
		$fields->addFieldToTab("Root.Search.Similarity", new TextField('MaxWordLength',
139
			'The maximum word length above which the terms will be ignored. The old name max_word_' .
140
			'len is deprecated. Defaults to unbounded (0).'));
141
		$fields->addFieldToTab("Root.Search.Similarity", new TextField('MinShouldMatch',
142
			'This parameter controls the number of terms that must match. This can be either a ' .
143
			'number or a percentage.  See ' .
144
			'https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-minimum-should-match.html'));
145
146
		// ---- search details tab ----
147
		$identifierField = new TextField('Identifier',
148
			'Identifier to allow this page to be found in form templates');
149
		$fields->addFieldToTab('Root.Main', $identifierField, 'Content');
150
		$fields->addFieldToTab('Root.Search.SearchFor', new CheckboxField('SiteTreeOnly', 'Show search results for all SiteTree objects only'));
151
152
		$sql = "SELECT DISTINCT ClassName from SiteTree_Live UNION "
153
			 . "SELECT DISTINCT ClassName from SiteTree "
154
			 . "WHERE ClassName != 'ErrorPage'"
155
			 . "ORDER BY ClassName"
156
		;
157
158
		$classes = array();
159
		$records = DB::query($sql);
160
		foreach($records as $record) {
161
			array_push($classes, $record['ClassName']);
162
		}
163
		$list = implode(',', $classes);
164
165
		$clazzes = '';
166
		$clazzes = $this->ClassesToSearch;
167
		$allSearchableClasses = SearchableClass::get()->sort('Name')->map('Name')->toArray();
168
		$classesToSearchField = StringTagField::create(
169
			'ClassesToSearch',
170
			'Choose which SilverStripe classes to search',
171
			$allSearchableClasses,
172
			$clazzes
173
		);
174
175
		$fields->addFieldToTab('Root.Search.SearchFor', $classesToSearchField);
176
177
178
		$html = '<div class="field text" id="SiteTreeOnlyInfo">';
179
		$html .= "<p>Copy the following into the above field to ensure that all SiteTree classes are searched</p>";
180
		$html .= '<p class="message">' . $list;
181
		$html .= "</p></div>";
182
		$infoField = new LiteralField('InfoField', $html);
183
		$fields->addFieldToTab('Root.Search.SearchFor', $infoField);
184
185
		$fields->addFieldToTab('Root.Main', new HTMLEditorField('ContentForEmptySearch'));
186
187
188
		$fields->addFieldToTab('Root.Search.SearchFor', new NumericField('ResultsPerPage',
189
											'The number of results to return on a page'));
190
		$fields->addFieldToTab('Root.Search.Aggregations', new TextField('SearchHelper',
191
			'ClassName of object to manipulate search details and results.  Leave blank for standard search'));
192
193
		$ottos = AutoCompleteOption::get()->Filter('Locale', $this->Locale)->map('ID', 'Name')->
194
											toArray();
195
		$df = DropdownField::create('AutoCompleteFunctionID', 'Autocomplete Function')->
196
									setSource($ottos);
197
		$df->setEmptyString('-- Please select what do do after find as you type has occurred --');
198
199
		$ottos = $this->ElasticaSearchableFields()->filter('EnableAutocomplete', 1)->Map('ID', 'Name')->toArray();
200
		$autoCompleteFieldDF = DropDownField::create('AutoCompleteFieldID', 'Field to use for autocomplete')->setSource($ottos);
201
		$autoCompleteFieldDF->setEmptyString('-- Please select which field to use for autocomplete --');
202
203
		$fields->addFieldToTab("Root.Search.AutoComplete",
204
		  		FieldGroup::create(
205
		  			$autoCompleteFieldDF,
206
		 			$df
207
		 		)->setTitle('Autocomplete')
208
		 );
209
210
		// ---- grid of searchable fields ----
211
		$html = '<p id="SearchFieldIntro">' . _t('SiteConfig.ELASTICA_SEARCH_INFO',
212
				"Select a field to edit it's properties") . '</p>';
213
		$fields->addFieldToTab('Root.Search.Fields', $h1 = new LiteralField('SearchInfo', $html));
214
		$searchPicker = new PickerField('ElasticaSearchableFields', 'Searchable Fields',
215
			$this->ElasticaSearchableFields()->filter('Active', 1)->sort('Name'));
216
217
		$fields->addFieldToTab('Root.Search.Fields', $searchPicker);
218
219
		$pickerConfig = $searchPicker->getConfig();
220
221
		$pickerConfig->removeComponentsByType(new GridFieldAddNewButton());
222
		$pickerConfig->removeComponentsByType(new GridFieldDeleteAction());
223
		$pickerConfig->removeComponentsByType(new PickerFieldAddExistingSearchButton());
224
		$pickerConfig->getComponentByType('GridFieldPaginator')->setItemsPerPage(100);
225
226
		$searchPicker->enableEdit();
227
		$edittest = $pickerConfig->getComponentByType('GridFieldDetailForm');
228
		$edittest->setFields(FieldList::create(
229
			TextField::create('Name', 'Field Name'),
230
			TextField::create('ClazzName', 'Class'),
231
			HiddenField::create('Autocomplete', 'This can be autocompleted'),
232
			CheckboxField::create('ManyMany[Searchable]', 'Use for normal searching'),
233
			CheckboxField::create('ManyMany[SimilarSearchable]', 'Use for similar search'),
234
			NumericField::create('ManyMany[Weight]', 'Weighting'),
235
			CheckboxField::create('ShowHighlights', 'Show highlights from search in results for this field')
236
		));
237
238
		$edittest->setItemEditFormCallback(function($form) {
239
			$fields = $form->Fields();
240
			$fields->dataFieldByName('ClazzName')->setReadOnly(true);
241
			$fields->dataFieldByName('ClazzName')->setDisabled(true);
242
			$fields->dataFieldByName('Name')->setReadOnly(true);
243
			$fields->dataFieldByName('Name')->setDisabled(true);
244
		});
245
246
247
		// What do display on the grid of searchable fields
248
		$dataColumns = $pickerConfig->getComponentByType('GridFieldDataColumns');
249
		$dataColumns->setDisplayFields(array(
250
			'Name' => 'Name',
251
			'ClazzName' => 'Class',
252
			'Type' => 'Type',
253
			'Searchable' => 'Use for Search?',
254
			'SimilarSearchable' => 'Use for Similar Search?',
255
			'ShowHighlights' => 'Show Search Highlights',
256
			'Weight' => 'Weighting'
257
		));
258
259
		return $fields;
260
	}
261
262
263
	public function getCMSValidator() {
264
		return new ElasticSearchPage_Validator();
265
	}
266
267
268
	/**
269
	 * Avoid duplicate identifiers, and check that ClassesToSearch actually exist and are Searchable
270
	 * @return DataObject result with or without error
271
	 */
272
	public function validate() {
273
		$result = parent::validate();
274
		$mode = Versioned::get_reading_mode();
275
		$suffix = '';
276
		if($mode == 'Stage.Live') {
277
			$suffix = '_Live';
278
		}
279
280
		if(!$this->Identifier) {
281
			$result->error('The identifier cannot be blank');
282
		}
283
284
		$where = 'ElasticSearchPage' . $suffix . '.ID != ' . $this->ID . " AND `Identifier` = '{$this->Identifier}'";
285
		$existing = ElasticSearchPage::get()->where($where)->count();
286
		if($existing > 0) {
287
			$result->error('The identifier ' . $this->Identifier . ' already exists');
288
		}
289
290
		// now check classes to search actually exist, assuming in site tree not set
291
		if(!$this->SiteTreeOnly) {
292
			if($this->ClassesToSearch == '') {
293
				$result->error('At least one searchable class must be available, or SiteTreeOnly flag set');
294
			} else {
295
				$toSearch = explode(',', $this->ClassesToSearch);
296
				foreach($toSearch as $clazz) {
297
					try {
298
						$instance = Injector::inst()->create($clazz);
299
						if(!$instance->hasExtension('SilverStripe\Elastica\Searchable')) {
300
							$result->error('The class ' . $clazz . ' must have the Searchable extension');
301
						}
302
					} catch (ReflectionException $e) {
303
						$result->error('The class ' . $clazz . ' does not exist');
304
					}
305
				}
306
			}
307
		}
308
309
310
		foreach($this->ElasticaSearchableFields() as $esf) {
311
			if($esf->Weight == 0) {
312
				$result->error("The field {$esf->ClazzName}.{$esf->Name} has a zero weight. ");
313
			} else if($esf->Weight < 0) {
314
				$result->error("The field {$esf->ClazzName}.{$esf->Name} has a negative weight. ");
315
			}
316
		}
317
318
		return $result;
319
	}
320
321
322
	public function onAfterWrite() {
323
		// FIXME - move to a separate testable method and call at build time also
324
		$nameToMapping = QueryGenerator::getSearchFieldsMappingForClasses($this->ClassesToSearch);
325
		$names = array_keys($nameToMapping);
326
327
		#FIXME -  SiteTree only
328
		$relevantClasses = $this->ClassesToSearch; // due to validation this will be valid
329
		if($this->SiteTreeOnly) {
330
			$relevantClasses = SearchableClass::get()->filter('InSiteTree', true)->Map('Name')->toArray();
331
332
		}
333
		$quotedClasses = QueryGenerator::convertToQuotedCSV($relevantClasses);
334
		$quotedNames = QueryGenerator::convertToQuotedCSV($names);
335
336
		$where = "Name in ($quotedNames) AND ClazzName IN ($quotedClasses)";
337
338
		// Get the searchfields for the ClassNames searched
339
		$sfs = SearchableField::get()->where($where);
340
341
342
		// Get the searchable fields associated with this search page
343
		$esfs = $this->ElasticaSearchableFields();
344
345
		// Remove existing searchable fields for this page from the list of all available
346
		$delta = array_keys($esfs->map()->toArray());
347
		$newSearchableFields = $sfs->exclude('ID', $delta);
348
349
		if($newSearchableFields->count() > 0) {
350
			foreach($newSearchableFields->getIterator() as $newSearchableField) {
351
				$newSearchableField->Active = true;
352
				$newSearchableField->Weight = 1;
353
354
				$esfs->add($newSearchableField);
355
356
				// Note 1 used instead of true for SQLite3 testing compatibility
357
				$sql = "UPDATE ElasticSearchPage_ElasticaSearchableFields SET ";
358
				$sql .= 'Active=1, Weight=1 WHERE ElasticSearchPageID = ' . $this->ID;
359
				DB::query($sql);
360
			}
361
		}
362
363
364
365
		// Mark all the fields for this page as inactive initially
366
		$sql = "UPDATE ElasticSearchPage_ElasticaSearchableFields SET ACTIVE=0 WHERE ";
367
		$sql .= "ElasticSearchPageID={$this->ID}";
368
		DB::query($sql);
369
370
		$activeIDs = array_keys($sfs->map()->toArray());
371
		$activeIDs = implode(',', $activeIDs);
372
373
		//Mark as active the relevant ones
374
		$sql = "UPDATE ElasticSearchPage_ElasticaSearchableFields SET ACTIVE=1 WHERE ";
375
		$sql .= "ElasticSearchPageID={$this->ID} AND SearchableFieldID IN (";
376
		$sql .= "$activeIDs)";
377
		DB::query($sql);
378
	}
379
380
381
	/*
382
	Obtain an instance of the form - this is need for rendering the search box in the header
383
	*/
384
	public function SearchForm($buttonTextOverride = null) {
385
		$result = new ElasticSearchForm($this, 'SearchForm');
386
		$fields = $result->Fields();
387
		$identifierField = new HiddenField('identifier');
388
		$identifierField->setValue($this->Identifier);
389
		$fields->push($identifierField);
390
		$qField = $fields->fieldByName('q');
391
392
393
		if($buttonTextOverride) {
394
			$result->setButtonText($buttonTextOverride);
395
		}
396
397
		if($this->AutoCompleteFieldID > 0) {
398
			ElasticaUtil::addAutocompleteToQueryField(
399
				$qField,
400
				$this->ClassesToSearch,
401
				$this->SiteTreeOnly,
402
				$this->Link(),
403
				$this->AutocompleteFunction()->Slug
404
			);
405
		}
406
		return $result;
407
	}
408
409
410
	/*
411
	If a manipulator object is set, assume aggregations are present.  Used to add the column
412
	for aggregates
413
	 */
414
	public function HasAggregations() {
415
		return $this->SearchHelper != null;
416
	}
417
418
}
419