mirror of
https://github.com/silverstripe/silverstripe-fulltextsearch
synced 2024-10-22 14:05:29 +02:00
Merge branch 'master' into compat4/btasker
This commit is contained in:
commit
c29e8ec6bf
@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
# FullTextSearch module
|
# FullTextSearch module
|
||||||
|
|
||||||
[![Build Status](https://secure.travis-ci.org/silverstripe-labs/silverstripe-fulltextsearch.png?branch=master)](http://travis-ci.org/silverstripe-labs/silverstripe-fulltextsearch)
|
[![Build Status](https://secure.travis-ci.org/silverstripe/silverstripe-fulltextsearch.png?branch=master)](http://travis-ci.org/silverstripe/silverstripe-fulltextsearch)
|
||||||
|
|
||||||
Adds support for fulltext search engines like Sphinx and Solr to SilverStripe CMS.
|
Adds support for fulltext search engines like Sphinx and Solr to SilverStripe CMS.
|
||||||
|
|
||||||
|
15
code/search/SearchUpdater_BindManipulationCaptureFilter.php
Normal file
15
code/search/SearchUpdater_BindManipulationCaptureFilter.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
class SearchUpdater_BindManipulationCaptureFilter implements RequestFilter
|
||||||
|
{
|
||||||
|
public function preRequest(SS_HTTPRequest $request, Session $session, DataModel $model)
|
||||||
|
{
|
||||||
|
SearchUpdater::bind_manipulation_capture();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function postRequest(SS_HTTPRequest $request, SS_HTTPResponse $response, DataModel $model)
|
||||||
|
{
|
||||||
|
/* NOP */
|
||||||
|
}
|
||||||
|
}
|
71
code/search/SearchUpdater_ObjectHandler.php
Normal file
71
code/search/SearchUpdater_ObjectHandler.php
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete operations do not use database manipulations.
|
||||||
|
*
|
||||||
|
* If a delete has been requested, force a write on objects that should be
|
||||||
|
* indexed. This causes the object to be marked for deletion from the index.
|
||||||
|
*/
|
||||||
|
class SearchUpdater_ObjectHandler extends DataExtension
|
||||||
|
{
|
||||||
|
public function onAfterDelete()
|
||||||
|
{
|
||||||
|
// Calling delete() on empty objects does nothing
|
||||||
|
if (!$this->owner->ID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force SearchUpdater to mark this record as dirty
|
||||||
|
// Note: Some extensions require entire hierarchy passed to augmentWrite()
|
||||||
|
$manipulation = array();
|
||||||
|
foreach (ClassInfo::ancestry($this->owner) as $class) {
|
||||||
|
if (!is_subclass_of($class, 'DataObject')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$manipulation[$class] = array(
|
||||||
|
'fields' => array(),
|
||||||
|
'id' => $this->owner->ID,
|
||||||
|
// Note: 'delete' command not actually handled by manipulations,
|
||||||
|
// but added so that SearchUpdater can detect the deletion
|
||||||
|
'command' => 'delete'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$this->owner->extend('augmentWrite', $manipulation);
|
||||||
|
SearchUpdater::handle_manipulation($manipulation);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forces this object to trigger a re-index in the current state
|
||||||
|
*/
|
||||||
|
public function triggerReindex()
|
||||||
|
{
|
||||||
|
if (!$this->owner->ID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = $this->owner->ID;
|
||||||
|
$class = $this->owner->ClassName;
|
||||||
|
$state = SearchVariant::current_state($class);
|
||||||
|
$base = ClassInfo::baseDataClass($class);
|
||||||
|
$key = "$id:$base:" . serialize($state);
|
||||||
|
|
||||||
|
$statefulids = array(
|
||||||
|
array(
|
||||||
|
'id' => $id,
|
||||||
|
'state' => $state
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$writes = array(
|
||||||
|
$key => array(
|
||||||
|
'base' => $base,
|
||||||
|
'class' => $class,
|
||||||
|
'id' => $id,
|
||||||
|
'statefulids' => $statefulids,
|
||||||
|
'fields' => array()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
SearchUpdater::process_writes($writes);
|
||||||
|
}
|
||||||
|
}
|
@ -99,6 +99,7 @@ class SearchUpdater extends Object
|
|||||||
$id = $details['id'];
|
$id = $details['id'];
|
||||||
$state = $details['state'];
|
$state = $details['state'];
|
||||||
$class = $details['class'];
|
$class = $details['class'];
|
||||||
|
$command = $details['command'];
|
||||||
$fields = isset($details['fields']) ? $details['fields'] : array();
|
$fields = isset($details['fields']) ? $details['fields'] : array();
|
||||||
|
|
||||||
$base = DataObject::getSchema()->baseDataClass($class);
|
$base = DataObject::getSchema()->baseDataClass($class);
|
||||||
@ -113,6 +114,7 @@ class SearchUpdater extends Object
|
|||||||
'class' => $class,
|
'class' => $class,
|
||||||
'id' => $id,
|
'id' => $id,
|
||||||
'statefulids' => $statefulids,
|
'statefulids' => $statefulids,
|
||||||
|
'command' => $command,
|
||||||
'fields' => array()
|
'fields' => array()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -127,9 +129,9 @@ class SearchUpdater extends Object
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trim records without fields
|
// Trim non-delete records without fields
|
||||||
foreach(array_keys($writes) as $key) {
|
foreach (array_keys($writes) as $key) {
|
||||||
if(empty($writes[$key]['fields'])) {
|
if ($writes[$key]['command'] !== 'delete' && empty($writes[$key]['fields'])) {
|
||||||
unset($writes[$key]);
|
unset($writes[$key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,9 +72,21 @@ class SearchVariantSubsites extends SearchVariant
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This field has been altered to allow a user to obtain search results for a particular subsite
|
||||||
|
* When attempting to do this in project code, SearchVariantSubsites kicks and overwrites any filter you've applied
|
||||||
|
* This fix prevents the module from doing this if a filter is applied on the index or the query, or if a field is
|
||||||
|
* being excluded specifically before being executed.
|
||||||
|
*
|
||||||
|
* A pull request has been raised for this issue. Once accepted this forked module can be deleted and the parent
|
||||||
|
* project should be used instead.
|
||||||
|
*/
|
||||||
public function alterQuery($query, $index)
|
public function alterQuery($query, $index)
|
||||||
{
|
{
|
||||||
|
if ($this->isFieldFiltered('_subsite', $query)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$subsite = Subsite::currentSubsiteID();
|
$subsite = Subsite::currentSubsiteID();
|
||||||
$query->filter('_subsite', array($subsite, SearchQuery::$missing));
|
$query->filter('_subsite', array($subsite, SearchQuery::$missing));
|
||||||
}
|
}
|
||||||
@ -121,4 +133,19 @@ class SearchVariantSubsites extends SearchVariant
|
|||||||
$writes[$key]['statefulids'] = $next;
|
$writes[$key]['statefulids'] = $next;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if a field with a certain name is filtered by the search query or on the index
|
||||||
|
* This is the equivalent of saying "show me the results that do ONLY contain this value"
|
||||||
|
* @param $field string name of the field being filtered
|
||||||
|
* @param $query SearchQuery currently being executed
|
||||||
|
* @param $index SearchIndex which specifies a filter field
|
||||||
|
* @return bool true if $field is being filtered, false if it is not being filtered
|
||||||
|
*/
|
||||||
|
protected function isFieldFiltered($field, $query)
|
||||||
|
{
|
||||||
|
$queryHasFilter = !empty($query->require[$field]);
|
||||||
|
|
||||||
|
return $queryHasFilter;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -191,6 +191,17 @@ from a new file `mysite/solr/templates/types.ss` instead:
|
|||||||
return $this->renderWith(Director::baseFolder() . '/mysite/solr/templates/types.ss');
|
return $this->renderWith(Director::baseFolder() . '/mysite/solr/templates/types.ss');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#### Searching for words containing numbers
|
||||||
|
|
||||||
|
By default, the fulltextmodule is configured to split words containing numbers into multiple tokens. For example, the word “A1” would be interpreted as “A” “1”; since “a” is a common stopword, the term “A1” will be excluded from search.
|
||||||
|
|
||||||
|
To allow searches on words containing numeric tokens, you'll need to update your overloaded template to change the behaviour of the WordDelimiterFilterFactory. Each instance of `<filter class="solr.WordDelimiterFilterFactory">` needs to include the following attributes and values:
|
||||||
|
|
||||||
|
* add splitOnNumerics="0" on all WordDelimiterFilterFactory fields
|
||||||
|
* change catenateOnNumbers="1" on all WordDelimiterFilterFactory fields
|
||||||
|
|
||||||
|
Update your index to point to your overloaded template using the method described above.
|
||||||
|
|
||||||
### Spell Checking ("Did you mean...")
|
### Spell Checking ("Did you mean...")
|
||||||
|
|
||||||
|
91
tests/SearchVariantSubsitesTest.php
Normal file
91
tests/SearchVariantSubsitesTest.php
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class SearchVariantSubsiteTest extends SapphireTest
|
||||||
|
{
|
||||||
|
|
||||||
|
private static $index = null;
|
||||||
|
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
// Check versioned available
|
||||||
|
if (!class_exists('Subsite')) {
|
||||||
|
return $this->markTestSkipped('The subsites module is not installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::$index === null) {
|
||||||
|
self::$index = singleton('SearchVariantSubsiteTest');
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchUpdater::bind_manipulation_capture();
|
||||||
|
|
||||||
|
Config::inst()->update('Injector', 'SearchUpdateProcessor', array(
|
||||||
|
'class' => 'SearchUpdateImmediateProcessor'
|
||||||
|
));
|
||||||
|
|
||||||
|
FullTextSearch::force_index_list(self::$index);
|
||||||
|
SearchUpdater::clear_dirty_indexes();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testQueryIsAlteredWhenSubsiteNotSet()
|
||||||
|
{
|
||||||
|
$index = new SolrIndexTest_FakeIndex();
|
||||||
|
$query = new SearchQuery();
|
||||||
|
|
||||||
|
//typical behaviour: nobody is explicitly filtering on subsite, so the search variant adds a filter to the query
|
||||||
|
$this->assertArrayNotHasKey('_subsite', $query->require);
|
||||||
|
$variant = new SearchVariantSubsites();
|
||||||
|
$variant->alterDefinition('SearchUpdaterTest_Container', $index);
|
||||||
|
$variant->alterQuery($query, $index);
|
||||||
|
|
||||||
|
//check that the "default" query has been put in place: it's not empty, and we're searching on Subsite ID:0 and
|
||||||
|
// an object of SearchQuery::missing
|
||||||
|
$this->assertNotEmpty($query->require['_subsite']);
|
||||||
|
$this->assertEquals(0, $query->require['_subsite'][0]);
|
||||||
|
|
||||||
|
//check that SearchQuery::missing is set (by default, it is an object of stdClass)
|
||||||
|
$this->assertInstanceOf('stdClass', $query->require['_subsite'][1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function testQueryIsAlteredWhenSubsiteIsSet()
|
||||||
|
{
|
||||||
|
//now we want to test if somebody has already applied the _subsite filter to the query
|
||||||
|
$index = new SolrIndexTest_FakeIndex();
|
||||||
|
$query = new SearchQuery();
|
||||||
|
|
||||||
|
//check that _subsite is not applied yet
|
||||||
|
//this key should not be exist until the SearchVariant applies it later
|
||||||
|
$this->assertArrayNotHasKey('_subsite', $query->require);
|
||||||
|
|
||||||
|
//apply the subsite filter on the query (for example, if it's passed into a controller and set before searching)
|
||||||
|
//we've chosen an arbirary value of 2 here, to check if it is changed later
|
||||||
|
$query->filter('_subsite', 2);
|
||||||
|
$this->assertNotEmpty($query->require['_subsite']);
|
||||||
|
|
||||||
|
//apply the search variant's definition and query
|
||||||
|
$variant = new SearchVariantSubsites();
|
||||||
|
$variant->alterDefinition('SearchUpdaterTest_Container', $index);
|
||||||
|
|
||||||
|
//the protected function isFieldFiltered is implicitly tested here
|
||||||
|
$variant->alterQuery($query, $index);
|
||||||
|
|
||||||
|
//confirm that the query has been altered, but NOT with default values
|
||||||
|
//first check that _subsite filter is not empty
|
||||||
|
$this->assertNotEmpty($query->require['_subsite']);
|
||||||
|
//subsite filter first value is not 0
|
||||||
|
$this->assertNotEquals(0, $query->require['_subsite'][0]);
|
||||||
|
|
||||||
|
//subsite filter SearchQuery::missing should not be set so its expected location is empty
|
||||||
|
$this->assertArrayNotHasKey(1, $query->require['_subsite']);
|
||||||
|
|
||||||
|
//subsite filter has been modified with our arbitrary test value. The second value is not set
|
||||||
|
//this proves that the query has not been altered by the variant
|
||||||
|
$this->assertEquals(2, $query->require['_subsite'][0]);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -79,6 +79,28 @@ class SearchVariantVersionedTest extends SapphireTest
|
|||||||
));
|
));
|
||||||
$added = self::$index->getAdded(array('ID', '_versionedstage'));
|
$added = self::$index->getAdded(array('ID', '_versionedstage'));
|
||||||
$this->assertEquals($expected, $added);
|
$this->assertEquals($expected, $added);
|
||||||
|
|
||||||
|
// Test unpublish
|
||||||
|
|
||||||
|
self::$index->reset();
|
||||||
|
|
||||||
|
$item->deleteFromStage('Live');
|
||||||
|
|
||||||
|
SearchUpdater::flush_dirty_indexes();
|
||||||
|
|
||||||
|
$this->assertCount(1, self::$index->deleted);
|
||||||
|
$this->assertEquals(
|
||||||
|
'SiteTree',
|
||||||
|
self::$index->deleted[0]['base']
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
$item->ID,
|
||||||
|
self::$index->deleted[0]['id']
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
'Live',
|
||||||
|
self::$index->deleted[0]['state']['SearchVariantVersioned']
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testExcludeVariantState()
|
public function testExcludeVariantState()
|
||||||
|
Loading…
Reference in New Issue
Block a user