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
|
||||
|
||||
[![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.
|
||||
|
||||
|
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'];
|
||||
$state = $details['state'];
|
||||
$class = $details['class'];
|
||||
$command = $details['command'];
|
||||
$fields = isset($details['fields']) ? $details['fields'] : array();
|
||||
|
||||
$base = DataObject::getSchema()->baseDataClass($class);
|
||||
@ -113,6 +114,7 @@ class SearchUpdater extends Object
|
||||
'class' => $class,
|
||||
'id' => $id,
|
||||
'statefulids' => $statefulids,
|
||||
'command' => $command,
|
||||
'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) {
|
||||
if(empty($writes[$key]['fields'])) {
|
||||
if ($writes[$key]['command'] !== 'delete' && empty($writes[$key]['fields'])) {
|
||||
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)
|
||||
{
|
||||
if ($this->isFieldFiltered('_subsite', $query)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$subsite = Subsite::currentSubsiteID();
|
||||
$query->filter('_subsite', array($subsite, SearchQuery::$missing));
|
||||
}
|
||||
@ -121,4 +133,19 @@ class SearchVariantSubsites extends SearchVariant
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
@ -192,6 +192,17 @@ from a new file `mysite/solr/templates/types.ss` instead:
|
||||
}
|
||||
}
|
||||
|
||||
#### 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...")
|
||||
|
||||
Solr has various spell checking strategies (see the ["SpellCheckComponent" docs](http://wiki.apache.org/solr/SpellCheckComponent)), all of which are configured through `solrconfig.xml`.
|
||||
|
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'));
|
||||
$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()
|
||||
|
Loading…
Reference in New Issue
Block a user