mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
API Enable single-column fulltext filter search as fallback
This commit is contained in:
parent
40c5b8b675
commit
782c4cbf6f
@ -39,6 +39,43 @@ records and cannot easily be adapted to include custom `DataObject` instances. T
|
|||||||
default site search, have a look at those extensions and modify as required.
|
default site search, have a look at those extensions and modify as required.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
### Fulltext Filter
|
||||||
|
|
||||||
|
SilverStripe provides a `[api:FulltextFiler]` which you can use to perform custom fulltext searches on
|
||||||
|
`[api:DataList]`'s.
|
||||||
|
|
||||||
|
Example DataObject:
|
||||||
|
|
||||||
|
:::php
|
||||||
|
class SearchableDataObject extends DataObject {
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
"Title" => "Varchar(255)",
|
||||||
|
"Content" => "HTMLText",
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $indexes = array(
|
||||||
|
'SearchFields' => array(
|
||||||
|
'type' => 'fulltext',
|
||||||
|
'name' => 'SearchFields',
|
||||||
|
'value' => '"Title", "Content"',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $create_table_options = array(
|
||||||
|
'MySQLDatabase' => 'ENGINE=MyISAM'
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Performing the search:
|
||||||
|
|
||||||
|
:::php
|
||||||
|
SearchableDataObject::get()->filter('SearchFields:fulltext', 'search term');
|
||||||
|
|
||||||
|
If your search index is a single field size, then you may also specify the search filter by the name of the
|
||||||
|
field instead of the index.
|
||||||
|
|
||||||
## API Documentation
|
## API Documentation
|
||||||
|
|
||||||
* [api:FulltextSearchable]
|
* [api:FulltextSearchable]
|
@ -68,18 +68,16 @@ class FulltextFilter extends SearchFilter {
|
|||||||
} else {
|
} else {
|
||||||
// Parse a fulltext string (eg. fulltext ("ColumnA", "ColumnB")) to figure out which columns
|
// Parse a fulltext string (eg. fulltext ("ColumnA", "ColumnB")) to figure out which columns
|
||||||
// we need to search.
|
// we need to search.
|
||||||
if(preg_match('/^fulltext\ \((.+)\)$/i', $index, $matches)) {
|
if(preg_match('/^fulltext\s+\((.+)\)$/i', $index, $matches)) {
|
||||||
return $matches[1];
|
return $matches[1];
|
||||||
} else {
|
} else {
|
||||||
throw new Exception("Invalid fulltext index format for '" . $this->getName()
|
throw new Exception("Invalid fulltext index format for '" . $this->getName()
|
||||||
. "' on '" . $this->model . "'");
|
. "' on '" . $this->model . "'");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $columns;
|
}
|
||||||
}
|
|
||||||
|
return parent::getDbName();
|
||||||
throw new Exception($this->getName() . ' is not a fulltext index on ' . $this->model . '.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -9,27 +9,27 @@
|
|||||||
* @subpackage search
|
* @subpackage search
|
||||||
*/
|
*/
|
||||||
abstract class SearchFilter extends Object {
|
abstract class SearchFilter extends Object {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string Classname of the inspected {@link DataObject}
|
* @var string Classname of the inspected {@link DataObject}
|
||||||
*/
|
*/
|
||||||
protected $model;
|
protected $model;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected $name;
|
protected $name;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected $fullName;
|
protected $fullName;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var mixed
|
* @var mixed
|
||||||
*/
|
*/
|
||||||
protected $value;
|
protected $value;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
@ -41,7 +41,7 @@ abstract class SearchFilter extends Object {
|
|||||||
* {@link applyRelation()}.
|
* {@link applyRelation()}.
|
||||||
*/
|
*/
|
||||||
protected $relation;
|
protected $relation;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $fullName Determines the name of the field, as well as the searched database
|
* @param string $fullName Determines the name of the field, as well as the searched database
|
||||||
* column. Can contain a relation name in dot notation, which will automatically join
|
* column. Can contain a relation name in dot notation, which will automatically join
|
||||||
@ -58,7 +58,7 @@ abstract class SearchFilter extends Object {
|
|||||||
$this->value = $value;
|
$this->value = $value;
|
||||||
$this->setModifiers($modifiers);
|
$this->setModifiers($modifiers);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called by constructor to convert a string pathname into
|
* Called by constructor to convert a string pathname into
|
||||||
* a well defined relationship sequence.
|
* a well defined relationship sequence.
|
||||||
@ -74,7 +74,7 @@ abstract class SearchFilter extends Object {
|
|||||||
$this->name = $name;
|
$this->name = $name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the root model class to be selected by this
|
* Set the root model class to be selected by this
|
||||||
* search query.
|
* search query.
|
||||||
@ -84,7 +84,7 @@ abstract class SearchFilter extends Object {
|
|||||||
public function setModel($className) {
|
public function setModel($className) {
|
||||||
$this->model = $className;
|
$this->model = $className;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the current value to be filtered on.
|
* Set the current value to be filtered on.
|
||||||
*
|
*
|
||||||
@ -93,7 +93,7 @@ abstract class SearchFilter extends Object {
|
|||||||
public function setValue($value) {
|
public function setValue($value) {
|
||||||
$this->value = $value;
|
$this->value = $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accessor for the current value to be filtered on.
|
* Accessor for the current value to be filtered on.
|
||||||
* Caution: Data is not escaped.
|
* Caution: Data is not escaped.
|
||||||
@ -121,7 +121,7 @@ abstract class SearchFilter extends Object {
|
|||||||
public function getModifiers() {
|
public function getModifiers() {
|
||||||
return $this->modifiers;
|
return $this->modifiers;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The original name of the field.
|
* The original name of the field.
|
||||||
*
|
*
|
||||||
@ -137,7 +137,7 @@ abstract class SearchFilter extends Object {
|
|||||||
public function setName($name) {
|
public function setName($name) {
|
||||||
$this->name = $name;
|
$this->name = $name;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The full name passed to the constructor,
|
* The full name passed to the constructor,
|
||||||
* including any (optional) relations in dot notation.
|
* including any (optional) relations in dot notation.
|
||||||
@ -154,7 +154,7 @@ abstract class SearchFilter extends Object {
|
|||||||
public function setFullName($name) {
|
public function setFullName($name) {
|
||||||
$this->fullName = $name;
|
$this->fullName = $name;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalizes the field name to table mapping.
|
* Normalizes the field name to table mapping.
|
||||||
*
|
*
|
||||||
@ -168,7 +168,9 @@ abstract class SearchFilter extends Object {
|
|||||||
|
|
||||||
// Ensure that we're dealing with a DataObject.
|
// Ensure that we're dealing with a DataObject.
|
||||||
if (!is_subclass_of($this->model, 'DataObject')) {
|
if (!is_subclass_of($this->model, 'DataObject')) {
|
||||||
throw new Exception("Model supplied to " . get_class($this) . " should be an instance of DataObject.");
|
throw new InvalidArgumentException(
|
||||||
|
"Model supplied to " . get_class($this) . " should be an instance of DataObject."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$candidateClass = ClassInfo::table_for_object_field(
|
$candidateClass = ClassInfo::table_for_object_field(
|
||||||
@ -183,9 +185,9 @@ abstract class SearchFilter extends Object {
|
|||||||
return '"' . implode('"."', $parts) . '"';
|
return '"' . implode('"."', $parts) . '"';
|
||||||
}
|
}
|
||||||
|
|
||||||
return "\"{$candidateClass}\".\"{$this->name}\"";
|
return sprintf('"%s"."%s"', $candidateClass, $this->name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the value of the field as processed by the DBField class
|
* Return the value of the field as processed by the DBField class
|
||||||
*
|
*
|
||||||
@ -200,7 +202,6 @@ abstract class SearchFilter extends Object {
|
|||||||
return $dbField->RAW();
|
return $dbField->RAW();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply filter criteria to a SQL query.
|
* Apply filter criteria to a SQL query.
|
||||||
*
|
*
|
||||||
@ -272,7 +273,7 @@ abstract class SearchFilter extends Object {
|
|||||||
protected function excludeMany(DataQuery $query) {
|
protected function excludeMany(DataQuery $query) {
|
||||||
throw new InvalidArgumentException(get_class($this) . " can't be used to filter by a list of items.");
|
throw new InvalidArgumentException(get_class($this) . " can't be used to filter by a list of items.");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if a field has a value,
|
* Determines if a field has a value,
|
||||||
* and that the filter should be applied.
|
* and that the filter should be applied.
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
class FulltextFilterTest extends SapphireTest {
|
class FulltextFilterTest extends SapphireTest {
|
||||||
|
|
||||||
|
protected $extraDataObjects = array(
|
||||||
|
'FulltextFilterTest_DataObject'
|
||||||
|
);
|
||||||
|
|
||||||
protected static $fixture_file = "FulltextFilterTest.yml";
|
protected static $fixture_file = "FulltextFilterTest.yml";
|
||||||
|
|
||||||
|
|
||||||
public function testFilter() {
|
public function testFilter() {
|
||||||
if(DB::getConn() instanceof MySQLDatabase) {
|
if(DB::getConn() instanceof MySQLDatabase) {
|
||||||
$baseQuery = FulltextDataObject::get();
|
$baseQuery = FulltextFilterTest_DataObject::get();
|
||||||
$this->assertEquals(3, $baseQuery->count(), "FulltextDataObject count does not match.");
|
$this->assertEquals(3, $baseQuery->count(), "FulltextFilterTest_DataObject count does not match.");
|
||||||
|
|
||||||
// First we'll text the 'SearchFields' which has been set using an array
|
// First we'll text the 'SearchFields' which has been set using an array
|
||||||
$search = $baseQuery->filter("SearchFields:fulltext", 'SilverStripe');
|
$search = $baseQuery->filter("SearchFields:fulltext", 'SilverStripe');
|
||||||
@ -25,24 +28,64 @@ class FulltextFilterTest extends SapphireTest {
|
|||||||
$search = $baseQuery->exclude("OtherSearchFields:fulltext", "SilverStripe");
|
$search = $baseQuery->exclude("OtherSearchFields:fulltext", "SilverStripe");
|
||||||
$this->assertEquals(2, $search->count());
|
$this->assertEquals(2, $search->count());
|
||||||
|
|
||||||
// Edgecase
|
// Search on a single field
|
||||||
$this->setExpectedException("Exception");
|
$search = $baseQuery->filter("ColumnE:fulltext", 'Dragons');
|
||||||
$search = $baseQuery->exclude("Madeup:fulltext", "SilverStripe");
|
$this->assertEquals(1, $search->count());
|
||||||
|
|
||||||
|
$search = $baseQuery->exclude("ColumnE:fulltext", "Dragons");
|
||||||
|
$this->assertEquals(2, $search->count());
|
||||||
} else {
|
} else {
|
||||||
$this->markTestSkipped("FulltextFilter only supports MySQL syntax.");
|
$this->markTestSkipped("FulltextFilter only supports MySQL syntax.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testGenerateQuery() {
|
||||||
|
// Test SearchFields
|
||||||
|
$filter1 = new FulltextFilter('SearchFields', 'SilverStripe');
|
||||||
|
$filter1->setModel('FulltextFilterTest_DataObject');
|
||||||
|
$query1 = FulltextFilterTest_DataObject::get()->dataQuery();
|
||||||
|
$filter1->apply($query1);
|
||||||
|
$this->assertEquals('"ColumnA", "ColumnB"', $filter1->getDbName());
|
||||||
|
$this->assertEquals(
|
||||||
|
array("MATCH (\"ColumnA\", \"ColumnB\") AGAINST ('SilverStripe')"),
|
||||||
|
$query1->query()->getWhere()
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
// Test Other searchfields
|
||||||
|
$filter2 = new FulltextFilter('OtherSearchFields', 'SilverStripe');
|
||||||
|
$filter2->setModel('FulltextFilterTest_DataObject');
|
||||||
|
$query2 = FulltextFilterTest_DataObject::get()->dataQuery();
|
||||||
|
$filter2->apply($query2);
|
||||||
|
$this->assertEquals('"ColumnC", "ColumnD"', $filter2->getDbName());
|
||||||
|
$this->assertEquals(
|
||||||
|
array("MATCH (\"ColumnC\", \"ColumnD\") AGAINST ('SilverStripe')"),
|
||||||
|
$query2->query()->getWhere()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test fallback to single field
|
||||||
|
$filter3 = new FulltextFilter('ColumnA', 'SilverStripe');
|
||||||
|
$filter3->setModel('FulltextFilterTest_DataObject');
|
||||||
|
$query3 = FulltextFilterTest_DataObject::get()->dataQuery();
|
||||||
|
$filter3->apply($query3);
|
||||||
|
$this->assertEquals('"FulltextFilterTest_DataObject"."ColumnA"', $filter3->getDbName());
|
||||||
|
$this->assertEquals(
|
||||||
|
array("MATCH (\"FulltextFilterTest_DataObject\".\"ColumnA\") AGAINST ('SilverStripe')"),
|
||||||
|
$query3->query()->getWhere()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class FulltextDataObject extends DataObject {
|
class FulltextFilterTest_DataObject extends DataObject implements TestOnly {
|
||||||
|
|
||||||
private static $db = array(
|
private static $db = array(
|
||||||
"ColumnA" => "Varchar(255)",
|
"ColumnA" => "Varchar(255)",
|
||||||
"ColumnB" => "HTMLText",
|
"ColumnB" => "HTMLText",
|
||||||
"ColumnC" => "Varchar(255)",
|
"ColumnC" => "Varchar(255)",
|
||||||
"ColumnD" => "HTMLText",
|
"ColumnD" => "HTMLText",
|
||||||
|
"ColumnE" => 'Varchar(255)'
|
||||||
);
|
);
|
||||||
|
|
||||||
private static $indexes = array(
|
private static $indexes = array(
|
||||||
@ -52,6 +95,7 @@ class FulltextDataObject extends DataObject {
|
|||||||
'value' => '"ColumnA", "ColumnB"',
|
'value' => '"ColumnA", "ColumnB"',
|
||||||
),
|
),
|
||||||
'OtherSearchFields' => 'fulltext ("ColumnC", "ColumnD")',
|
'OtherSearchFields' => 'fulltext ("ColumnC", "ColumnD")',
|
||||||
|
'SingleIndex' => 'fulltext ("ColumnE")'
|
||||||
);
|
);
|
||||||
|
|
||||||
private static $create_table_options = array(
|
private static $create_table_options = array(
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
FulltextDataObject:
|
FulltextFilterTest_DataObject:
|
||||||
object1:
|
object1:
|
||||||
ColumnA: 'SilverStripe'
|
ColumnA: 'SilverStripe'
|
||||||
CluumnB: <p>Some content about SilverStripe.</p>
|
CluumnB: '<p>Some content about SilverStripe.</p>'
|
||||||
ColumnC: 'SilverStripe'
|
ColumnC: 'SilverStripe'
|
||||||
ColumnD: '<p>Some content about SilverStripe.</p>
|
ColumnD: '<p>Some content about SilverStripe.</p>'
|
||||||
|
ColumnE: 'Dragons be here'
|
||||||
object2:
|
object2:
|
||||||
ColumnA: 'Test Row'
|
ColumnA: 'Test Row'
|
||||||
ColumnB: '<p>Some information about this test row.</p>'
|
ColumnB: '<p>Some information about this test row.</p>'
|
||||||
ColumnC: 'Test Row'
|
ColumnC: 'Test Row'
|
||||||
ColumnD: '<p>Some information about this test row.</p>'
|
ColumnD: '<p>Some information about this test row.</p>'
|
||||||
|
ColumnE: 'No'
|
||||||
object3:
|
object3:
|
||||||
ColumnA: 'Fulltext Search'
|
ColumnA: 'Fulltext Search'
|
||||||
ColumnB: '<p>Testing fulltext search.</p>'
|
ColumnB: '<p>Testing fulltext search.</p>'
|
||||||
ColumnC: 'Fulltext Search
|
ColumnC: 'Fulltext Search'
|
||||||
ColumnD: '<p>Testing fulltext search.</p>'
|
ColumnD: '<p>Testing fulltext search.</p>'
|
||||||
|
ColumnE: ''
|
||||||
|
Loading…
Reference in New Issue
Block a user