Skip to content
This repository was archived by the owner on Feb 21, 2025. It is now read-only.

Commit 52b1092

Browse files
committed
2 parents 6e394fb + 5bf394b commit 52b1092

File tree

6 files changed

+179
-18
lines changed

6 files changed

+179
-18
lines changed

docs/03-Usage.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,9 @@ Looking at the `tests` folder, there is a `TestIndexFour`. This index is not loa
259259
To search, here's an example using all the features, and setting the resulting outcome from the search
260260
onto the current `Controller` to be useable in templates.
261261

262+
More advanced filter options are available, see the [Advanced filters & excludes](04-Advanced-Options/05-Filters-excludes.md)
263+
page for more information.
264+
262265
```php
263266
class SearchController extends PageController
264267
{
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Advanced filters & excludes
2+
3+
When performing a search, in addition to simple single-value and array-of-value filters, it’s possible
4+
to build more complex filter/exclude criteria. Some examples of this include:
5+
6+
- Range filters (greater than/less than)
7+
- Geospatial search
8+
- Partial match filters (starts with/ends with)
9+
10+
## Usage
11+
12+
Advanced search/exclude filters are constructed from `Criteria` objects, from the [MinimalCode Solr
13+
Search Criteria](https://github.com/minimalcode-org/search) package. More information and usage
14+
examples are available [here](https://github.com/minimalcode-org/minimalcode-parent/wiki/4.1-Solr-Search-%28Php%29).
15+
16+
When passing a `Criteria` object to `addFilter` or `addExclude`, the first argument (usually the
17+
field name) can be set to any string value.
18+
19+
```php
20+
$query = new BaseQuery();
21+
22+
// Simple date filter - exclude any pages which have an embargo date in the future
23+
$criteria = Criteria::where('SiteTree_Embargo')
24+
->greaterThanEqual('NOW');
25+
$query->addExclude('embargo', $criteria);
26+
27+
// Starts with/ends with filter
28+
$criteria = Criteria::where('SiteTree_Title')
29+
->startsWith('prefix')
30+
->endsWith('suffix');
31+
$query->addFilter('title-partial-match', $criteria);
32+
33+
// Nested criteria
34+
$topLevel = Criteria::where('SiteTree_ParentID')
35+
->is(0);
36+
$criteria = Criteria::where('SiteTree_Title')
37+
->startsWith('test')
38+
->andWhere($topLevel);
39+
$query->addFilter('top-level-test-pages', $criteria);
40+
```

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Advanced searching for SilverStripe with Solr. Versions 4-8+ are supported.
1212
02. [Boosting](04-Advanced-Options/02-Boosting.md)
1313
03. [Fuzzy search](04-Advanced-Options/03-Fuzzy-search.md)
1414
04. [Elevation [WIP]](04-Advanced-Options/04-Elevation.md)
15+
05. [Advanced filters & excludes](04-Advanced-Options/05-Filters-excludes.md)
1516
05. [Customisation](05-Customisation.md)
1617
06. [CMS Usage](06-CMS-Usage.md)
1718
07. [Debugging](07-Debugging.md)

src/Traits/QueryComponentTraits/QueryComponentFilterTrait.php

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,32 @@ trait QueryComponentFilterTrait
3333
*/
3434
protected $clientQuery;
3535

36+
/**
37+
* Convert a field/value filter pair to a Criteria object that can build part of a Solr query.
38+
* If a Criteria object is passed as the value, it will be returned unmodified.
39+
*
40+
* @param string $field
41+
* @param mixed $value
42+
* @return Criteria
43+
*/
44+
protected function buildCriteriaFilter(string $field, $value): Criteria
45+
{
46+
if ($value instanceof Criteria) {
47+
return $value;
48+
}
49+
50+
$value = (array)$value;
51+
return Criteria::where($field)->in($value);
52+
}
53+
3654
/**
3755
* Create filter queries
3856
*/
3957
protected function buildFilters(): void
4058
{
4159
$filters = $this->query->getFilter();
4260
foreach ($filters as $field => $value) {
43-
$value = is_array($value) ? $value : [$value];
44-
$criteria = Criteria::where($field)->in($value);
61+
$criteria = $this->buildCriteriaFilter($field, $value);
4562
$this->clientQuery->createFilterQuery('filter-' . $field)
4663
->setQuery($criteria->getQuery());
4764
}
@@ -93,10 +110,8 @@ protected function buildExcludes(): void
93110
{
94111
$filters = $this->query->getExclude();
95112
foreach ($filters as $field => $value) {
96-
$value = is_array($value) ? $value : [$value];
97-
$criteria = Criteria::where($field)
98-
->in($value)
99-
->not();
113+
$criteria = $this->buildCriteriaFilter($field, $value);
114+
$criteria = $criteria->not(); // Negate the filter as we're excluding
100115
$this->clientQuery->createFilterQuery('exclude-' . $field)
101116
->setQuery($criteria->getQuery());
102117
}

src/Traits/QueryTraits/BaseQueryTrait.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
namespace Firesphere\SolrSearch\Traits;
1212

13+
use Minimalcode\Search\Criteria;
14+
1315
/**
1416
* Trait BaseQueryTrait Extraction from the BaseQuery class to keep things readable.
1517
*
@@ -83,7 +85,7 @@ public function addTerm(string $term, array $fields = [], int $boost = 0, $fuzzy
8385
* Adds filters to filter on by value
8486
*
8587
* @param string $field Field to filter on
86-
* @param string|array $value Value for this field
88+
* @param string|array|Criteria $value Value for this field
8789
* @return $this
8890
*/
8991
public function addFilter($field, $value): self
@@ -112,7 +114,7 @@ public function addField($field): self
112114
* Exclude fields from the search action
113115
*
114116
* @param string $field
115-
* @param string|array $value
117+
* @param string|array|Criteria $value
116118
* @return $this
117119
*/
118120
public function addExclude($field, $value): self

tests/unit/QueryComponentFactoryTest.php

Lines changed: 110 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,20 @@
88
use Firesphere\SolrSearch\Factories\QueryComponentFactory;
99
use Firesphere\SolrSearch\Indexes\BaseIndex;
1010
use Firesphere\SolrSearch\Queries\BaseQuery;
11+
use Minimalcode\Search\Criteria;
1112
use Page;
13+
use ReflectionMethod;
1214
use SilverStripe\Core\Config\Config;
1315
use SilverStripe\Core\Injector\Injector;
1416
use SilverStripe\Dev\SapphireTest;
1517
use Solarium\Core\Query\Helper;
18+
use Solarium\QueryType\Select\Query\FilterQuery;
19+
use Solarium\QueryType\Select\Query\Query;
1620

1721
class QueryComponentFactoryTest extends SapphireTest
1822
{
1923
protected static $fixture_file = '../fixtures/DataResolver.yml';
24+
2025
protected static $extra_dataobjects = [
2126
TestObject::class,
2227
TestPage::class,
@@ -28,6 +33,17 @@ class QueryComponentFactoryTest extends SapphireTest
2833
*/
2934
protected $factory;
3035

36+
protected function setUp()
37+
{
38+
parent::setUp();
39+
Injector::inst()->get(Page::class)->requireDefaultRecords();
40+
foreach (self::$extra_dataobjects as $className) {
41+
Config::modify()->merge($className, 'extensions', [DataObjectExtension::class]);
42+
}
43+
$this->factory = new QueryComponentFactory();
44+
$this->factory->setIndex(Injector::inst()->get(CircleCITestIndex::class));
45+
}
46+
3147
public function testBuildQuery()
3248
{
3349
$index = new CircleCITestIndex();
@@ -61,7 +77,6 @@ public function testBuildQuery()
6177
$this->assertInternalType('array', $this->factory->getBoostTerms());
6278
}
6379

64-
6580
public function testEscapeTerms()
6681
{
6782
$term = '"test me" help';
@@ -72,19 +87,104 @@ public function testEscapeTerms()
7287
$this->assertEquals('"test me" help', $escaped);
7388

7489
$term = 'help me';
75-
7690
$this->assertEquals('help me', $this->factory->escapeSearch($term));
7791
}
7892

93+
public function testBuildCriteriaFilter()
94+
{
95+
$reflectionMethod = new ReflectionMethod(QueryComponentFactory::class, 'buildCriteriaFilter');
96+
$reflectionMethod->setAccessible(true);
97+
98+
/** @var Criteria $criteria */
99+
$criteria = $reflectionMethod->invoke(new QueryComponentFactory(), 'TestField', 'TestValue');
100+
$this->assertInstanceOf(Criteria::class, $criteria);
101+
$this->assertEquals('TestField:TestValue', $criteria->getQuery());
102+
103+
$criteria = $reflectionMethod->invoke(new QueryComponentFactory(), 'TestField', ['Arr1', 'Arr2']);
104+
$this->assertInstanceOf(Criteria::class, $criteria);
105+
$this->assertEquals('TestField:(Arr1 Arr2)', $criteria->getQuery());
106+
107+
$criteriaValue = Criteria::where('TestField')->is('TestValue');
108+
$criteria = $reflectionMethod->invoke(new QueryComponentFactory(), 'TestField', $criteriaValue);
109+
$this->assertInstanceOf(Criteria::class, $criteria);
110+
$this->assertSame($criteriaValue, $criteria);
111+
}
79112

80-
protected function setUp()
113+
public function testFilterAndExcludeStrings()
81114
{
82-
parent::setUp();
83-
Injector::inst()->get(Page::class)->requireDefaultRecords();
84-
foreach (self::$extra_dataobjects as $className) {
85-
Config::modify()->merge($className, 'extensions', [DataObjectExtension::class]);
86-
}
87-
$this->factory = new QueryComponentFactory();
88-
$this->factory->setIndex(Injector::inst()->get(CircleCITestIndex::class));
115+
$mockFilterQuery = $this->createMock(FilterQuery::class);
116+
$mockFilterQuery->expects($this->once())
117+
->method('setQuery')
118+
->with($this->equalTo('TestFilterField:TestFilterValue'));
119+
$mockExcludeQuery = $this->createMock(FilterQuery::class);
120+
$mockExcludeQuery->expects($this->once())
121+
->method('setQuery')
122+
->with($this->equalTo('-TestExcludeField:TestExcludeValue'));
123+
124+
$mockClientQuery = new class extends Query {
125+
public $mockFilterQuery;
126+
public $mockExcludeQuery;
127+
public function createFilterQuery($options = null): FilterQuery
128+
{
129+
if ($options === 'filter-TestFilterField') {
130+
return $this->mockFilterQuery;
131+
} elseif ($options === 'exclude-TestExcludeField') {
132+
return $this->mockExcludeQuery;
133+
}
134+
135+
return parent::createFilterQuery($options);
136+
}
137+
};
138+
$mockClientQuery->mockFilterQuery = $mockFilterQuery;
139+
$mockClientQuery->mockExcludeQuery = $mockExcludeQuery;
140+
141+
$baseQuery = new BaseQuery();
142+
$baseQuery->addFilter('TestFilterField', 'TestFilterValue');
143+
$baseQuery->addExclude('TestExcludeField', 'TestExcludeValue');
144+
145+
$factory = new QueryComponentFactory();
146+
$factory->setIndex(Injector::inst()->get(CircleCITestIndex::class));
147+
$factory->setQuery($baseQuery);
148+
$factory->setClientQuery($mockClientQuery);
149+
$factory->buildQuery();
150+
}
151+
152+
public function testFilterAndExcludeCriteria()
153+
{
154+
$mockFilterQuery = $this->createMock(FilterQuery::class);
155+
$mockFilterQuery->expects($this->once())
156+
->method('setQuery')
157+
->with($this->equalTo('TestFilterField:TestFilterValue'));
158+
$mockExcludeQuery = $this->createMock(FilterQuery::class);
159+
$mockExcludeQuery->expects($this->once())
160+
->method('setQuery')
161+
->with($this->equalTo('-TestExcludeField:TestExcludeValue'));
162+
163+
$mockClientQuery = new class extends Query {
164+
public $mockFilterQuery;
165+
public $mockExcludeQuery;
166+
public function createFilterQuery($options = null): FilterQuery
167+
{
168+
if ($options === 'filter-TestFilterField') {
169+
return $this->mockFilterQuery;
170+
} elseif ($options === 'exclude-TestExcludeField') {
171+
return $this->mockExcludeQuery;
172+
}
173+
174+
return parent::createFilterQuery($options);
175+
}
176+
};
177+
$mockClientQuery->mockFilterQuery = $mockFilterQuery;
178+
$mockClientQuery->mockExcludeQuery = $mockExcludeQuery;
179+
180+
$baseQuery = new BaseQuery();
181+
$baseQuery->addFilter('TestFilterField', Criteria::where('TestFilterField')->is('TestFilterValue'));
182+
$baseQuery->addExclude('TestExcludeField', Criteria::where('TestExcludeField')->is('TestExcludeValue'));
183+
184+
$factory = new QueryComponentFactory();
185+
$factory->setIndex(Injector::inst()->get(CircleCITestIndex::class));
186+
$factory->setQuery($baseQuery);
187+
$factory->setClientQuery($mockClientQuery);
188+
$factory->buildQuery();
89189
}
90190
}

0 commit comments

Comments
 (0)