mirror of
https://github.com/silverstripe/silverstripe-fulltextsearch
synced 2024-10-22 14:05:29 +02:00
Merge pull request #150 from silverstripe-terraformers/feature/complex-filtering
Add nested filtering via Criteria and Criterion objects.
This commit is contained in:
commit
1f156dfbb4
@ -4,3 +4,8 @@ Name: fulltextsearchconfig
|
||||
SilverStripe\ORM\DataObject:
|
||||
extensions:
|
||||
- SilverStripe\FullTextSearch\Search\Extensions\SearchUpdater_ObjectHandler
|
||||
|
||||
SilverStripe\Core\Injector\Injector:
|
||||
SilverStripe\FullTextSearch\Search\Queries\SearchQuery:
|
||||
calls:
|
||||
- [ setHandler, [ %$SilverStripe\FullTextSearch\Search\Adapters\SolrSearchAdapter ]]
|
||||
|
@ -19,6 +19,7 @@
|
||||
- [Searching value ranges](04_querying.md#searching-value-ranges)
|
||||
- [Empty or existing values](04_querying.md#empty-or-existing-values)
|
||||
- [Executing your query](04_querying.md#executing-your-query)
|
||||
- [Complex filtering with Criteria](04_querying.md#complex-filtering-with-criteria)
|
||||
- Advanced configuration
|
||||
- [Facets](05_advanced_configuration.md#facets)
|
||||
- [Using multiple indexes](05_advanced_configuration.md#multiple-indexes)
|
||||
|
@ -128,3 +128,164 @@ The return value of a `search()` call is an object which contains a few properti
|
||||
* `Suggestion`: (optional) Any suggested spelling corrections in the original query notation
|
||||
* `SuggestionNice`: (optional) Any suggested spelling corrections for display (without query notation)
|
||||
* `SuggestionQueryString` (optional) Link to repeat the search with suggested spelling corrections
|
||||
|
||||
## Complex Filtering with Criteria
|
||||
|
||||
### Filtering related Objects
|
||||
|
||||
* `SearchCriteriaInterface`: Interface for `SearchCriterion` and `SearchCriteria` classes.
|
||||
* `SearchCriterion`: An object containing a single field filter (target field, comparison value, comparison type).
|
||||
* `SearchCriteria`: An object containing a collection of `SearchCriterion` and/or `SearchCriteria` with conjunctions (IE: `AND`, `OR`) between each.
|
||||
* `SearchQueryWriter`: A class used to generate a query string based on a `SearchCriterion`.
|
||||
* `SearchAdapterInterface`: An Interface for our SearchAdapters. This adapter will control what `SearchQueryWriter` is used for each `SearchCriteria`.
|
||||
|
||||
### General usage
|
||||
|
||||
We need 3 things to create a `SearchCriterion`:
|
||||
|
||||
* **`Target`**: EG the field in our Search Index that we want to filter against.
|
||||
* **`Value`**: The value we want to use for comparison.
|
||||
* **`Comparison`**: The type of comparison (EG: `EQUAL`, `IN`, etc).
|
||||
|
||||
All currently supported comparisons can be found as constants in `SearchCriterion`.
|
||||
|
||||
### Creating a new `SearchCriterion`
|
||||
|
||||
#### Method 1a and 1b
|
||||
|
||||
```php
|
||||
// `EQUAL` is the default comparison for `SearchCriterion`, so no third param is required.
|
||||
$criterion = new SearchCriterion('Product_Title', 'My Product');
|
||||
|
||||
// Or use the `create` static method.
|
||||
$criterion = SearchCriterion::create('Product_Title', 'My Product');
|
||||
```
|
||||
|
||||
### Creating a new `SearchCriteria`
|
||||
|
||||
`SearchCriteria` has a property called `$clauses` which is a collection of `SearchCriterion` (above) and/or `SearchCriteria` (allowing for infinite nesting of clauses), along with the conjunction used between each clause (IE: `AND`, `OR`). We want to build up our `SearchCriteria` by adding to it's `$clauses` collection.
|
||||
|
||||
`SearchCriteria` can either be passed an object that implements `SearchCriteriaInterface`, or it can be passed the `Target`, `Value`, and `Comparison` (like above).
|
||||
|
||||
#### Method 1
|
||||
|
||||
Instantiate a new `SearchCriteria` by providing an already instantiated `SearchCriterion` object. This `$criterion` will be added as the first item in the `$clauses` collection.
|
||||
|
||||
```php
|
||||
$criteria = SearchCriteria::create($criterion);
|
||||
```
|
||||
|
||||
#### Method 2
|
||||
|
||||
Instantiate a new `SearchCriteria` objects and define the `Target`, `Value`, and `Comparison`. `SearchCriteria` will create a new `SearchCriterion` object based on the values, and add it to the `$clauses` collection.
|
||||
|
||||
```php
|
||||
$criteria = SearchCriteria::create('Product_CatID', array(21, 24, 25), AbstractCriterion::IN);
|
||||
```
|
||||
|
||||
### Adding additional `SearchCriterion` to our `SearchCriteria`
|
||||
|
||||
When you want to add more complexity to your `SearchCriteria`, there are two methods available:
|
||||
|
||||
* `addAnd`: Add a new `SearchCriterion` or `SearchCriteria` with an `AND` conjunction.
|
||||
* `addOr`: Add a new `SearchCriterion` or `SearchCriteria` with an `OR` conjunction.
|
||||
|
||||
#### Method 1
|
||||
|
||||
Use method chaining to create a `SearchCriterion` with two clauses.
|
||||
|
||||
```php
|
||||
// Filter by products with stock that are in either of these 3 categories.
|
||||
$criteria = SearchCriteria::create('Product_CatID', array(21, 24, 25), AbstractCriterion::IN)
|
||||
->addAnd('Product_Stock', 0, AbstractSearchCriterion::GREATER_THAN);
|
||||
```
|
||||
|
||||
#### Method 2
|
||||
|
||||
Systematically add clauses to your already instantiated `SearchCriteria`.
|
||||
|
||||
```php
|
||||
// Filter by products in either of these 3 categories.
|
||||
$criteria = SearchCriteria::create('Product_CatID', array(21, 24, 25), AbstractCriterion::IN);
|
||||
|
||||
... other stuff
|
||||
|
||||
// Filter by products with stock.
|
||||
$criteria->addAnd('Product_StockLevel', 0, AbstractCriterion::GREATER_THAN);
|
||||
```
|
||||
|
||||
### Adding multiple levels of filtering to our `SearchCriteria`
|
||||
|
||||
`SearchCriteria` also allows you to pass in other `SearchCriteria` objects as you instantiate it and as you use the `addAnd` and `addOr` methods.
|
||||
|
||||
```php
|
||||
// Filter by products that are in either of these 3 categories with stock.
|
||||
$stockCategoryCriteria = SearchCriteria::create('Product_CatID', array(21, 24, 25), AbstractCriterion::IN)
|
||||
->addAnd('Product_Stock', 0, AbstractSearchCriterion::GREATER_THAN);
|
||||
|
||||
// Filter by products in Category ID 1 with stock over 5.
|
||||
$legoCriteria = SearchCriteria::create('Product_CatID', 1, AbstractCriterion::EQUAL)
|
||||
->addAnd('Product_Stock', 5, AbstractSearchCriterion::GREATER_THAN);
|
||||
|
||||
// Combine the two criteria with an `OR` conjunction
|
||||
$criteria = SearchCriteria::create($stockCategoryCriteria)
|
||||
->addOr($legoCriteria);
|
||||
```
|
||||
|
||||
### Adding `SearchCriteria` to our `SearchQuery`
|
||||
|
||||
Our `SearchQuery` class now has a property called `$criteria` which holds all of our `SearchCriteria`. You can add new `SearchCriteria` by using `SearchQuery::filterBy()`.
|
||||
|
||||
#### Method 1
|
||||
|
||||
Pass in an already instantiated `SearchCriteria` object. If you implemented complex filtering (above), you will probably need to follow this method - fully creating your `SearchCriteria` first, and then passing it to the `SearchQuery`.
|
||||
|
||||
```php
|
||||
$query->filterBy($criteria);
|
||||
```
|
||||
|
||||
#### Method 2a
|
||||
Where basic (single level) filtering is ok, the `SearchQuery::filterBy()` method can be used to create your `SearchCriterion` and `SearchCriteria` object.
|
||||
|
||||
```php
|
||||
$query->filterBy('Product_CatID', array(21, 24, 25), AbstractCriterion::IN);
|
||||
```
|
||||
|
||||
#### Method 2b
|
||||
The `filterBy()` method will return the **current** `SearchCriteria`, this allows you to method chain the `addAnd` and `addOr` methods.
|
||||
|
||||
```php
|
||||
// Filter by products with stock that are in either of these 3 categories.
|
||||
$searchQuery->filterBy('Product_CategoryID', array(21, 24, 25), AbstractCriterion::IN)
|
||||
->addAnd('Product_StockLevel', 0, AbstractCriterion::GREATER_THAN);
|
||||
```
|
||||
|
||||
Each item in the `$criteria` collection are treated with an `AND` conjunction (matching current `filter`/`exclude` functionality).
|
||||
|
||||
### Search Query Writers
|
||||
|
||||
Provided are 3 different `SearchQueryWriter`s for Solr:
|
||||
|
||||
* `SolrSearchQueryWriter_Basic`
|
||||
* `SolrSearchQueryWriter_In`
|
||||
* `SolrSearchQueryWriter_Range`
|
||||
|
||||
When these Writers are provided a `SearchCriterion`, they will generate the desired query string.
|
||||
|
||||
### Search Adapters
|
||||
|
||||
Search Adapters need to provide the following information:
|
||||
|
||||
* What is the search engine's conjunction strings? (EG: are they "AND" and "OR", or are they "&&" and "||", etc).
|
||||
* What is the desired comparison container string? (EG: "**+(** query here **)**") for Solr).
|
||||
* Most importantly - how to generate the query string from a `SearchCriterion`.
|
||||
|
||||
The `SolrSearchAdapter` uses `SearchQueryWriter`s (above) to generate query strings from a `SearchCriterion`.
|
||||
|
||||
### Customising your `SearchCriterion`/`SearchQueryWriter`
|
||||
|
||||
If you find that you do not want your `SearchCriterion` being parsed by one of the default `SearchQueryWriter`s (for whatever reason), you can optionally pass your own `SearchQueryWriter` to your `SearchCriterion` either as the **fourth parameter** when instantiating it, or by calling `setSearchQueryWriter()`.
|
||||
|
||||
If this value is set, then the (default Solr) Adapter will always use the provided `SearchQueryWriter`, rather than deciding for itself.
|
||||
|
||||
This should allow you to have full control over how your query strings are being generated if the default `SearchQueryWriter`s are not cutting it for you.
|
56
src/Search/Adapters/SearchAdapterInterface.php
Normal file
56
src/Search/Adapters/SearchAdapterInterface.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\FullTextSearch\Search\Adapters;
|
||||
|
||||
use SilverStripe\FullTextSearch\Search\Criteria\SearchCriterion;
|
||||
|
||||
/**
|
||||
* Interface SearchAdapterInterface
|
||||
* @package SilverStripe\FullTextSearch\Adapters
|
||||
*/
|
||||
interface SearchAdapterInterface
|
||||
{
|
||||
/**
|
||||
* Parameter $conjunction should be CONJUNCTION_AND or CONJUNCTION_OR, and your Adapter should return the
|
||||
* appropriate string representation of that conjunction.
|
||||
*
|
||||
* @param string $conjunction
|
||||
* @return string
|
||||
*/
|
||||
public function getConjunctionFor($conjunction);
|
||||
|
||||
/**
|
||||
* Due to the fact that we have filter criteria coming from legacy methods (as well as our Criteria), you may find
|
||||
* that you need to prepend (or append) something to your group of Criteria statements.
|
||||
*
|
||||
* EG: For Solr, we need to add a "+" between the default filters, and our Criteria.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPrependToCriteriaComponent();
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getAppendToCriteriaComponent();
|
||||
|
||||
/**
|
||||
* Define how each of your comparisons should be contained.
|
||||
*
|
||||
* EG: For Solr, we wrap each comparison in ().
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getOpenComparisonContainer();
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCloseComparisonContainer();
|
||||
|
||||
/**
|
||||
* @param SearchCriterion $criterion
|
||||
* @return string
|
||||
*/
|
||||
public function generateQueryString(SearchCriterion $criterion);
|
||||
}
|
115
src/Search/Adapters/SolrSearchAdapter.php
Normal file
115
src/Search/Adapters/SolrSearchAdapter.php
Normal file
@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\FullTextSearch\Search\Adapters;
|
||||
|
||||
use SilverStripe\FullTextSearch\Search\Criteria\SearchCriteria;
|
||||
use SilverStripe\FullTextSearch\Search\Criteria\SearchCriterion;
|
||||
use SilverStripe\FullTextSearch\Search\Queries\AbstractSearchQueryWriter;
|
||||
use SilverStripe\FullTextSearch\Solr\Writers\SolrSearchQueryWriterBasic;
|
||||
use SilverStripe\FullTextSearch\Solr\Writers\SolrSearchQueryWriterIn;
|
||||
use SilverStripe\FullTextSearch\Solr\Writers\SolrSearchQueryWriterRange;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class SolrSearchAdapter
|
||||
* @package SilverStripe\FullTextSearch\Search\Adapters
|
||||
*/
|
||||
class SolrSearchAdapter implements SearchAdapterInterface
|
||||
{
|
||||
/**
|
||||
* @param SearchCriterion $criterion
|
||||
* @return string
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function generateQueryString(SearchCriterion $criterion)
|
||||
{
|
||||
$writer = $this->getSearchQueryWriter($criterion);
|
||||
|
||||
return $writer->generateQueryString($criterion);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $conjunction
|
||||
* @return string
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function getConjunctionFor($conjunction)
|
||||
{
|
||||
switch ($conjunction) {
|
||||
case SearchCriteria::CONJUNCTION_AND:
|
||||
case SearchCriteria::CONJUNCTION_OR:
|
||||
return sprintf(' %s ', $conjunction);
|
||||
default:
|
||||
throw new InvalidArgumentException(
|
||||
sprintf('Invalid conjunction supplied to SolrSearchAdapter: "%s".', $conjunction)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPrependToCriteriaComponent()
|
||||
{
|
||||
return '+';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getAppendToCriteriaComponent()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getOpenComparisonContainer()
|
||||
{
|
||||
return '(';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCloseComparisonContainer()
|
||||
{
|
||||
return ')';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SearchCriterion $searchCriterion
|
||||
* @return AbstractSearchQueryWriter
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function getSearchQueryWriter(SearchCriterion $searchCriterion)
|
||||
{
|
||||
if ($searchCriterion->getSearchQueryWriter() instanceof AbstractSearchQueryWriter) {
|
||||
// The user has defined their own SearchQueryWriter, so we should just return it.
|
||||
return $searchCriterion->getSearchQueryWriter();
|
||||
}
|
||||
|
||||
switch ($searchCriterion->getComparison()) {
|
||||
case SearchCriterion::EQUAL:
|
||||
case SearchCriterion::NOT_EQUAL:
|
||||
return SolrSearchQueryWriterBasic::create();
|
||||
case SearchCriterion::IN:
|
||||
case SearchCriterion::NOT_IN:
|
||||
return SolrSearchQueryWriterIn::create();
|
||||
case SearchCriterion::GREATER_EQUAL:
|
||||
case SearchCriterion::GREATER_THAN:
|
||||
case SearchCriterion::LESS_EQUAL:
|
||||
case SearchCriterion::LESS_THAN:
|
||||
case SearchCriterion::ISNULL:
|
||||
case SearchCriterion::ISNOTNULL:
|
||||
return SolrSearchQueryWriterRange::create();
|
||||
case SearchCriterion::CUSTOM:
|
||||
// CUSTOM requires a SearchQueryWriter be provided. One can't have been provided, or it would have been
|
||||
// picked up at the top of the method.
|
||||
throw new InvalidArgumentException('SearchQueryWriter undefined or unsupported in SearchCriterion');
|
||||
default:
|
||||
throw new InvalidArgumentException('Unsupported comparison type in SolrSearchAdapter');
|
||||
}
|
||||
}
|
||||
}
|
239
src/Search/Criteria/SearchCriteria.php
Normal file
239
src/Search/Criteria/SearchCriteria.php
Normal file
@ -0,0 +1,239 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\FullTextSearch\Search\Criteria;
|
||||
|
||||
use SilverStripe\FullTextSearch\Search\Adapters\SearchAdapterInterface;
|
||||
use SilverStripe\FullTextSearch\Search\Queries\AbstractSearchQueryWriter;
|
||||
|
||||
/**
|
||||
* Class SearchCriteria
|
||||
* @package SilverStripe\FullTextSearch\Criteria
|
||||
*/
|
||||
class SearchCriteria implements SearchCriteriaInterface
|
||||
{
|
||||
/**
|
||||
* @param string
|
||||
*/
|
||||
const CONJUNCTION_AND = 'AND';
|
||||
|
||||
/**
|
||||
* @param string
|
||||
*/
|
||||
const CONJUNCTION_OR = 'OR';
|
||||
|
||||
/**
|
||||
* A collection of SearchCriterion and SearchCriteria.
|
||||
*
|
||||
* @var SearchCriteriaInterface[]
|
||||
*/
|
||||
protected $clauses = array();
|
||||
|
||||
/**
|
||||
* The conjunctions used between Criteria (AND/OR).
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $conjunctions = array();
|
||||
|
||||
/**
|
||||
* @var SearchAdapterInterface|null
|
||||
*/
|
||||
protected $adapter = null;
|
||||
|
||||
/**
|
||||
* You can pass through a string value, Criteria object, or Criterion object for $target.
|
||||
*
|
||||
* String value might be "SiteTree_Title" or whatever field in your index that you're trying to target.
|
||||
*
|
||||
* If you require complex filtering then you can build your Criteria object first with multiple layers/levels of
|
||||
* Criteria, and then pass it in here when you're ready.
|
||||
*
|
||||
* If you have your own Criterion object that you've created that you want to use, you can also pass that in here.
|
||||
*
|
||||
* @param string|SearchCriterion $target
|
||||
* @param mixed $value
|
||||
* @param string|null $comparison
|
||||
* @param AbstractSearchQueryWriter $searchQueryWriter
|
||||
*/
|
||||
public function __construct(
|
||||
$target,
|
||||
$value = null,
|
||||
$comparison = null,
|
||||
AbstractSearchQueryWriter $searchQueryWriter = null
|
||||
) {
|
||||
$this->addClause($this->getCriterionForCondition($target, $value, $comparison, $searchQueryWriter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Static create method provided so that you can perform method chaining.
|
||||
*
|
||||
* @param $target
|
||||
* @param null $value
|
||||
* @param null $comparison
|
||||
* @param AbstractSearchQueryWriter $searchQueryWriter
|
||||
* @return SearchCriteria
|
||||
*/
|
||||
public static function create(
|
||||
$target,
|
||||
$value = null,
|
||||
$comparison = null,
|
||||
AbstractSearchQueryWriter $searchQueryWriter = null
|
||||
) {
|
||||
return new SearchCriteria($target, $value, $comparison, $searchQueryWriter);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null|SearchAdapterInterface
|
||||
*/
|
||||
public function getAdapter()
|
||||
{
|
||||
return $this->adapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SearchAdapterInterface $adapter
|
||||
* @return $this
|
||||
*/
|
||||
public function setAdapter(SearchAdapterInterface $adapter)
|
||||
{
|
||||
$this->adapter = $adapter;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $ps Current prepared statement.
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function appendPreparedStatementTo(&$ps)
|
||||
{
|
||||
$adapter = $this->getAdapter();
|
||||
|
||||
if (!$adapter instanceof SearchAdapterInterface) {
|
||||
throw new \Exception('No adapter has been applied to SearchCriteria');
|
||||
}
|
||||
|
||||
$ps .= $adapter->getOpenComparisonContainer();
|
||||
|
||||
foreach ($this->getClauses() as $key => $clause) {
|
||||
$clause->setAdapter($adapter);
|
||||
$clause->appendPreparedStatementTo($ps);
|
||||
|
||||
// There's always one less conjunction then there are clauses.
|
||||
if ($this->getConjunction($key) !== null) {
|
||||
$ps .= $adapter->getConjunctionFor($this->getConjunction($key));
|
||||
}
|
||||
}
|
||||
|
||||
$ps .= $adapter->getCloseComparisonContainer();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|SearchCriteriaInterface $target
|
||||
* @param mixed $value
|
||||
* @param string|null $comparison
|
||||
* @param AbstractSearchQueryWriter $searchQueryWriter
|
||||
* @return $this
|
||||
*/
|
||||
public function addAnd(
|
||||
$target,
|
||||
$value = null,
|
||||
$comparison = null,
|
||||
AbstractSearchQueryWriter $searchQueryWriter = null
|
||||
) {
|
||||
$criterion = $this->getCriterionForCondition($target, $value, $comparison, $searchQueryWriter);
|
||||
|
||||
$this->addConjunction(SearchCriteria::CONJUNCTION_AND);
|
||||
$this->addClause($criterion);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|SearchCriteriaInterface $target
|
||||
* @param mixed $value
|
||||
* @param string|null $comparison
|
||||
* @param AbstractSearchQueryWriter $searchQueryWriter
|
||||
* @return $this
|
||||
*/
|
||||
public function addOr(
|
||||
$target,
|
||||
$value = null,
|
||||
$comparison = null,
|
||||
AbstractSearchQueryWriter $searchQueryWriter = null
|
||||
) {
|
||||
$criterion = $this->getCriterionForCondition($target, $value, $comparison, $searchQueryWriter);
|
||||
|
||||
$this->addConjunction(SearchCriteria::CONJUNCTION_OR);
|
||||
$this->addClause($criterion);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|SearchCriteriaInterface $target
|
||||
* @param mixed $value
|
||||
* @param string $comparison
|
||||
* @param AbstractSearchQueryWriter $searchQueryWriter
|
||||
* @return SearchCriteriaInterface
|
||||
*/
|
||||
protected function getCriterionForCondition(
|
||||
$target,
|
||||
$value,
|
||||
$comparison,
|
||||
AbstractSearchQueryWriter $searchQueryWriter = null
|
||||
) {
|
||||
if ($target instanceof SearchCriteriaInterface) {
|
||||
return $target;
|
||||
}
|
||||
|
||||
return new SearchCriterion($target, $value, $comparison, $searchQueryWriter);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return SearchCriteriaInterface[]
|
||||
*/
|
||||
protected function getClauses()
|
||||
{
|
||||
return $this->clauses;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SearchCriteriaInterface $criterion
|
||||
*/
|
||||
protected function addClause($criterion)
|
||||
{
|
||||
$this->clauses[] = $criterion;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
protected function getConjunctions()
|
||||
{
|
||||
return $this->conjunctions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $key
|
||||
* @return string|null
|
||||
*/
|
||||
protected function getConjunction($key)
|
||||
{
|
||||
$conjunctions = $this->getConjunctions();
|
||||
if (!array_key_exists($key, $conjunctions)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $conjunctions[$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $conjunction
|
||||
*/
|
||||
protected function addConjunction($conjunction)
|
||||
{
|
||||
$this->conjunctions[] = $conjunction;
|
||||
}
|
||||
}
|
36
src/Search/Criteria/SearchCriteriaInterface.php
Normal file
36
src/Search/Criteria/SearchCriteriaInterface.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\FullTextSearch\Search\Criteria;
|
||||
|
||||
use SilverStripe\FullTextSearch\Search\Adapters\SearchAdapterInterface;
|
||||
|
||||
/**
|
||||
* Interface SearchCriteriaInterface
|
||||
*
|
||||
* SearchCriteria and SearchCriterion objects must implement this interface.
|
||||
*/
|
||||
interface SearchCriteriaInterface
|
||||
{
|
||||
/**
|
||||
* The method used in all SearchCriterion to generate and append their filter query statements.
|
||||
*
|
||||
* This is also used in SearchCriteria to loop through it's collected SearchCriterion and append the above. This
|
||||
* allows us to have SearchCriteria and SearchCriterion in the same collections (allowing us to have complex nested
|
||||
* filtering).
|
||||
*
|
||||
* @param $ps
|
||||
* @return void
|
||||
*/
|
||||
public function appendPreparedStatementTo(&$ps);
|
||||
|
||||
/**
|
||||
* @return SearchAdapterInterface
|
||||
*/
|
||||
public function getAdapter();
|
||||
|
||||
/**
|
||||
* @param SearchAdapterInterface $adapter
|
||||
* @return $this
|
||||
*/
|
||||
public function setAdapter(SearchAdapterInterface $adapter);
|
||||
}
|
269
src/Search/Criteria/SearchCriterion.php
Normal file
269
src/Search/Criteria/SearchCriterion.php
Normal file
@ -0,0 +1,269 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\FullTextSearch\Search\Criteria;
|
||||
|
||||
use SilverStripe\Core\Injector\Injectable;
|
||||
use SilverStripe\FullTextSearch\Search\Adapters\SearchAdapterInterface;
|
||||
use SilverStripe\FullTextSearch\Search\Queries\AbstractSearchQueryWriter;
|
||||
|
||||
/**
|
||||
* Class SearchCriterion
|
||||
* @package SilverStripe\FullTextSearch\Search\Criteria
|
||||
*/
|
||||
class SearchCriterion implements SearchCriteriaInterface
|
||||
{
|
||||
use Injectable;
|
||||
|
||||
/**
|
||||
* field:value
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const EQUAL = 'EQUAL';
|
||||
|
||||
/**
|
||||
* -field:value
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const NOT_EQUAL = 'NOT_EQUAL';
|
||||
|
||||
/**
|
||||
* field:[value TO *]
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const GREATER_EQUAL = 'GREATER_EQUAL';
|
||||
|
||||
/**
|
||||
* field:{value TO *}
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const GREATER_THAN = 'GREATER_THAN';
|
||||
|
||||
/**
|
||||
* field:[* TO value]
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const LESS_EQUAL = 'LESS_EQUAL';
|
||||
|
||||
/**
|
||||
* field:{* TO value}
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const LESS_THAN = 'LESS_THAN';
|
||||
|
||||
/**
|
||||
* (field:value1 field:value2 field:value3)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const IN = 'IN';
|
||||
|
||||
/**
|
||||
* -(field:value1 field:value2 field:value3)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const NOT_IN = 'NOT_IN';
|
||||
|
||||
/**
|
||||
* field:[* TO *]
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const ISNULL = 'ISNULL';
|
||||
|
||||
/**
|
||||
* -field:[* TO *]
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const ISNOTNULL = 'ISNOTNULL';
|
||||
|
||||
/**
|
||||
* A custom Criterion with it's own SearchQueryWriter
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const CUSTOM = 'CUSTOM';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $comparison;
|
||||
|
||||
/**
|
||||
* The table and field that this Criterion is applied to.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $target;
|
||||
|
||||
/**
|
||||
* @var mixed
|
||||
*/
|
||||
protected $value;
|
||||
|
||||
/**
|
||||
* @var SearchAdapterInterface
|
||||
*/
|
||||
protected $adapter;
|
||||
|
||||
/**
|
||||
* @var AbstractSearchQueryWriter
|
||||
*/
|
||||
protected $searchQueryWriter;
|
||||
|
||||
/**
|
||||
* @param string $target
|
||||
* @param string|array $value
|
||||
* @param string|null $comparison
|
||||
* @param AbstractSearchQueryWriter $searchQueryWriter
|
||||
*/
|
||||
public function __construct(
|
||||
$target,
|
||||
$value,
|
||||
$comparison = null,
|
||||
AbstractSearchQueryWriter $searchQueryWriter = null
|
||||
) {
|
||||
// EQUAL is our default comparison.
|
||||
if ($comparison === null) {
|
||||
$comparison = SearchCriterion::EQUAL;
|
||||
}
|
||||
|
||||
$this->setTarget($target);
|
||||
$this->setValue($value);
|
||||
$this->setComparison($comparison);
|
||||
$this->setSearchQueryWriter($searchQueryWriter);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return SearchAdapterInterface
|
||||
*/
|
||||
public function getAdapter()
|
||||
{
|
||||
return $this->adapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SearchAdapterInterface $adapter
|
||||
* @return $this
|
||||
*/
|
||||
public function setAdapter(SearchAdapterInterface $adapter)
|
||||
{
|
||||
$this->adapter = $adapter;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $ps
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function appendPreparedStatementTo(&$ps)
|
||||
{
|
||||
$adapter = $this->getAdapter();
|
||||
|
||||
if (!$adapter instanceof SearchAdapterInterface) {
|
||||
throw new \Exception('No adapter has been applied to SearchCriteria');
|
||||
}
|
||||
|
||||
$ps .= $adapter->generateQueryString($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* String values should be passed into our filter string with quotation marks and escaping.
|
||||
*
|
||||
* @param string $value
|
||||
* @return string
|
||||
*/
|
||||
public function getQuoteValue($value)
|
||||
{
|
||||
if (is_string($value)) {
|
||||
return sprintf('"%s"', $value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AbstractSearchQueryWriter
|
||||
*/
|
||||
public function getSearchQueryWriter()
|
||||
{
|
||||
return $this->searchQueryWriter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AbstractSearchQueryWriter $searchQueryWriter
|
||||
* @return $this
|
||||
*/
|
||||
public function setSearchQueryWriter($searchQueryWriter)
|
||||
{
|
||||
$this->searchQueryWriter = $searchQueryWriter;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getComparison()
|
||||
{
|
||||
return $this->comparison;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $comparison
|
||||
* @return $this
|
||||
*/
|
||||
protected function setComparison($comparison)
|
||||
{
|
||||
$this->comparison = $comparison;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTarget()
|
||||
{
|
||||
return $this->target;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $target
|
||||
* @return $this
|
||||
*/
|
||||
protected function setTarget($target)
|
||||
{
|
||||
$this->target = $target;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|array
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|array $value
|
||||
* @return $this
|
||||
*/
|
||||
protected function setValue($value)
|
||||
{
|
||||
$this->value = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
21
src/Search/Queries/AbstractSearchQueryWriter.php
Normal file
21
src/Search/Queries/AbstractSearchQueryWriter.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\FullTextSearch\Search\Queries;
|
||||
|
||||
use SilverStripe\Core\Injector\Injectable;
|
||||
use SilverStripe\FullTextSearch\Search\Criteria\SearchCriterion;
|
||||
|
||||
/**
|
||||
* Class AbstractSearchQueryWriter
|
||||
* @package SilverStripe\FullTextSearch\Search\Queries
|
||||
*/
|
||||
abstract class AbstractSearchQueryWriter
|
||||
{
|
||||
use Injectable;
|
||||
|
||||
/**
|
||||
* @param SearchCriterion $searchCriterion
|
||||
* @return string
|
||||
*/
|
||||
abstract public function generateQueryString(SearchCriterion $searchCriterion);
|
||||
}
|
@ -3,6 +3,9 @@
|
||||
namespace SilverStripe\FullTextSearch\Search\Queries;
|
||||
|
||||
use SilverStripe\Dev\Deprecation;
|
||||
use SilverStripe\FullTextSearch\Search\Adapters\SearchAdapterInterface;
|
||||
use SilverStripe\FullTextSearch\Search\Criteria\SearchCriteria;
|
||||
use SilverStripe\FullTextSearch\Search\Criteria\SearchCriteriaInterface;
|
||||
use SilverStripe\View\ViewableData;
|
||||
use stdClass;
|
||||
|
||||
@ -27,11 +30,25 @@ class SearchQuery extends ViewableData
|
||||
public $require = [];
|
||||
public $exclude = [];
|
||||
|
||||
/**
|
||||
* @var SearchCriteriaInterface[]
|
||||
*/
|
||||
public $criteria = [];
|
||||
|
||||
protected $start = 0;
|
||||
protected $limit = -1;
|
||||
|
||||
/**
|
||||
* @var SearchAdapterInterface
|
||||
*/
|
||||
protected $adapter = null;
|
||||
|
||||
/** These are the API functions */
|
||||
|
||||
/**
|
||||
* SearchQuery constructor.
|
||||
* @throws \Psr\Container\NotFoundExceptionInterface
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
if (self::$missing === null) {
|
||||
@ -42,12 +59,24 @@ class SearchQuery extends ViewableData
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SearchAdapterInterface $adapter
|
||||
* @return SearchQuery
|
||||
*/
|
||||
public function setHandler(SearchAdapterInterface $adapter)
|
||||
{
|
||||
$this->adapter = $adapter;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $text Search terms. Exact format (grouping, boolean expressions, etc.) depends on
|
||||
* the search implementation.
|
||||
* @param array $fields Limits the search to specific fields (using composite field names)
|
||||
* @param array $boost Map of composite field names to float values. The higher the value,
|
||||
* the more important the field gets for relevancy.
|
||||
* @return $this
|
||||
*/
|
||||
public function addSearchTerm($text, $fields = null, $boost = [])
|
||||
{
|
||||
@ -68,6 +97,7 @@ class SearchQuery extends ViewableData
|
||||
* @param string $text See {@link addSearchTerm()}
|
||||
* @param array $fields See {@link addSearchTerm()}
|
||||
* @param array $boost See {@link addSearchTerm()}
|
||||
* @return $this
|
||||
*/
|
||||
public function addFuzzySearchTerm($text, $fields = null, $boost = [])
|
||||
{
|
||||
@ -116,6 +146,7 @@ class SearchQuery extends ViewableData
|
||||
*
|
||||
* @param string $field Composite name of the field
|
||||
* @param mixed $values Scalar value, array of values, or an instance of SearchQuery_Range
|
||||
* @return $this
|
||||
*/
|
||||
public function addFilter($field, $values)
|
||||
{
|
||||
@ -138,6 +169,7 @@ class SearchQuery extends ViewableData
|
||||
*
|
||||
* @param string $field
|
||||
* @param mixed $values
|
||||
* @return $this
|
||||
*/
|
||||
public function addExclude($field, $values)
|
||||
{
|
||||
@ -155,6 +187,37 @@ class SearchQuery extends ViewableData
|
||||
return $this->exclude;
|
||||
}
|
||||
|
||||
/**
|
||||
* You can pass through a string value, Criteria object, or Criterion object for $target.
|
||||
*
|
||||
* String value might be "SiteTree_Title" or whatever field in your index that you're trying to target.
|
||||
*
|
||||
* If you require complex filtering then you can build your Criteria object first with multiple layers/levels of
|
||||
* Criteria, and then pass it in here when you're ready.
|
||||
*
|
||||
* If you have your own Criterion object that you've created that you want to use, you can also pass that in here.
|
||||
*
|
||||
* @param string|SearchCriteriaInterface $target
|
||||
* @param mixed $value
|
||||
* @param string|null $comparison
|
||||
* @param AbstractSearchQueryWriter $searchQueryWriter
|
||||
* @return SearchCriteriaInterface
|
||||
*/
|
||||
public function filterBy(
|
||||
$target,
|
||||
$value = null,
|
||||
$comparison = null,
|
||||
AbstractSearchQueryWriter $searchQueryWriter = null
|
||||
) {
|
||||
if (!$target instanceof SearchCriteriaInterface) {
|
||||
$target = new SearchCriteria($target, $value, $comparison, $searchQueryWriter);
|
||||
}
|
||||
|
||||
$this->addCriteria($target);
|
||||
|
||||
return $target;
|
||||
}
|
||||
|
||||
public function setStart($start)
|
||||
{
|
||||
$this->start = $start;
|
||||
@ -206,6 +269,14 @@ class SearchQuery extends ViewableData
|
||||
return $this->search || $this->classes || $this->require || $this->exclude;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return SearchAdapterInterface
|
||||
*/
|
||||
public function getAdapter()
|
||||
{
|
||||
return $this->adapter;
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return "Search Query\n";
|
||||
@ -290,4 +361,32 @@ class SearchQuery extends ViewableData
|
||||
Deprecation::notice('4.0', 'Use setPageSize() instead');
|
||||
return $this->setPageSize($page);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return SearchCriteriaInterface[]
|
||||
*/
|
||||
public function getCriteria()
|
||||
{
|
||||
return $this->criteria;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SearchCriteriaInterface[] $criteria
|
||||
* @return SearchQuery
|
||||
*/
|
||||
public function setCriteria($criteria)
|
||||
{
|
||||
$this->criteria = $criteria;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SearchCriteriaInterface $criteria
|
||||
* @return SearchQuery
|
||||
*/
|
||||
public function addCriteria($criteria)
|
||||
{
|
||||
$this->criteria[] = $criteria;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
@ -632,6 +632,8 @@ abstract class SolrIndex extends SearchIndex
|
||||
static::warn($e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -643,6 +645,7 @@ abstract class SolrIndex extends SearchIndex
|
||||
*
|
||||
* @param array $classes List of non-obsolete classes in the same format as SolrIndex::getClasses()
|
||||
* @return bool Flag if successful
|
||||
* @throws \Apache_Solr_HttpTransportException
|
||||
*/
|
||||
public function clearObsoleteClasses($classes)
|
||||
{
|
||||
@ -676,6 +679,8 @@ abstract class SolrIndex extends SearchIndex
|
||||
static::warn($e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -685,6 +690,8 @@ abstract class SolrIndex extends SearchIndex
|
||||
* @param array $params Extra request parameters passed through to Solr
|
||||
* @return ArrayData Map with the following keys:
|
||||
* - 'Matches': ArrayList of the matched object instances
|
||||
* @throws \Apache_Solr_HttpTransportException
|
||||
* @throws \Apache_Solr_InvalidArgumentException
|
||||
*/
|
||||
public function search(SearchQuery $query, $offset = -1, $limit = -1, $params = array())
|
||||
{
|
||||
@ -989,18 +996,57 @@ abstract class SolrIndex extends SearchIndex
|
||||
return $fq;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SearchQuery $searchQuery
|
||||
* @return string
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function getCriteriaComponent(SearchQuery $searchQuery)
|
||||
{
|
||||
if (count($searchQuery->getCriteria()) === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($searchQuery->getAdapter() === null) {
|
||||
throw new \Exception('SearchQuery does not have a SearchAdapter applied');
|
||||
}
|
||||
|
||||
// Need to start with a positive conjunction.
|
||||
$ps = $searchQuery->getAdapter()->getPrependToCriteriaComponent();
|
||||
|
||||
foreach ($searchQuery->getCriteria() as $clause) {
|
||||
$clause->setAdapter($searchQuery->getAdapter());
|
||||
$clause->appendPreparedStatementTo($ps);
|
||||
}
|
||||
|
||||
// Need to start with a positive conjunction.
|
||||
$ps .= $searchQuery->getAdapter()->getAppendToCriteriaComponent();
|
||||
|
||||
// Returned as an array because that's how `getFiltersComponent` expects it.
|
||||
return $ps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all filter conditions for this search
|
||||
*
|
||||
* @param SearchQuery $searchQuery
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getFiltersComponent(SearchQuery $searchQuery)
|
||||
{
|
||||
return array_merge(
|
||||
$criteriaComponent = $this->getCriteriaComponent($searchQuery);
|
||||
|
||||
$components = array_merge(
|
||||
$this->getRequireFiltersComponent($searchQuery),
|
||||
$this->getExcludeFiltersComponent($searchQuery)
|
||||
);
|
||||
|
||||
if ($criteriaComponent !== null) {
|
||||
$components[] = $criteriaComponent;
|
||||
}
|
||||
|
||||
return $components;
|
||||
}
|
||||
|
||||
protected $service;
|
||||
|
54
src/Solr/Writers/SolrSearchQueryWriterBasic.php
Normal file
54
src/Solr/Writers/SolrSearchQueryWriterBasic.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\FullTextSearch\Solr\Writers;
|
||||
|
||||
use SilverStripe\FullTextSearch\Search\Criteria\SearchCriterion;
|
||||
use SilverStripe\FullTextSearch\Search\Queries\AbstractSearchQueryWriter;
|
||||
|
||||
/**
|
||||
* Class SolrSearchQueryWriter_Basic
|
||||
* @package SilverStripe\FullTextSearch\Solr\Writers
|
||||
*/
|
||||
class SolrSearchQueryWriterBasic extends AbstractSearchQueryWriter
|
||||
{
|
||||
/**
|
||||
* @var SearchCriterion $searchCriterion
|
||||
* @return string
|
||||
*/
|
||||
public function generateQueryString(SearchCriterion $searchCriterion)
|
||||
{
|
||||
return sprintf(
|
||||
'%s(%s%s%s)',
|
||||
$this->getComparisonPolarity($searchCriterion->getComparison()),
|
||||
addslashes($searchCriterion->getTarget()),
|
||||
$this->getComparisonConjunction(),
|
||||
$searchCriterion->getQuoteValue($searchCriterion->getValue())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this a positive (+) or negative (-) Solr comparison.
|
||||
*
|
||||
* @param string $comparison
|
||||
* @return string
|
||||
*/
|
||||
protected function getComparisonPolarity($comparison)
|
||||
{
|
||||
switch ($comparison) {
|
||||
case SearchCriterion::NOT_EQUAL:
|
||||
return '-';
|
||||
default:
|
||||
return '+';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide how we are comparing our left and right values.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getComparisonConjunction()
|
||||
{
|
||||
return ':';
|
||||
}
|
||||
}
|
86
src/Solr/Writers/SolrSearchQueryWriterIn.php
Normal file
86
src/Solr/Writers/SolrSearchQueryWriterIn.php
Normal file
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\FullTextSearch\Solr\Writers;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use SilverStripe\FullTextSearch\Search\Criteria\SearchCriterion;
|
||||
use SilverStripe\FullTextSearch\Search\Queries\AbstractSearchQueryWriter;
|
||||
|
||||
/**
|
||||
* Class SolrSearchQueryWriter_In
|
||||
* @package SilverStripe\FullTextSearch\Solr\Writers
|
||||
*/
|
||||
class SolrSearchQueryWriterIn extends AbstractSearchQueryWriter
|
||||
{
|
||||
/**
|
||||
* @param SearchCriterion $searchCriterion
|
||||
* @return string
|
||||
*/
|
||||
public function generateQueryString(SearchCriterion $searchCriterion)
|
||||
{
|
||||
return sprintf(
|
||||
'%s%s',
|
||||
$this->getComparisonPolarity($searchCriterion->getComparison()),
|
||||
$this->getInComparisonString($searchCriterion)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this a positive (+) or negative (-) Solr comparison.
|
||||
*
|
||||
* @param string $comparison
|
||||
* @return string
|
||||
*/
|
||||
protected function getComparisonPolarity($comparison)
|
||||
{
|
||||
switch ($comparison) {
|
||||
case SearchCriterion::NOT_IN:
|
||||
return '-';
|
||||
default:
|
||||
return '+';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SearchCriterion $searchCriterion
|
||||
* @return string
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function getInComparisonString(SearchCriterion $searchCriterion)
|
||||
{
|
||||
$conditions = array();
|
||||
|
||||
if (!is_array($searchCriterion->getValue())) {
|
||||
throw new InvalidArgumentException('Invalid value type for Criterion IN');
|
||||
}
|
||||
|
||||
foreach ($searchCriterion->getValue() as $value) {
|
||||
if (is_string($value)) {
|
||||
// String values need to be wrapped in quotes and escaped.
|
||||
$value = $searchCriterion->getQuoteValue($value);
|
||||
}
|
||||
|
||||
$conditions[] = sprintf(
|
||||
'%s%s%s',
|
||||
addslashes($searchCriterion->getTarget()),
|
||||
$this->getComparisonConjunction(),
|
||||
$value
|
||||
);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'(%s)',
|
||||
implode(' ', $conditions)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide how we are comparing our left and right values.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getComparisonConjunction()
|
||||
{
|
||||
return ':';
|
||||
}
|
||||
}
|
150
src/Solr/Writers/SolrSearchQueryWriterRange.php
Normal file
150
src/Solr/Writers/SolrSearchQueryWriterRange.php
Normal file
@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\FullTextSearch\Solr\Writers;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use SilverStripe\FullTextSearch\Search\Criteria\SearchCriterion;
|
||||
use SilverStripe\FullTextSearch\Search\Queries\AbstractSearchQueryWriter;
|
||||
|
||||
/**
|
||||
* Class SolrSearchQueryWriter_Range
|
||||
* @package SilverStripe\FullTextSearch\Solr\Writers
|
||||
*/
|
||||
class SolrSearchQueryWriterRange extends AbstractSearchQueryWriter
|
||||
{
|
||||
/**
|
||||
* @param SearchCriterion $searchCriterion
|
||||
* @return string
|
||||
*/
|
||||
public function generateQueryString(SearchCriterion $searchCriterion)
|
||||
{
|
||||
return sprintf(
|
||||
'%s(%s:%s%s%s%s%s)',
|
||||
$this->getComparisonPolarity($searchCriterion->getComparison()),
|
||||
addslashes($searchCriterion->getTarget()),
|
||||
$this->getOpenComparisonContainer($searchCriterion->getComparison()),
|
||||
$this->getLeftComparison($searchCriterion),
|
||||
$this->getComparisonConjunction(),
|
||||
$this->getRightComparison($searchCriterion),
|
||||
$this->getCloseComparisonContainer($searchCriterion->getComparison())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this a positive (+) or negative (-) Solr comparison.
|
||||
*
|
||||
* @param string $comparison
|
||||
* @return string
|
||||
*/
|
||||
protected function getComparisonPolarity($comparison)
|
||||
{
|
||||
switch ($comparison) {
|
||||
case SearchCriterion::ISNULL:
|
||||
return '-';
|
||||
default:
|
||||
return '+';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the value that we want as our left comparison value.
|
||||
*
|
||||
* @param SearchCriterion $searchCriterion
|
||||
* @return mixed|string
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function getLeftComparison(SearchCriterion $searchCriterion)
|
||||
{
|
||||
switch ($searchCriterion->getComparison()) {
|
||||
case SearchCriterion::GREATER_EQUAL:
|
||||
case SearchCriterion::GREATER_THAN:
|
||||
return $searchCriterion->getValue();
|
||||
case SearchCriterion::ISNULL:
|
||||
case SearchCriterion::ISNOTNULL:
|
||||
case SearchCriterion::LESS_EQUAL:
|
||||
case SearchCriterion::LESS_THAN:
|
||||
return '*';
|
||||
default:
|
||||
throw new InvalidArgumentException('Invalid comparison for RangeCriterion');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the value that we want as our right comparison value.
|
||||
*
|
||||
* @param SearchCriterion $searchCriterion
|
||||
* @return mixed|string
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function getRightComparison(SearchCriterion $searchCriterion)
|
||||
{
|
||||
switch ($searchCriterion->getComparison()) {
|
||||
case SearchCriterion::GREATER_EQUAL:
|
||||
case SearchCriterion::GREATER_THAN:
|
||||
case SearchCriterion::ISNULL:
|
||||
case SearchCriterion::ISNOTNULL:
|
||||
return '*';
|
||||
case SearchCriterion::LESS_EQUAL:
|
||||
case SearchCriterion::LESS_THAN:
|
||||
return $searchCriterion->getValue();
|
||||
default:
|
||||
throw new InvalidArgumentException('Invalid comparison for RangeCriterion');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide how we are comparing our left and right values.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getComparisonConjunction()
|
||||
{
|
||||
return ' TO ';
|
||||
}
|
||||
|
||||
/**
|
||||
* Does our comparison need a container? EG: "[* TO *]"? If so, return the opening container brace.
|
||||
*
|
||||
* @param string $comparison
|
||||
* @return string
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function getOpenComparisonContainer($comparison)
|
||||
{
|
||||
switch ($comparison) {
|
||||
case SearchCriterion::GREATER_EQUAL:
|
||||
case SearchCriterion::LESS_EQUAL:
|
||||
case SearchCriterion::ISNULL:
|
||||
case SearchCriterion::ISNOTNULL:
|
||||
return '[';
|
||||
case SearchCriterion::GREATER_THAN:
|
||||
case SearchCriterion::LESS_THAN:
|
||||
return '{';
|
||||
default:
|
||||
throw new InvalidArgumentException('Invalid comparison for RangeCriterion');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Does our comparison need a container? EG: "[* TO *]"? If so, return the closing container brace.
|
||||
*
|
||||
* @param string $comparison
|
||||
* @return string
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function getCloseComparisonContainer($comparison)
|
||||
{
|
||||
switch ($comparison) {
|
||||
case SearchCriterion::GREATER_EQUAL:
|
||||
case SearchCriterion::LESS_EQUAL:
|
||||
case SearchCriterion::ISNULL:
|
||||
case SearchCriterion::ISNOTNULL:
|
||||
return ']';
|
||||
case SearchCriterion::GREATER_THAN:
|
||||
case SearchCriterion::LESS_THAN:
|
||||
return '}';
|
||||
default:
|
||||
throw new InvalidArgumentException('Invalid comparison for RangeCriterion');
|
||||
}
|
||||
}
|
||||
}
|
242
tests/SolrWritersTest.php
Normal file
242
tests/SolrWritersTest.php
Normal file
@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\FullTextSearch\Tests;
|
||||
|
||||
use \InvalidArgumentException;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\FullTextSearch\Search\Adapters\SolrSearchAdapter;
|
||||
use SilverStripe\FullTextSearch\Search\Criteria\SearchCriteria;
|
||||
use SilverStripe\FullTextSearch\Search\Criteria\SearchCriterion;
|
||||
use SilverStripe\FullTextSearch\Search\Queries\SearchQuery;
|
||||
use SilverStripe\FullTextSearch\Solr\Writers\SolrSearchQueryWriterBasic;
|
||||
use SilverStripe\FullTextSearch\Solr\Writers\SolrSearchQueryWriterIn;
|
||||
use SilverStripe\FullTextSearch\Solr\Writers\SolrSearchQueryWriterRange;
|
||||
use SilverStripe\FullTextSearch\Tests\SolrIndexTest\SolrIndexTest_FakeIndex;
|
||||
|
||||
/**
|
||||
* Class SolrWritersTest
|
||||
* @package SilverStripe\FullTextSearch\Tests
|
||||
*/
|
||||
class SolrWritersTest extends SapphireTest
|
||||
{
|
||||
public function testBasicEqualQueryString()
|
||||
{
|
||||
$criteria = new SearchCriterion('Title', 'Test', SearchCriterion::EQUAL);
|
||||
$writer = SolrSearchQueryWriterBasic::create();
|
||||
$expected = '+(Title:"Test")';
|
||||
|
||||
$this->assertEquals($expected, $writer->generateQueryString($criteria));
|
||||
}
|
||||
|
||||
public function testBasicNotEqualQueryString()
|
||||
{
|
||||
$criteria = new SearchCriterion('Title', 'Test', SearchCriterion::NOT_EQUAL);
|
||||
$writer = SolrSearchQueryWriterBasic::create();
|
||||
$expected = '-(Title:"Test")';
|
||||
|
||||
$this->assertEquals($expected, $writer->generateQueryString($criteria));
|
||||
}
|
||||
|
||||
public function testBasicInQueryString()
|
||||
{
|
||||
$criteria = new SearchCriterion('ID', [1,2,3], SearchCriterion::IN);
|
||||
$writer = SolrSearchQueryWriterIn::create();
|
||||
$expected = '+(ID:1 ID:2 ID:3)';
|
||||
|
||||
$this->assertEquals($expected, $writer->generateQueryString($criteria));
|
||||
}
|
||||
|
||||
public function testBasicNotInQueryString()
|
||||
{
|
||||
$criteria = new SearchCriterion('ID', [1,2,3], SearchCriterion::NOT_IN);
|
||||
$writer = SolrSearchQueryWriterIn::create();
|
||||
$expected = '-(ID:1 ID:2 ID:3)';
|
||||
|
||||
$this->assertEquals($expected, $writer->generateQueryString($criteria));
|
||||
}
|
||||
|
||||
public function testBasicGreaterEqualQueryString()
|
||||
{
|
||||
$criteria = new SearchCriterion('Stock', 2, SearchCriterion::GREATER_EQUAL);
|
||||
$writer = SolrSearchQueryWriterRange::create();
|
||||
$expected = '+(Stock:[2 TO *])';
|
||||
|
||||
$this->assertEquals($expected, $writer->generateQueryString($criteria));
|
||||
}
|
||||
|
||||
public function testBasicGreaterQueryString()
|
||||
{
|
||||
$criteria = new SearchCriterion('Stock', 2, SearchCriterion::GREATER_THAN);
|
||||
$writer = SolrSearchQueryWriterRange::create();
|
||||
$expected = '+(Stock:{2 TO *})';
|
||||
|
||||
$this->assertEquals($expected, $writer->generateQueryString($criteria));
|
||||
}
|
||||
|
||||
public function testBasicLessEqualQueryString()
|
||||
{
|
||||
$criteria = new SearchCriterion('Stock', 2, SearchCriterion::LESS_EQUAL);
|
||||
$writer = SolrSearchQueryWriterRange::create();
|
||||
$expected = '+(Stock:[* TO 2])';
|
||||
|
||||
$this->assertEquals($expected, $writer->generateQueryString($criteria));
|
||||
}
|
||||
|
||||
public function testBasicLessQueryString()
|
||||
{
|
||||
$criteria = new SearchCriterion('Stock', 2, SearchCriterion::LESS_THAN);
|
||||
$writer = SolrSearchQueryWriterRange::create();
|
||||
$expected = '+(Stock:{* TO 2})';
|
||||
|
||||
$this->assertEquals($expected, $writer->generateQueryString($criteria));
|
||||
}
|
||||
|
||||
public function testBasicIsNullQueryString()
|
||||
{
|
||||
$criteria = new SearchCriterion('Stock', null, SearchCriterion::ISNULL);
|
||||
$writer = SolrSearchQueryWriterRange::create();
|
||||
$expected = '-(Stock:[* TO *])';
|
||||
|
||||
$this->assertEquals($expected, $writer->generateQueryString($criteria));
|
||||
}
|
||||
|
||||
public function testBasicIsNotNullQueryString()
|
||||
{
|
||||
$criteria = new SearchCriterion('Stock', null, SearchCriterion::ISNOTNULL);
|
||||
$writer = SolrSearchQueryWriterRange::create();
|
||||
$expected = '+(Stock:[* TO *])';
|
||||
|
||||
$this->assertEquals($expected, $writer->generateQueryString($criteria));
|
||||
}
|
||||
|
||||
public function testConjunction()
|
||||
{
|
||||
$adapter = new SolrSearchAdapter();
|
||||
|
||||
$this->assertEquals(' AND ', $adapter->getConjunctionFor(SearchCriteria::CONJUNCTION_AND));
|
||||
$this->assertEquals(' OR ', $adapter->getConjunctionFor(SearchCriteria::CONJUNCTION_OR));
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException InvalidArgumentException
|
||||
*/
|
||||
public function testConjunctionFailure()
|
||||
{
|
||||
$adapter = new SolrSearchAdapter();
|
||||
$adapter->getConjunctionFor('FAIL');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testComplexPositiveFilterQueryString()
|
||||
{
|
||||
$expected = '+((+(Page_TaxonomyTerms_ID:"Lego") AND +(Page_TaxonomyTerms_ID:"StarWars") AND +(Stock:[5 TO *]))';
|
||||
$expected .= ' OR (+(Page_TaxonomyTerms_ID:"Books") AND +(Page_TaxonomyTerms_ID:"HarryPotter")';
|
||||
$expected .= ' AND +(Stock:[1 TO *])))';
|
||||
|
||||
$legoCriteria = SearchCriteria::create(
|
||||
'Page_TaxonomyTerms_ID',
|
||||
[
|
||||
'Lego',
|
||||
],
|
||||
SearchCriterion::IN
|
||||
);
|
||||
|
||||
$legoCriteria->addAnd(
|
||||
'Page_TaxonomyTerms_ID',
|
||||
[
|
||||
'StarWars',
|
||||
],
|
||||
SearchCriterion::IN
|
||||
);
|
||||
|
||||
$legoCriteria->addAnd(
|
||||
'Stock',
|
||||
5,
|
||||
SearchCriterion::GREATER_EQUAL
|
||||
);
|
||||
|
||||
$booksCriteria = SearchCriteria::create(
|
||||
'Page_TaxonomyTerms_ID',
|
||||
[
|
||||
'Books',
|
||||
],
|
||||
SearchCriterion::IN
|
||||
);
|
||||
|
||||
$booksCriteria->addAnd(
|
||||
'Page_TaxonomyTerms_ID',
|
||||
[
|
||||
'HarryPotter',
|
||||
],
|
||||
SearchCriterion::IN
|
||||
);
|
||||
|
||||
$booksCriteria->addAnd(
|
||||
'Stock',
|
||||
1,
|
||||
SearchCriterion::GREATER_EQUAL
|
||||
);
|
||||
|
||||
// Combine the two criteria with an `OR` conjunction
|
||||
$criteria = SearchCriteria::create($legoCriteria)->addOr($booksCriteria);
|
||||
|
||||
$query = SearchQuery::create();
|
||||
$query->filterBy($criteria);
|
||||
|
||||
$index = new SolrIndexTest_FakeIndex();
|
||||
|
||||
$this->assertTrue(in_array($expected, $index->getFiltersComponent($query)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testComplexNegativeFilterQueryString()
|
||||
{
|
||||
$expected = '+((-(Page_TaxonomyTerms_ID:"Lego" Page_TaxonomyTerms_ID:"StarWars") AND +(Stock:[* TO 5]))';
|
||||
$expected .= ' OR (-(Page_TaxonomyTerms_ID:"Books" Page_TaxonomyTerms_ID:"HarryPotter")';
|
||||
$expected .= ' AND +(Stock:[* TO 2])))';
|
||||
|
||||
$legoCriteria = SearchCriteria::create(
|
||||
'Page_TaxonomyTerms_ID',
|
||||
[
|
||||
'Lego',
|
||||
'StarWars',
|
||||
],
|
||||
SearchCriterion::NOT_IN
|
||||
);
|
||||
|
||||
$legoCriteria->addAnd(
|
||||
'Stock',
|
||||
5,
|
||||
SearchCriterion::LESS_EQUAL
|
||||
);
|
||||
|
||||
$booksCriteria = SearchCriteria::create(
|
||||
'Page_TaxonomyTerms_ID',
|
||||
[
|
||||
'Books',
|
||||
'HarryPotter',
|
||||
],
|
||||
SearchCriterion::NOT_IN
|
||||
);
|
||||
|
||||
$booksCriteria->addAnd(
|
||||
'Stock',
|
||||
2,
|
||||
SearchCriterion::LESS_EQUAL
|
||||
);
|
||||
|
||||
// Combine the two criteria with an `OR` conjunction
|
||||
$criteria = SearchCriteria::create($legoCriteria)->addOr($booksCriteria);
|
||||
|
||||
$query = SearchQuery::create();
|
||||
$query->filterBy($criteria);
|
||||
|
||||
$index = new SolrIndexTest_FakeIndex();
|
||||
|
||||
$this->assertTrue(in_array($expected, $index->getFiltersComponent($query)));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user