Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow filtering by comparison operators #835

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,13 @@ class Builder
*/
public $whereNotIns = [];

/**
* The "where" comparisons added to the query.
*
* @var array
*/
public $whereComparisons = [];

/**
* The "limit" that should be applied to the search.
*
Expand Down Expand Up @@ -167,6 +174,21 @@ public function whereNotIn($field, array $values)
return $this;
}

/**
* Add a "where comparison" constraint to the search query.
*
* @param string $field
* @param string $operator
* @param mixed $value
* @return $this
*/
public function whereComparison($field, $operator, $value)
{
$this->whereComparisons[] = compact('field', 'operator', 'value');

return $this;
}

/**
* Include soft deleted records in the results.
*
Expand Down
3 changes: 3 additions & 0 deletions src/Engines/AlgoliaEngine.php
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ protected function filters(Builder $builder)
return collect($values)->map(function ($value) use ($key) {
return $key.'='.$value;
})->all();
})->values())
->merge(collect($builder->whereComparisons)->map(function ($comparison) {
return $comparison['field'].$comparison['operator'].$comparison['value'];
})->values())->values()->all();
}

Expand Down
5 changes: 5 additions & 0 deletions src/Engines/CollectionEngine.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ protected function searchModels(Builder $builder)
$query->whereNotIn($key, $values);
}
})
->when(! $builder->callback && count($builder->whereComparisons) > 0, function ($query) use ($builder) {
foreach ($builder->whereComparisons as $comparison) {
$query->where($comparison['field'], $comparison['operator'], $comparison['value']);
}
})
->when($builder->orders, function ($query) use ($builder) {
foreach ($builder->orders as $order) {
$query->orderBy($order['column'], $order['direction']);
Expand Down
4 changes: 4 additions & 0 deletions src/Engines/DatabaseEngine.php
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,10 @@ protected function addAdditionalConstraints(Builder $builder, $query)
foreach ($builder->whereNotIns as $key => $values) {
$query->whereNotIn($key, $values);
}
})->when(! $builder->callback && count($builder->whereComparisons) > 0, function ($query) use ($builder) {
foreach ($builder->whereComparisons as $comparison) {
$query->where($comparison['field'], $comparison['operator'], $comparison['value']);
}
})->when(! is_null($builder->queryCallback), function ($query) use ($builder) {
call_user_func($builder->queryCallback, $query);
});
Expand Down
32 changes: 25 additions & 7 deletions src/Engines/MeilisearchEngine.php
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,7 @@ protected function performSearch(Builder $builder, array $searchParams = [])
protected function filters(Builder $builder)
{
$filters = collect($builder->wheres)->map(function ($value, $key) {
if (is_bool($value)) {
return sprintf('%s=%s', $key, $value ? 'true' : 'false');
}

return is_numeric($value)
? sprintf('%s=%s', $key, $value)
: sprintf('%s="%s"', $key, $value);
return sprintf('%s=%s', $key, $this->formatValue($value));
});

$whereInOperators = [
Expand All @@ -207,9 +201,33 @@ protected function filters(Builder $builder)
}
}

collect($builder->whereComparisons)->each(function ($comparison) use ($filters) {
$filters->push(sprintf(
'%s%s%s',
$comparison['field'],
$comparison['operator'],
$this->formatValue($comparison['value']))
);
});

return $filters->values()->implode(' AND ');
}

/**
* Format the value for the filter depending on its type.
*
* @param mixed $value
* @return string
*/
protected function formatValue($value)
{
if (is_bool($value)) {
return $value ? 'true' : 'false';
}

return is_numeric($value) ? $value : sprintf('"%s"', $value);
}

