Completed
Push — dev2 ( 44876d...1ebec9 )
by Gordon
03:20
created

Searchable::getFormatForDate()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 19
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

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