Merge 27ac599e38
into 9e5d432aa5
This commit is contained in:
commit
d36772dd0d
|
@ -1,4 +1,4 @@
|
|||
# See https://github.com/silverstripe-labs/silverstripe-travis-support for setup details
|
||||
# See https://github.com/silverstripe/silverstripe-travis-support for setup details
|
||||
|
||||
language: php
|
||||
|
||||
|
@ -7,10 +7,9 @@ sudo: false
|
|||
php:
|
||||
- 5.4
|
||||
- 5.5
|
||||
- 5.6
|
||||
|
||||
env:
|
||||
- DB=MYSQL CORE_RELEASE=3.2
|
||||
- DB=MYSQL CORE_RELEASE=3.5
|
||||
|
||||
matrix:
|
||||
include:
|
||||
|
@ -22,10 +21,12 @@ matrix:
|
|||
env: DB=MYSQL CORE_RELEASE=3.3 SUBSITES=1
|
||||
- php: 5.6
|
||||
env: DB=MYSQL CORE_RELEASE=3.3 QUEUEDJOBS=1
|
||||
- php: 7.1
|
||||
env: DB=MYSQL CORE_RELEASE=3.6
|
||||
|
||||
before_script:
|
||||
- composer self-update || true
|
||||
- git clone git://github.com/silverstripe-labs/silverstripe-travis-support.git ~/travis-support
|
||||
- git clone git://github.com/silverstripe/silverstripe-travis-support.git ~/travis-support
|
||||
- "if [ \"$SUBSITES\" = \"\" -a \"$QUEUEDJOBS\" = \"\" ]; then php ~/travis-support/travis_setup.php --source `pwd` --target ~/builds/ss; fi"
|
||||
- "if [ \"$SUBSITES\" = \"1\" ]; then php ~/travis-support/travis_setup.php --source `pwd` --target ~/builds/ss --require silverstripe/subsites; fi"
|
||||
- "if [ \"$QUEUEDJOBS\" = \"1\" ]; then php ~/travis-support/travis_setup.php --source `pwd` --target ~/builds/ss --require silverstripe/queuedjobs; fi"
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -37,6 +37,27 @@ abstract class SearchIndex extends ViewableData
|
|||
*/
|
||||
private static $hide_ancestor;
|
||||
|
||||
/**
|
||||
* Used to separate class name and relation name in the sources array
|
||||
* this string must not be present in class name
|
||||
* @var string
|
||||
* @config
|
||||
*/
|
||||
private static $class_delimiter = '_|_';
|
||||
|
||||
/**
|
||||
* This is used to clean the source name from suffix
|
||||
* suffixes are needed to support multiple relations with the same name on different page types
|
||||
* @param string $source
|
||||
* @return string
|
||||
*/
|
||||
private function getSourceName($source)
|
||||
{
|
||||
$source = explode(self::$class_delimiter, $source);
|
||||
|
||||
return $source[0];
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
@ -77,19 +98,35 @@ abstract class SearchIndex extends ViewableData
|
|||
foreach ($lookups as $lookup) {
|
||||
$next = array();
|
||||
|
||||
foreach ($sources as $source => $options) {
|
||||
$class = null;
|
||||
foreach ($sources as $source => $baseOptions) {
|
||||
$source = $this->getSourceName($source);
|
||||
|
||||
foreach (SearchIntrospection::hierarchy($source, $options['include_children']) as $dataclass) {
|
||||
foreach (SearchIntrospection::hierarchy($source, $baseOptions['include_children']) as $dataclass) {
|
||||
$class = null;
|
||||
$options = $baseOptions;
|
||||
$singleton = singleton($dataclass);
|
||||
|
||||
if ($hasOne = $singleton->has_one($lookup)) {
|
||||
// we only want to include base class for relation, omit classes that inherited the relation
|
||||
$relationList = Config::inst()->get($dataclass, 'has_one', Config::UNINHERITED);
|
||||
$relationList = (!is_null($relationList)) ? $relationList : [];
|
||||
if (!array_key_exists($lookup, $relationList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$class = $hasOne;
|
||||
$options['lookup_chain'][] = array(
|
||||
'call' => 'method', 'method' => $lookup,
|
||||
'through' => 'has_one', 'class' => $dataclass, 'otherclass' => $class, 'foreignkey' => "{$lookup}ID"
|
||||
);
|
||||
} elseif ($hasMany = $singleton->has_many($lookup)) {
|
||||
// we only want to include base class for relation, omit classes that inherited the relation
|
||||
$relationList = Config::inst()->get($dataclass, 'has_many', Config::UNINHERITED);
|
||||
$relationList = (!is_null($relationList)) ? $relationList : [];
|
||||
if (!array_key_exists($lookup, $relationList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$class = $hasMany;
|
||||
$options['multi_valued'] = true;
|
||||
$options['lookup_chain'][] = array(
|
||||
|
@ -97,6 +134,13 @@ abstract class SearchIndex extends ViewableData
|
|||
'through' => 'has_many', 'class' => $dataclass, 'otherclass' => $class, 'foreignkey' => $singleton->getRemoteJoinField($lookup, 'has_many')
|
||||
);
|
||||
} elseif ($manyMany = $singleton->many_many($lookup)) {
|
||||
// we only want to include base class for relation, omit classes that inherited the relation
|
||||
$relationList = Config::inst()->get($dataclass, 'many_many', Config::UNINHERITED);
|
||||
$relationList = (!is_null($relationList)) ? $relationList : [];
|
||||
if (!array_key_exists($lookup, $relationList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$class = $manyMany[1];
|
||||
$options['multi_valued'] = true;
|
||||
$options['lookup_chain'][] = array(
|
||||
|
@ -109,8 +153,10 @@ abstract class SearchIndex extends ViewableData
|
|||
if (!isset($options['origin'])) {
|
||||
$options['origin'] = $dataclass;
|
||||
}
|
||||
$next[$class] = $options;
|
||||
continue 2;
|
||||
|
||||
// we add suffix here to prevent the relation to be overwritten by other instances
|
||||
// all sources lookups must clean the source name before reading it via getSourceName()
|
||||
$next[$class . self::$class_delimiter . $dataclass] = $options;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -123,6 +169,7 @@ abstract class SearchIndex extends ViewableData
|
|||
}
|
||||
|
||||
foreach ($sources as $class => $options) {
|
||||
$class = $this->getSourceName($class);
|
||||
$dataclasses = SearchIntrospection::hierarchy($class, $options['include_children']);
|
||||
|
||||
while (count($dataclasses)) {
|
||||
|
|
|
@ -60,9 +60,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));
|
||||
}
|
||||
|
@ -109,4 +121,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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,10 +27,14 @@
|
|||
"silverstripe/cms": "~3.1",
|
||||
"hafriedlander/silverstripe-phockito": "*"
|
||||
},
|
||||
"extra": [],
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.x-dev"
|
||||
}
|
||||
},
|
||||
"suggest": {
|
||||
"silverstripe/fulltextsearch-localsolr": "Adds a ready-to-use local Solr server for initial development"
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"license": "BSD-3-Clause"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -191,6 +191,17 @@ from a new file `mysite/solr/templates/types.ss` instead:
|
|||
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...")
|
||||
|
||||
|
|
|
@ -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]);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue