Completed
Push — dev2 ( 69ecf2...fdc527 )
by Gordon
03:22
created

Searchable::addIndexedFields()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 35
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 35
rs 8.439
cc 5
eloc 24
nc 4
nop 2
1
<?php
2
3
namespace SilverStripe\Elastica;
4
5
use Elastica\Document;
6
use Elastica\Type\Mapping;
7
use ShortcodeParser;
8
9
/**
10
 * Adds elastic search integration to a data object.
11
 */
12
class Searchable extends \DataExtension {
13
14
	/**
15
	 * Counter used to display progress of indexing
16
	 * @var integer
17
	 */
18
	public static $index_ctr = 0;
19
20
	/**
21
	 * Everytime progressInterval divides $index_ctr exactly display progress
22
	 * @var integer
23
	 */
24
	private static $progressInterval = 0;
25
26
	public static $mappings = array(
27
		'Boolean'     => 'boolean',
28
		'Decimal'     => 'double',
29
		'Currency'    => 'double',
30
		'Double'      => 'double',
31
		'Enum'        => 'string',
32
		'Float'       => 'float',
33
		'HTMLText'    => 'string',
34
		'HTMLVarchar' => 'string',
35
		'Int'         => 'integer',
36
		'Text'        => 'string',
37
		'VarChar'     => 'string',
38
		'Varchar'     => 'string',
39
		'Year'        => 'integer',
40
		'Percentage'  => 'double',
41
		'Time'  => 'date',
42
43
		// The 2 different date types will be stored with different formats
44
		'Date'        => 'date',
45
		'SS_Datetime' => 'date',
46
		'Datetime' => 'date',
47
		'DBLocale'    => 'string'
48
	);
49
50
51
	/**
52
	 * @var ElasticaService associated elastica search service
53
	 */
54
	protected $service;
55
56
57
	/**
58
	 * Array of fields that need HTML parsed
59
	 * @var array
60
	 */
61
	protected $html_fields = array();
62
63
	/**
64
	 * Store a mapping of relationship name to result type
65
	 */
66
	protected $relationship_methods = array();
67
68
69
	/**
70
	 * If importing a large number of items from a fixtures file, or indeed some other source, then
71
	 * it is quicker to set a flag of value IndexingOff => false.  This has the effect of ensuring
72
	 * no indexing happens, a request is normally made per fixture when loading.  One can then run
73
	 * the reindexing teask to bulk index in one HTTP POST request to Elasticsearch
74
	 *
75
	 * @var boolean
76
	 */
77
	private static $IndexingOff = false;
78
79
80
	/**
81
	 * @see getElasticaResult
82
	 * @var \Elastica\Result
83
	 */
84
	protected $elastica_result;
85
86
	public function __construct(ElasticaService $service) {
87
		$this->service = $service;
88
		parent::__construct();
89
	}
90
91
92
	/**
93
	 * Get the elasticsearch type name
94
	 *
95
	 * @return string
96
	 */
97
	public function getElasticaType() {
98
		return get_class($this->owner);
99
	}
100
101
102
	/**
103
	 * If the owner is part of a search result
104
	 * the raw Elastica search result is returned
105
	 * if set via setElasticaResult
106
	 *
107
	 * @return \Elastica\Result
108
	 */
109
	public function getElasticaResult() {
110
		return $this->elastica_result;
111
	}
112
113
114
	/**
115
	 * Set the raw Elastica search result
116
	 *
117
	 * @param \Elastica\Result
118
	 */
119
	public function setElasticaResult(\Elastica\Result $result) {
120
		$this->elastica_result = $result;
121
	}
122
123
124
	/**
125
	 * Gets an array of elastic field definitions.
126
	 *
127
	 * @return array
128
	 */
129
	public function getElasticaFields($storeMethodName = false, $recurse = true) {
130
		$db = $this->owner->db();
131
		$fields = $this->getAllSearchableFields();
132
		$result = array();
133
134
		foreach($fields as $name => $params) {
135
			$spec = array();
136
			$name = str_replace('()', '', $name);
137
138
			if(array_key_exists($name, $db)) {
139
				$class = $db[$name];
140
				$this->assignSpecForStandardFieldType($name, $class, $spec);
141
			} else {
142
				// field name is not in the db, it could be a method
143
				$has_lists = $this->getListRelationshipMethods();
144
				$has_ones = $this->owner->has_one();
145
146
				// check has_many and many_many relations
147
				if(isset($has_lists[$name])) {
148
					// the classes returned by the list method
149
					$resultType = $has_lists[$name];
150
					$this->assignSpecForManyRelationship($name, $resultType, $spec, $storeMethodName, $recurse);
151
				} else if(isset($has_ones[$name])) {
152
					$resultType = $has_ones[$name];
153
					$this->assignSpecForHasOne($name, $resultType, $spec, $storeMethodName, $recurse);
154
				}
155
				// otherwise fall back to string - Enum is one such category
156
				else {
157
					$spec["type"] = "string";
158
				}
159
			}
160
161
			$this->addIndexedFields($name, $spec);
162
163
			$result[$name] = $spec;
164
		}
165
166
		if($this->owner->hasMethod('updateElasticHTMLFields')) {
167
			$this->html_fields = $this->owner->updateElasticHTMLFields($this->html_fields);
168
		}
169
170
		return $result;
171
	}
172
173
174
175
	private function addIndexedFields($name, &$spec) {
176
		// in the case of a relationship type will not be set
177
			if(isset($spec['type'])) {
178
				if($spec['type'] == 'string') {
179
					$unstemmed = array();
180
					$unstemmed['type'] = "string";
181
					$unstemmed['analyzer'] = "unstemmed";
182
					$unstemmed['term_vector'] = "yes";
183
					$extraFields = array('standard' => $unstemmed);
184
185
					$shingles = array();
186
					$shingles['type'] = "string";
187
					$shingles['analyzer'] = "shingles";
188
					$shingles['term_vector'] = "yes";
189
					$extraFields['shingles'] = $shingles;
190
191
					//Add autocomplete field if so required
192
					$autocomplete = \Config::inst()->get($this->owner->ClassName, 'searchable_autocomplete');
193
194
					if(isset($autocomplete) && in_array($name, $autocomplete)) {
195
						$autocompleteField = array();
196
						$autocompleteField['type'] = "string";
197
						$autocompleteField['index_analyzer'] = "autocomplete_index_analyzer";
198
						$autocompleteField['search_analyzer'] = "autocomplete_search_analyzer";
199
						$autocompleteField['term_vector'] = "yes";
200
						$extraFields['autocomplete'] = $autocompleteField;
201
					}
202
203
					$spec['fields'] = $extraFields;
204
					// FIXME - make index/locale specific, get from settings
205
					$spec['analyzer'] = 'stemmed';
206
					$spec['term_vector'] = "yes";
207
				}
208
			}
209
	}
210
211
212 View Code Duplication
	private function assignSpecForHasOne($name, $resultType, &$spec, $storeMethodName, $recurse) {
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
213
		$resultTypeInstance = \Injector::inst()->create($resultType);
214
215
		$resultTypeMapping = array();
216
217
		// get the fields for the result type, but do not recurse
218
		if($recurse) {
219
			$resultTypeMapping = $resultTypeInstance->getElasticaFields($storeMethodName, false);
220
		}
221
222
		$resultTypeMapping['ID'] = array('type' => 'integer');
223
224
		if($storeMethodName) {
225
			$resultTypeMapping['__method'] = $name;
226
		}
227
		$spec = array('properties' => $resultTypeMapping);
228
		// we now change the name to the result type, not the method name
229
		$name = $resultType;
0 ignored issues
show
Unused Code introduced by
$name is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
230
	}
231
232
233 View Code Duplication
	private function assignSpecForManyRelationship($name, $resultType, &$spec, $storeMethodName, $recurse) {
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
234
		$resultTypeInstance = \Injector::inst()->create($resultType);
235
		$resultTypeMapping = array();
236
237
		// get the fields for the result type, but do not recurse
238
		if($recurse) {
239
			$resultTypeMapping = $resultTypeInstance->getElasticaFields($storeMethodName, false);
240
		}
241
242
		$resultTypeMapping['ID'] = array('type' => 'integer');
243
244
		if($storeMethodName) {
245
			$resultTypeMapping['__method'] = $name;
246
		}
247
248
		$spec = array('properties' => $resultTypeMapping);
249
250
251
		// we now change the name to the result type, not the method name
252
		$name = $resultType;
0 ignored issues
show
Unused Code introduced by
$name is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
253
	}
254
255
256
	private function assignSpecForStandardFieldType($name, $class, &$spec) {
257
		if(($pos = strpos($class, '('))) {
258
			// Valid in the case of Varchar(255)
259
			$class = substr($class, 0, $pos);
260
		}
261
262
		if(array_key_exists($class, self::$mappings)) {
263
			$spec['type'] = self::$mappings[$class];
264
			if($spec['type'] === 'date') {
265
				if($class == 'Date') {
266
					$spec['format'] = 'y-M-d';
267
				} elseif($class == 'SS_Datetime') {
268
					$spec['format'] = 'y-M-d H:m:s';
269
				} elseif($class == 'Datetime') {
270
					$spec['format'] = 'y-M-d H:m:s';
271
				} elseif($class == 'Time') {
272
					$spec['format'] = 'H:m:s';
273
				}
274
			}
275
			if($class === 'HTMLText' || $class === 'HTMLVarchar') {
276
				array_push($this->html_fields, $name);
277
			}
278
		}
279
		// no need for an extra case here as all SS types checked in tests
280
	}
281
282
283
	/**
284
	 * Get the elasticsearch mapping for the current document/type
285
	 *
286
	 * @return \Elastica\Type\Mapping
287
	 */
288
	public function getElasticaMapping() {
289
		$mapping = new Mapping();
290
291
		$fields = $this->getElasticaFields(false);
292
293
		$localeMapping = array();
294
295
		if($this->owner->hasField('Locale')) {
296
			$localeMapping['type'] = 'string';
297
			// we wish the locale to be stored as is
298
			$localeMapping['index'] = 'not_analyzed';
299
			$fields['Locale'] = $localeMapping;
300
		}
301
302
		// ADD CUSTOM FIELDS HERE THAT ARE INDEXED BY DEFAULT
303
		// add a mapping to flag whether or not class is in SiteTree
304
		$fields['IsInSiteTree'] = array('type'=>'boolean');
305
		$fields['Link'] = array('type' => 'string', 'index' => 'not_analyzed');
306
307
		$mapping->setProperties($fields);
308
309
		//This concatenates all the fields together into a single field.
310
		//Initially added for suggestions compatibility, in that searching
311
		//_all field picks up all possible suggestions
312
		$mapping->enableAllField();
313
314
		if($this->owner->hasMethod('updateElasticsearchMapping')) {
315
			$mapping = $this->owner->updateElasticsearchMapping($mapping);
316
		}
317
		return $mapping;
318
	}
319
320
321
	/**
322
	 * Get an elasticsearch document
323
	 *
324
	 * @return \Elastica\Document
325
	 */
326
	public function getElasticaDocument() {
327
		self::$index_ctr++;
328
		$fields = $this->getFieldValuesAsArray();
329
		$progress = \Controller::curr()->request->getVar('progress');
330
		if(!empty($progress)) {
331
			self::$progressInterval = (int)$progress;
332
		}
333
334
		if(self::$progressInterval > 0) {
335
			if(self::$index_ctr % self::$progressInterval === 0) {
336
				ElasticaUtil::message("\t" . $this->owner->ClassName . " - Prepared " . self::$index_ctr . " for indexing...");
337
			}
338
		}
339
340
		// Optionally update the document
341
		$document = new Document($this->owner->ID, $fields);
342
		if($this->owner->hasMethod('updateElasticsearchDocument')) {
343
			$document = $this->owner->updateElasticsearchDocument($document);
344
		}
345
346
		// Check if the current classname is part of the site tree or not
347
		// Results are cached to save reprocessing the same
348
		$classname = $this->owner->ClassName;
349
		$inSiteTree = $this->isInSiteTree($classname);
350
351
		$document->set('IsInSiteTree', $inSiteTree);
352
353
		if($inSiteTree) {
354
			$document->set('Link', $this->owner->AbsoluteLink());
355
		}
356
357
		if(isset($this->owner->Locale)) {
358
			$document->set('Locale', $this->owner->Locale);
359
		}
360
361
		return $document;
362
	}
363
364
365
	public function getFieldValuesAsArray($recurse = true) {
366
		$fields = array();
367
		$has_ones = $this->owner->has_one();
368
369
		foreach($this->getElasticaFields($recurse) as $field => $config) {
370
			if(null === $this->owner->$field && is_callable(get_class($this->owner) . "::" . $field)) {
371
				// call a method to get a field value
372
				if(in_array($field, $this->html_fields)) {
373
					// Parse short codes in HTML, and then convert to text
374
					$fields[$field] = $this->owner->$field;
375
					$html = ShortcodeParser::get_active()->parse($this->owner->$field());
376
					$txt = \Convert::html2raw($html);
377
					$fields[$field] = $txt;
378
				} else {
379
					// Plain text
380
					$fields[$field] = $this->owner->$field();
381
				}
382
383
			} else {
384
				if(in_array($field, $this->html_fields)) {
385
					$fields[$field] = $this->owner->$field;
386
					if(gettype($this->owner->$field) !== 'NULL') {
387
						$html = ShortcodeParser::get_active()->parse($this->owner->$field);
388
						$txt = \Convert::html2raw($html);
389
						$fields[$field] = $txt;
390
					}
391
				} else {
392
					if(isset($config['properties']['__method'])) {
393
						$methodName = $config['properties']['__method'];
394
						$data = $this->owner->$methodName();
395
						$relArray = array();
396
397
						// get the fields of a has_one relational object
398
						if(isset($has_ones[$methodName])) {
399
							if($data->ID > 0) {
400
								$item = $data->getFieldValuesAsArray(false);
401
								$relArray = $item;
402
							}
403
404
						// get the fields for a has_many or many_many relational list
405
						} else {
406
							foreach($data->getIterator() as $item) {
407
								if($recurse) {
408
									// populate the subitem but do not recurse any further if more relationships
409
									$itemDoc = $item->getFieldValuesAsArray(false);
410
									array_push($relArray, $itemDoc);
411
								}
412
							}
413
						}
414
						// save the relation as an array (for now)
415
						$fields[$methodName] = $relArray;
416
					} else {
417
						$fields[$field] = $this->owner->$field;
418
					}
419
420
				}
421
422
			}
423
		}
424
425
		return $fields;
426
	}
427
428
429
	/**
430
	 * Returns whether to include the document into the search index.
431
	 * All documents are added unless they have a field "ShowInSearch" which is set to false
432
	 *
433
	 * @return boolean
434
	 */
435
	public function showRecordInSearch() {
436
		return !($this->owner->hasField('ShowInSearch') && false == $this->owner->ShowInSearch);
437
	}
438
439
440
	/**
441
	 * Delete the record from the search index if ShowInSearch is deactivated (non-SiteTree).
442
	 */
443
	public function onBeforeWrite() {
444
		if(($this->owner instanceof \SiteTree)) {
445
			if($this->owner->hasField('ShowInSearch') &&
446
				$this->owner->isChanged('ShowInSearch', 2) && false == $this->owner->ShowInSearch) {
447
				$this->doDeleteDocument();
448
			}
449
		}
450
	}
451
452
453
	/**
454
	 * Delete the record from the search index if ShowInSearch is deactivated (SiteTree).
455
	 */
456
	public function onBeforePublish() {
457
		if(false == $this->owner->ShowInSearch) {
458
			if($this->owner->isPublished()) {
459
				$liveRecord = \Versioned::get_by_stage(get_class($this->owner), 'Live')->
460
					byID($this->owner->ID);
461
				if($liveRecord->ShowInSearch != $this->owner->ShowInSearch) {
462
					$this->doDeleteDocument();
463
				}
464
			}
465
		}
466
	}
467
468
469
	/**
470
	 * Updates the record in the search index (non-SiteTree).
471
	 */
472
	public function onAfterWrite() {
473
		$this->doIndexDocument();
474
	}
475
476
477
	/**
478
	 * Updates the record in the search index (SiteTree).
479
	 */
480
	public function onAfterPublish() {
481
		$this->doIndexDocument();
482
	}
483
484
485
	/**
486
	 * Updates the record in the search index.
487
	 */
488
	protected function doIndexDocument() {
489
		if($this->showRecordInSearch()) {
490
			if(!$this->owner->IndexingOff) {
491
				$this->service->index($this->owner);
492
			}
493
		}
494
	}
495
496
497
	/**
498
	 * Removes the record from the search index (non-SiteTree).
499
	 */
500
	public function onAfterDelete() {
501
		$this->doDeleteDocumentIfInSearch();
502
	}
503
504
505
	/**
506
	 * Removes the record from the search index (non-SiteTree).
507
	 */
508
	public function onAfterUnpublish() {
509
		$this->doDeleteDocumentIfInSearch();
510
	}
511
512
513
	/**
514
	 * Removes the record from the search index if the "ShowInSearch" attribute is set to true.
515
	 */
516
	protected function doDeleteDocumentIfInSearch() {
517
		if($this->showRecordInSearch()) {
518
			$this->doDeleteDocument();
519
		}
520
	}
521
522
523
	/**
524
	 * Removes the record from the search index.
525
	 */
526
	protected function doDeleteDocument() {
527
		try {
528
			if(!$this->owner->IndexingOff) {
529
				// this goes to elastica service
530
				$this->service->remove($this->owner);
531
			}
532
		} catch (\Elastica\Exception\NotFoundException $e) {
533
			trigger_error("Deleted document " . $this->owner->ClassName . " (" . $this->owner->ID .
534
				") not found in search index.", E_USER_NOTICE);
535
		}
536
537
	}
538
539
540
	/**
541
	 * Return all of the searchable fields defined in $this->owner::$searchable_fields and all the parent classes.
542
	 *
543
	 * @param  $recuse Whether or not to traverse relationships. First time round yes, subsequently no
544
	 * @return array searchable fields
545
	 */
546
	public function getAllSearchableFields($recurse = true) {
547
		$fields = \Config::inst()->get(get_class($this->owner), 'searchable_fields');
548
549
		// fallback to default method
550
		if(!$fields) {
551
			user_error('The field $searchable_fields must be set for the class ' . $this->owner->ClassName);
552
		}
553
554
		// get the values of these fields
555
		$elasticaMapping = $this->fieldsToElasticaConfig($fields);
556
557
		if($recurse) {
558
			// now for the associated methods and their results
559
			$methodDescs = \Config::inst()->get(get_class($this->owner), 'searchable_relationships');
560
			$has_ones = $this->owner->has_one();
561
			$has_lists = $this->getListRelationshipMethods();
562
563
			if(isset($methodDescs) && is_array($methodDescs)) {
564
				foreach($methodDescs as $methodDesc) {
565
					// split before the brackets which can optionally list which fields to index
566
					$splits = explode('(', $methodDesc);
567
					$methodName = $splits[0];
568
569
					if(isset($has_lists[$methodName])) {
570
571
						$relClass = $has_lists[$methodName];
572
						$fields = \Config::inst()->get($relClass, 'searchable_fields');
573
						if(!$fields) {
574
							user_error('The field $searchable_fields must be set for the class ' . $relClass);
575
						}
576
						$rewrite = $this->fieldsToElasticaConfig($fields);
577
578
						// mark as a method, the resultant fields are correct
579
						$elasticaMapping[$methodName . '()'] = $rewrite;
580
					} else if(isset($has_ones[$methodName])) {
581
						$relClass = $has_ones[$methodName];
582
						$fields = \Config::inst()->get($relClass, 'searchable_fields');
583
						if(!$fields) {
584
							user_error('The field $searchable_fields must be set for the class ' . $relClass);
585
						}
586
						$rewrite = $this->fieldsToElasticaConfig($fields);
587
588
						// mark as a method, the resultant fields are correct
589
						$elasticaMapping[$methodName . '()'] = $rewrite;
590
					} else {
591
						user_error('The method ' . $methodName . ' not found in class ' . $this->owner->ClassName .
592
								', please check configuration');
593
					}
594
				}
595
			}
596
		}
597
598
		return $elasticaMapping;
599
	}
600
601
602
	/*
603
	Evaluate each field, e.g. 'Title', 'Member.Name'
604
	 */
605
	private function fieldsToElasticaConfig($fields) {
606
		// Copied from DataObject::searchableFields() as there is no separate accessible method
607
		$rewrite = array();
608
		foreach($fields as $name => $specOrName) {
609
			$identifer = (is_int($name)) ? $specOrName : $name;
610
			$rewrite[$identifer] = array();
611
			if(!isset($rewrite[$identifer]['title'])) {
612
				$rewrite[$identifer]['title'] = (isset($labels[$identifer]))
613
					? $labels[$identifer] : \FormField::name_to_label($identifer);
614
			}
615
			if(!isset($rewrite[$identifer]['filter'])) {
616
				$rewrite[$identifer]['filter'] = 'PartialMatchFilter';
617
			}
618
		}
619
620
		return $rewrite;
621
	}
622
623
624
	public function requireDefaultRecords() {
625
		parent::requireDefaultRecords();
626
627
		$searchableFields = $this->getElasticaFields(true, true);
628
629
630
		$doSC = \SearchableClass::get()->filter(array('Name' => $this->owner->ClassName))->first();
631
		if(!$doSC) {
632
			$doSC = new \SearchableClass();
633
			$doSC->Name = $this->owner->ClassName;
634
635
			$inSiteTree = $this->isInSiteTree($this->owner->ClassName);
636
			$doSC->InSiteTree = $inSiteTree;
637
638
			$doSC->write();
639
		}
640
641
		foreach($searchableFields as $name => $searchableField) {
642
			// check for existence of methods and if they exist use that as the name
643
			if(!isset($searchableField['type'])) {
644
				$name = $searchableField['properties']['__method'];
645
			}
646
647
			$filter = array('ClazzName' => $this->owner->ClassName, 'Name' => $name);
648
			$doSF = \SearchableField::get()->filter($filter)->first();
649
650
651
			if(!$doSF) {
652
				$doSF = new \SearchableField();
653
				$doSF->ClazzName = $this->owner->ClassName;
654
				$doSF->Name = $name;
655
656
				if(isset($searchableField['type'])) {
657
					$doSF->Type = $searchableField['type'];
658
				} else {
659
					$doSF->Name = $searchableField['properties']['__method'];
660
					$doSF->Type = 'relationship';
661
				}
662
				$doSF->SearchableClassID = $doSC->ID;
663
664
				if(isset($searchableField['fields']['autocomplete'])) {
665
					$doSF->Autocomplete = true;
666
				}
667
668
				$doSF->write();
669
				\DB::alteration_message("Created new searchable editable field " . $name, "changed");
670
			}
671
672
			// FIXME deal with deletions
673
		}
674
	}
675
676
677
	private function getListRelationshipMethods() {
678
		$has_manys = $this->owner->has_many();
679
		$many_manys = $this->owner->many_many();
680
681
		// array of method name to retuned object ClassName for relationships returning lists
682
		$has_lists = $has_manys;
683
		foreach(array_keys($many_manys) as $key) {
684
			$has_lists[$key] = $many_manys[$key];
685
		}
686
687
		return $has_lists;
688
	}
689
690
691
	private function isInSiteTree($classname) {
692
		$inSiteTree = ($classname === 'SiteTree' ? true : false);
693
		if(!$inSiteTree) {
694
			$class = new \ReflectionClass($this->owner->ClassName);
695
			while($class = $class->getParentClass()) {
696
				$parentClass = $class->getName();
697
				if($parentClass == 'SiteTree') {
698
					$inSiteTree = true;
699
					break;
700
				}
701
			}
702
		}
703
		return $inSiteTree;
704
	}
705
706
707
	/*
708
	Allow the option of overriding the default template with one of <ClassName>ElasticSearchResult
709
	 */
710
	public function RenderResult($linkToContainer = '') {
711
		$vars = new \ArrayData(array('SearchResult' => $this->owner, 'ContainerLink' => $linkToContainer));
712
		$possibleTemplates = array($this->owner->ClassName . 'ElasticSearchResult', 'ElasticSearchResult');
713
		return $this->owner->customise($vars)->renderWith($possibleTemplates);
714
	}
715
716
717
718
	public function getTermVectors() {
719
		return $this->service->getTermVectors($this->owner);
720
	}
721
722
723
	public function updateCMSFields(\FieldList $fields) {
724
		$isIndexed = false;
725
		// SIteTree object must have a live record, ShowInSearch = true
726
		if($this->isInSiteTree($this->owner->ClassName)) {
727
			$liveRecord = \Versioned::get_by_stage(get_class($this->owner), 'Live')->
728
				byID($this->owner->ID);
729
			if($liveRecord->ShowInSearch) {
730
				$isIndexed = true;
731
			} else {
732
				$isIndexed = false;
733
			}
734
		} else {
735
			// In the case of a DataObject we use the ShowInSearchFlag
736
			$isIndexed = true;
737
		}
738
739
		if($isIndexed) {
740
			$termVectors = $this->getTermVectors();
741
			$termFields = array_keys($termVectors);
742
			sort($termFields);
743
744
			foreach($termFields as $field) {
745
				$terms = new \ArrayList();
746
747
				foreach(array_keys($termVectors[$field]['terms']) as $term) {
748
					$do = new \DataObject();
749
					$do->Term = $term;
750
					$stats = $termVectors[$field]['terms'][$term];
751
					if(isset($stats['ttf'])) {
752
						$do->TTF = $stats['ttf'];
753
					}
754
755
					if(isset($stats['doc_freq'])) {
756
						$do->DocFreq = $stats['doc_freq'];
757
					}
758
759
					if(isset($stats['term_freq'])) {
760
						$do->TermFreq = $stats['term_freq'];
761
					}
762
					$terms->push($do);
763
				}
764
765
				$config = \GridFieldConfig_RecordViewer::create(100);
766
				$config->getComponentByType('GridFieldDataColumns')->setDisplayFields(array(
767
					'Term' => 'Term',
768
					'TTF' => 'Total term frequency (how often a term occurs in all documents)',
769
					'DocFreq' => 'n documents with this term',
770
					'TermFreq'=> 'n times this term appears in this field'
771
				));
772
773
			   $underscored = str_replace('.', '_', $field);
774
775
				$gridField = new \GridField(
776
					'TermsFor' . $underscored, // Field name
777
					$field . 'TITLE' . $field, // Field title
778
					$terms,
779
					$config
780
				);
781
			   $fields->addFieldToTab('Root.ElasticaTerms.' . $underscored, $gridField);
782
			}
783
784
		}
785
786
		return $fields;
787
	}
788
789
790
}
791