silverstripe-labs/silverstripe-fulltextsearch — pull request #23 by chillu (2013-05-08 15:37:14+00:00)
Title: Shared fields

Its not 100% clean to map one index field to multiple classes (at least in the way the current `SearchIndex` works), but its just too common to leave up to custom implementations. In all three search project I was involved in so far we needed to do this (to combine fields from CMS and DMS). See before/after example below.

- I'm unsetting `base` and `origin` keys in case the field is shared. Can you think of any side effects from this? All tests pass.
- It's assumed that the type of all shared fields is the same (up to the developer at the moment). The last type "wins".
- Its all-or-nothing, you can't share a field on some classes, and have a separate field with the same name on another. Due to the way SS property access works, this will fail silently on unknown properties.
- I've removed the "get" with type "method", and simply rely on with the type "property". This allows for more flexible overloading, e.g. SiteTree->Content as a native property, but DMSDocument->getContent() as a getter. SS treats them the same internally.
- Haven't checked if shared fields work with relation lookup chains.

Before:

```
addClass('Page');
$this->addClass('DMSDocument');

$stored = (Director::isDev()) ? 'true' : 'false';

// Fields are available on both types
$this->addFulltextField('Title', 'Text', array('boost' => '3',));
// Highlighted fields need to be stored
$this->addFulltextField('Content', 'HTMLText', array('stored' => $stored));
$this->addFulltextField('Description', 'HTMLText', array('stored' => $stored, 'boost' => '1.25'));

// Technically, filter and sort fields are the same in Solr/Lucene
$this->addSortField('ViewCount');
// Caution: Stores LastEdited for SiteTree, but LastChanged for DMSDocument (see below)
$this->addSortField('LastEdited', 'SSDatetime');
//$this->addSortField('Title');

// Aggregate fields for spelling checks
$this->addCopyField('SiteTree_Title', 'spellcheckData');
$this->addCopyField('DMSDocument_Title', 'spellcheckData');
$this->addCopyField('SiteTree_Content', 'spellcheckData');
$this->addCopyField('DMSDocument_Content', 'spellcheckData');

// Aggregate fields for highlighting
$this->addCopyField('SiteTree_Title', 'highlightData', array('maxChars' => 10000));
$this->addCopyField('DMSDocument_Title', 'highlightData', array('maxChars' => 10000));
$this->addCopyField('SiteTree_Content', 'highlightData', array('maxChars' => 10000));
$this->addCopyField('DMSDocument_Content', 'highlightData', array('maxChars' => 10000));

// Don't support searching staging
$this->excludeVariantState(array('SearchVariantVersioned' => 'Stage'));
}

protected function _addAs($object, $base, $options) {
$includeSubs = $options['include_children'];

$doc = new Apache_Solr_Document();

// Always present fields

$doc->setField('_documentid', $this->getDocumentID($object, $base, $includeSubs));
$doc->setField('ID', $object->ID);
$doc->setField('ClassName', $object->ClassName);

foreach (SearchIntrospection::hierarchy(get_class($object), false) as $class) $doc->addField('ClassHierarchy', $class);

// Add the user-specified fields

foreach ($this->getFieldsIterator() as $name => $field) {
if ($field['base'] == $base) $this->_addField($doc, $object, $field);
}

// CUSTOM Duplicate index combined fields ("Title" rather than "DMSDocument_Title" and "SiteTree_Title").
// This allows us to sort on these fields without deeper architectural changes to the fulltextsearch module.
// Note: We can't use for this purpose because it only writes into multiValue=true
// fields, and those can't be (reliably) sorted on.
$this->_addField($doc, $object, $this->getCustomPropertyFieldData('Title', $object));
$lastEditedField = ($object instanceof DMSDocument) ? 'LastChanged' : 'LastEdited';
$this->_addField($doc, $object, $this->getCustomPropertyFieldData('LastEdited', $object, 'SSDatetime', $lastEditedField));
// CUSTOM END

$this->getService()->addDocument($doc);

return $doc;
}

function getFieldDefinitions() {
$xml = parent::getFieldDefinitions();

$stored = (Director::isDev()) ? "stored='true'" : "stored='false'";

$xml .= "\n\n\t\t";
$xml .= "\n\t\t";
$xml .= "\n\t\t";

return $xml;
}

/**
* Return a fake field definition (mainly to stay DRY).
*/
protected function getCustomPropertyFieldData($name, $object, $type = null, $customProperty = null) {
if(!$type) $type = 'Varchar';
return array(
'name' => $name,
'field' => $name,
'fullfield' => $name,
'base' => get_class($object),
'origin' => get_class($object),
'class' => get_class($object),
'lookup_chain' => array(
array('call' => 'property', 'property' => $customProperty ? $customProperty : $name)
),
'type' => $type,
'multi_valued' => false,
'extra_options' => array()
);
}
}
```

After:

```
addClass('Page');
$this->addClass('DMSDocument');

// Fields are available on both types
$this->addFulltextField('Title', 'Text', array('boost' => '3', 'shared' => true));
// Highlighted fields need to be stored
$this->addFulltextField('Content', 'HTMLText', array('stored' => $stored, 'shared' => true));
$this->addFulltextField('Description', 'HTMLText', array('stored' => $stored, 'boost' => '1.25'));

// Technically, filter and sort fields are the same in Solr/Lucene
$this->addSortField('ViewCount');
$this->addSortField('LastEdited', 'SSDatetime', array('shared' => true));

// Aggregate fields for spelling checks
$this->addCopyField('Title', 'spellcheckData');
$this->addCopyField('Content', 'spellcheckData');

// Aggregate fields for highlighting
$this->addCopyField('Title', 'highlightData', array('maxChars' => 10000));
$this->addCopyField('Content', 'highlightData', array('maxChars' => 10000));

// Don't support searching staging
$this->excludeVariantState(array('SearchVariantVersioned' => 'Stage'));
}

}
```