This commit is contained in:
Mojmir Fendek 2017-06-15 21:27:56 +00:00 committed by GitHub
commit d36772dd0d
7 changed files with 195 additions and 13 deletions

View File

@ -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"

View File

@ -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.

View File

@ -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)) {

View File

@ -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;
}
}

View File

@ -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"
}
}

View File

@ -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...")

View 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]);
}
}