Skip to content
Merged
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
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,16 +79,17 @@ Any additional settings you want to define per index can be included in the `sta
'fields' => [
[
'name' => 'company_name',
'type' => 'string'
'type' => 'string',
],
[
'name' => 'num_employees',
'type' => 'int32'
'type' => 'int32',
'sort' => true,
],
[
'name' => 'country',
'type' => 'string',
'facet' => true
'facet' => true,
],
],
],
Expand Down
12 changes: 5 additions & 7 deletions src/Typesense/Index.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Illuminate\Support\Collection;
use Statamic\Contracts\Search\Searchable;
use Statamic\Facades\Blink;
use Statamic\Search\Documents;
use Statamic\Search\Index as BaseIndex;
use Statamic\Support\Arr;
Expand Down Expand Up @@ -108,11 +109,7 @@ public function searchUsingApi($query, array $options = []): array
->join(',') ?: '*';
}

foreach (Arr::get($this->config, 'settings.search_options', []) as $handle => $value) {
$options[$handle] = $value;
}

$searchResults = $this->getOrCreateIndex()->documents->search($options);
$searchResults = $this->getOrCreateIndex()->documents->search(array_merge(Arr::get($this->config, 'settings.search_options', []), $options));

$total = count($searchResults['hits']);

Expand Down Expand Up @@ -162,8 +159,9 @@ public function getOrCreateIndex()

public function getTypesenseSchemaFields(): Collection
{
return collect(Arr::get($this->getOrCreateIndex()->retrieve(), 'fields', []))
->pluck('type', 'name');
return Blink::once('statamic-typesense::schema::'.$this->name(), function () {
return collect(Arr::get($this->getOrCreateIndex()->retrieve(), 'fields', []));
});
}

private function getDefaultFields(Searchable $entry): array
Expand Down
43 changes: 31 additions & 12 deletions src/Typesense/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,13 @@ private function wheresToFilter(array $wheres): string
{
$filterBy = '';

$schemaFields = $this->index->getTypesenseSchemaFields();
$schemaFields = $this->index->getTypesenseSchemaFields()->pluck('type', 'name');

foreach ($this->wheres as $where) {
if ($filterBy != '') {
$filterBy .= $where['boolean'] == 'and' ? ' && ' : ' || ';
}
foreach ($wheres as $where) {
$operator = $filterBy != '' ? ($where['boolean'] == 'and' ? ' && ' : ' || ') : '';

if ($where['type'] == 'Nested') {
$filterBy .= ' ( '.$this->wheresToFilter($where->query['wheres']).' ) ';
$filterBy .= $operator.' ( '.$this->wheresToFilter($where->query['wheres']).' ) ';

continue;
}
Expand All @@ -46,18 +44,18 @@ private function wheresToFilter(array $wheres): string
continue;
}

$filterBy .= ' ( ';
$filterBy .= $operator.' ( ';

switch ($where['type']) {
case 'JsonContains':
case 'JsonOverlaps':
case 'WhereIn':
case 'In':
$filterBy .= $where['column'].':'.$this->transformArrayOfValuesForTypeSense($schemaType, $where['values']);
break;

case 'JsonDoesnContain':
case 'JsonDoesntOverlap':
case 'WhereNotIn':
case 'NotIn':
$filterBy .= $where['column'].':!='.$this->transformArrayOfValuesForTypeSense($schemaType, $where['values']);
break;

Expand Down Expand Up @@ -97,16 +95,37 @@ private function transformValueForTypeSense(string $schemaType, mixed $value): m
};
}

private function ordersToSortBy(array $orders): string
{
$schemaFields = $this->index->getTypesenseSchemaFields()->keyBy('name');

return collect($orders)
->filter(function ($order) use ($schemaFields) {
if (! $field = $schemaFields->get($order->sort)) {
return false;
}

return $field['sort'] ?? false;
})
->take(3) // typesense only allows up to 3 sort columns
->map(function ($order) {
return $order->sort.':'.$order->direction;
})
->join(',');
}

private function getApiResults()
{
$options = ['per_page' => $this->perPage, 'page' => $this->page];

$filterBy = $this->wheresToFilter($this->wheres);

if ($filterBy) {
if ($filterBy = $this->wheresToFilter($this->wheres)) {
$options['filter_by'] = $filterBy;
}

if ($orderBy = $this->ordersToSortBy($this->orderBys)) {
$options['sort_by'] = $orderBy;
}

return $this->index->searchUsingApi($this->query ?? '', $options);
}

Expand Down
235 changes: 235 additions & 0 deletions tests/Unit/QueryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
<?php

namespace StatamicRadPack\Typesense\Tests\Unit;

use PHPUnit\Framework\Attributes\Test;
use Statamic\Facades;
use Statamic\Query\OrderBy;
use StatamicRadPack\Typesense\Tests\TestCase;
use StatamicRadPack\Typesense\Typesense\Query;

class QueryTest extends TestCase
{
#[Test]
public function it_returns_simple_wheres_in_the_correct_format()
{
$index = Facades\Search::index('typesense_index');
$query = new Query($index);
$query->where('title', 'test');

Facades\Blink::put('statamic-typesense::schema::typesense_index', collect([
[
'name' => 'title',
'type' => 'string',
],
[
'name' => 'other',
'type' => 'string',
],
[
'name' => 'final',
'type' => 'int32',
],
]));

$reflection = new \ReflectionObject($query);
$method = $reflection->getMethod('wheresToFilter');
$method->setAccessible(true);

$property = $reflection->getProperty('wheres');
$property->setAccessible(true);

$result = $method->invoke($query, $property->getValue($query));

$this->assertSame($result, ' ( title:=`test` ) ');

$query->orWhere('other', 'value');

$result = $method->invoke($query, $property->getValue($query));

$this->assertSame($result, ' ( title:=`test` ) || ( other:=`value` ) ');

$query->where('final', 'value');

$result = $method->invoke($query, $property->getValue($query));

$this->assertSame($result, ' ( title:=`test` ) || ( other:=`value` ) && ( final:=0 ) ');
}

#[Test]
public function it_handles_where_ins()
{
$index = Facades\Search::index('typesense_index');
$query = new Query($index);
$query->whereIn('title', ['test', 'two', 'three']);

Facades\Blink::put('statamic-typesense::schema::typesense_index', collect([
[
'name' => 'title',
'type' => 'string[]',
],
]));

$reflection = new \ReflectionObject($query);
$method = $reflection->getMethod('wheresToFilter');
$method->setAccessible(true);

$property = $reflection->getProperty('wheres');
$property->setAccessible(true);

$result = $method->invoke($query, $property->getValue($query));

$this->assertSame($result, ' ( title:["`test`","`two`","`three`"] ) ');
}

#[Test]
public function it_handles_where_like()
{
$index = Facades\Search::index('typesense_index');
$query = new Query($index);
$query->where('title', 'like', 'test');

Facades\Blink::put('statamic-typesense::schema::typesense_index', collect([
[
'name' => 'title',
'type' => 'string',
],
]));

$reflection = new \ReflectionObject($query);
$method = $reflection->getMethod('wheresToFilter');
$method->setAccessible(true);

$property = $reflection->getProperty('wheres');
$property->setAccessible(true);

$result = $method->invoke($query, $property->getValue($query));

$this->assertSame($result, ' ( title:`test` ) ');
}

#[Test]
public function it_ignores_wheres_not_found_in_the_typesense_schema()
{
$index = Facades\Search::index('typesense_index');
$query = new Query($index);
$query->where('title', 'test');
$query->orWhere('other', 'value');
$query->where('final', 'value');

Facades\Blink::put('statamic-typesense::schema::typesense_index', collect([
[
'name' => 'title',
'type' => 'string',
],
[
'name' => 'other',
'type' => 'string',
],
]));

$reflection = new \ReflectionObject($query);
$method = $reflection->getMethod('wheresToFilter');
$method->setAccessible(true);

$property = $reflection->getProperty('wheres');
$property->setAccessible(true);

$result = $method->invoke($query, $property->getValue($query));

$this->assertSame($result, ' ( title:=`test` ) || ( other:=`value` ) ');
}

#[Test]
public function it_returns_sort_by_in_the_correct_format()
{
$index = Facades\Search::index('typesense_index');
$query = new Query($index);

Facades\Blink::put('statamic-typesense::schema::typesense_index', collect([
[
'name' => 'title',
'sort' => true,
],
[
'name' => 'other',
'sort' => true,
],
]));

$reflection = new \ReflectionObject($query);
$method = $reflection->getMethod('ordersToSortBy');
$method->setAccessible(true);

$orderBys = [
new OrderBy('title', 'desc'),
new OrderBy('other', 'asc'),
];

$result = $method->invoke($query, $orderBys);

$this->assertSame($result, 'title:desc,other:asc');
}

#[Test]
public function it_ignores_sorts_that_arent_found_in_the_typesense_schema()
{
$index = Facades\Search::index('typesense_index');
$query = new Query($index);

Facades\Blink::put('statamic-typesense::schema::typesense_index', collect([
[
'name' => 'title',
'sort' => true,
],
[
'name' => 'other',
'sort' => false,
],
]));

$reflection = new \ReflectionObject($query);
$method = $reflection->getMethod('ordersToSortBy');
$method->setAccessible(true);

$orderBys = [
new OrderBy('title', 'desc'),
new OrderBy('other', 'asc'),
];

$result = $method->invoke($query, $orderBys);

$this->assertSame($result, 'title:desc');
}

#[Test]
public function it_ignores_sorts_that_arent_sortable_in_the_typesense_schema()
{
$index = Facades\Search::index('typesense_index');
$query = new Query($index);

Facades\Blink::put('statamic-typesense::schema::typesense_index', collect([
[
'name' => 'title',
'sort' => true,
],
[
'name' => 'other',
'sort' => false,
],
]));

$reflection = new \ReflectionObject($query);
$method = $reflection->getMethod('ordersToSortBy');
$method->setAccessible(true);

$orderBys = [
new OrderBy('title', 'desc'),
new OrderBy('other', 'asc'),
];

$result = $method->invoke($query, $orderBys);

$this->assertSame($result, 'title:desc');
}
}