/**
* Get the sort array for the query.
*
Expand Down
12 changes: 9 additions & 3 deletions src/Engines/TypesenseEngine.php
Original file line number Diff line number Diff line change
Expand Up @@ -300,9 +300,15 @@ protected function filters(Builder $builder): string
->values()
->implode(' && ');

return $whereFilter.(
($whereFilter !== '' && $whereInFilter !== '') ? ' && ' : ''
).$whereInFilter;
$whereComparisonFilter = collect($builder->whereComparisons)
->map(fn ($comparison) => sprintf('%s:%s%s', $comparison['field'], $comparison['operator'], $comparison['value']))
->values()
->implode(' && ');

return collect([$whereFilter, $whereInFilter, $whereComparisonFilter])
->filter()
->values()
->implode(' && ');
}

/**
Expand Down
19 changes: 19 additions & 0 deletions tests/Unit/AlgoliaEngineTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,25 @@ public function test_search_sends_correct_parameters_to_algolia_for_empty_where_
$engine->search($builder);
}

public function test_search_sends_correct_parameters_to_algolia_for_where_comparison_search()
{
$client = m::mock(SearchClient::class);
$client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class));
$index->shouldReceive('search')->with('zonda', [
'numericFilters' => ['foo=1', ['bar=1', 'bar=2'], 'baz>2', 'qux<=3'],
]);

$engine = new AlgoliaEngine($client);
$builder = new Builder(new SearchableModel, 'zonda');
$builder
->where('foo', 1)
->whereIn('bar', [1, 2])
->whereComparison('baz', '>', 2)
->whereComparison('qux', '<=', 3);

$engine->search($builder);
}

public function test_map_correctly_maps_results_to_models()
{
$client = m::mock(SearchClient::class);
Expand Down
43 changes: 43 additions & 0 deletions tests/Unit/MeilisearchEngineTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,30 @@ public function test_where_not_in_conditions_are_applied()
$engine->search($builder);
}

public function test_where_comparison_conditions_are_applied()
{
$builder = new Builder(new SearchableModel(), '');
$builder->where('foo', 'bar');
$builder->where('bar', 'baz');
$builder->whereIn('qux', [1, 2]);
$builder->whereIn('quux', [1, 2]);
$builder->whereNotIn('eaea', [3]);
$builder->whereComparison('gt', '>', 10);
$builder->whereComparison('lt', '<', 20);
$builder->whereComparison('gte', '>=', 30);
$builder->whereComparison('lte', '<=', 40);

$client = m::mock(Client::class);
$client->shouldReceive('index')->once()->andReturn($index = m::mock(Indexes::class));
$index->shouldReceive('rawSearch')->once()->with($builder->query, array_filter([
'filter' => 'foo="bar" AND bar="baz" AND qux IN [1, 2] AND quux IN [1, 2] AND eaea NOT IN [3] AND gt>10 AND lt<20 AND gte>=30 AND lte<=40',
'hitsPerPage' => $builder->limit,
]))->andReturn([]);

$engine = new MeilisearchEngine($client);
$engine->search($builder);
}

public function test_where_in_conditions_are_applied_without_other_conditions()
{
$builder = new Builder(new SearchableModel(), '');
Expand Down Expand Up @@ -602,6 +626,25 @@ public function test_where_not_in_conditions_are_applied_without_other_condition
$engine->search($builder);
}

public function test_where_comparison_conditions_are_applied_without_other_conditions()
{
$builder = new Builder(new SearchableModel(), '');
$builder->whereComparison('gt', '>', 10);
$builder->whereComparison('lt', '<', 20);
$builder->whereComparison('gte', '>=', 30);
$builder->whereComparison('lte', '<=', 40);

$client = m::mock(Client::class);
$client->shouldReceive('index')->once()->andReturn($index = m::mock(Indexes::class));
$index->shouldReceive('rawSearch')->once()->with($builder->query, array_filter([
'filter' => 'gt>10 AND lt<20 AND gte>=30 AND lte<=40',
'hitsPerPage' => $builder->limit,
]))->andReturn([]);

$engine = new MeilisearchEngine($client);
$engine->search($builder);
}

public function test_empty_where_in_conditions_are_applied_correctly()
{
$builder = new Builder(new SearchableModel(), '');
Expand Down