Completed
Push — dev2 ( 594cc8...3fec4d )
by Gordon
03:32
created

Searchable::onAfterDelete()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

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