Skip to content

Commit 9e4ed3c

Browse files
fix: add support for pre-computed tsvector columns
1 parent 7125f37 commit 9e4ed3c

File tree

4 files changed

+111
-5
lines changed

4 files changed

+111
-5
lines changed

src/Filter.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ class Filter
114114

115115
protected bool $fullTextPrefixMatch = true;
116116

117+
protected bool $isTsVector = false;
118+
117119
/**
118120
* Create a new filter instance
119121
*
@@ -618,6 +620,22 @@ public function setFullTextPrefixMatch(bool $prefixMatch): self
618620
return $this;
619621
}
620622

623+
/**
624+
* Mark the column as a pre-computed tsvector column
625+
*
626+
* When enabled, the filter will use the column directly with @@ operator
627+
* instead of converting it to tsvector at query time. This is much faster
628+
* when you have a GIN-indexed tsvector column.
629+
*
630+
* @param bool $isTsVector If true, treats the column as a tsvector type
631+
*/
632+
public function useTsVector(bool $isTsVector = true): self
633+
{
634+
$this->isTsVector = $isTsVector;
635+
636+
return $this;
637+
}
638+
621639
/**
622640
* Get the processed value for this filter
623641
*
@@ -914,6 +932,14 @@ public function getFullTextPrefixMatch(): bool
914932
return $this->fullTextPrefixMatch;
915933
}
916934

935+
/**
936+
* Check if the column is a pre-computed tsvector column
937+
*/
938+
public function isTsVector(): bool
939+
{
940+
return $this->isTsVector;
941+
}
942+
917943
/**
918944
* Get the formatted attribute for SQL queries
919945
*/

src/Traits/Filterable.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -466,9 +466,10 @@ private function applyFullTextSearch(Builder $builder, Filter $filter, mixed $se
466466
$columns = $filter->getFullTextColumns() ?? [$filter->getAttribute()];
467467
$language = $filter->getFullTextLanguage();
468468
$prefixMatch = $filter->getFullTextPrefixMatch();
469+
$isTsVector = $filter->isTsVector();
469470

470471
if ($filter->isUsingPostgreSQL()) {
471-
$this->applyPostgreSQLFullTextSearch($builder, $searchTerm, $columns, $language, $prefixMatch);
472+
$this->applyPostgreSQLFullTextSearch($builder, $searchTerm, $columns, $language, $prefixMatch, $isTsVector);
472473

473474
return;
474475
}
@@ -481,12 +482,13 @@ private function applyFullTextSearch(Builder $builder, Filter $filter, mixed $se
481482
*
482483
* @param array<int, string> $columns
483484
*/
484-
private function applyPostgreSQLFullTextSearch(Builder $builder, string $searchTerm, array $columns, ?string $language, bool $prefixMatch): void
485+
private function applyPostgreSQLFullTextSearch(Builder $builder, string $searchTerm, array $columns, ?string $language, bool $prefixMatch, bool $isTsVector = false): void
485486
{
486487
$lang = $language ?? Config::get('app.fulltext_language', 'simple');
487488

488-
if (count($columns) === 1 && $columns[0] === 'search_vector') {
489-
$builder->whereRaw("search_vector @@ websearch_to_tsquery('simple', ?)", [$searchTerm]);
489+
if ($isTsVector && count($columns) === 1) {
490+
$column = $columns[0];
491+
$builder->whereRaw("{$column} @@ websearch_to_tsquery('{$lang}', ?)", [$searchTerm]);
490492

491493
return;
492494
}

tests/Unit/FilterableFullTextTest.php

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ class FilterableFullTextTestModel extends Model
122122
expect(true)->toBeTrue();
123123
});
124124

125-
it('uses websearch_to_tsquery for search_vector column', function (): void {
125+
it('uses websearch_to_tsquery for tsvector column with useTsVector', function (): void {
126126
global $builder, $model;
127127

128128
$builder->shouldReceive('whereRaw')
@@ -138,13 +138,62 @@ class FilterableFullTextTestModel extends Model
138138

139139
$filter = Filter::fullText('search_vector', 'search')
140140
->setDatabaseDriver('pgsql')
141+
->useTsVector()
141142
->setValue('laravel');
142143

143144
$model->scopeFilterable($builder, [$filter]);
144145

145146
expect(true)->toBeTrue();
146147
});
147148

149+
it('uses websearch_to_tsquery for custom tsvector column name', function (): void {
150+
global $builder, $model;
151+
152+
$builder->shouldReceive('whereRaw')
153+
->once()
154+
->withArgs(fn ($sql, $bindings): bool => str_contains((string) $sql, 'custom_fts_column') &&
155+
str_contains((string) $sql, 'websearch_to_tsquery') &&
156+
$bindings[0] === 'test query')
157+
->andReturnSelf();
158+
159+
$builder->shouldReceive('with')
160+
->zeroOrMoreTimes()
161+
->andReturnSelf();
162+
163+
$filter = Filter::fullText('custom_fts_column', 'q')
164+
->setDatabaseDriver('pgsql')
165+
->useTsVector()
166+
->setValue('test query');
167+
168+
$model->scopeFilterable($builder, [$filter]);
169+
170+
expect(true)->toBeTrue();
171+
});
172+
173+
it('uses configured language for tsvector column', function (): void {
174+
global $builder, $model;
175+
176+
$builder->shouldReceive('whereRaw')
177+
->once()
178+
->withArgs(fn ($sql, $bindings): bool => str_contains((string) $sql, "'portuguese'") &&
179+
str_contains((string) $sql, 'websearch_to_tsquery'))
180+
->andReturnSelf();
181+
182+
$builder->shouldReceive('with')
183+
->zeroOrMoreTimes()
184+
->andReturnSelf();
185+
186+
$filter = Filter::fullText('search_vector', 'search')
187+
->setDatabaseDriver('pgsql')
188+
->useTsVector()
189+
->setFullTextLanguage('portuguese')
190+
->setValue('teste');
191+
192+
$model->scopeFilterable($builder, [$filter]);
193+
194+
expect(true)->toBeTrue();
195+
});
196+
148197
it('ignores empty search terms in full-text', function (): void {
149198
global $builder, $model;
150199

tests/Unit/FullTextSearchTest.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,35 @@
8282
->and($filter->getAttribute())->toBe('search_vector');
8383
});
8484

85+
it('can mark column as tsvector with useTsVector', function (): void {
86+
$filter = Filter::fullText('search_vector')->useTsVector();
87+
88+
expect($filter->isTsVector())->toBeTrue();
89+
});
90+
91+
it('defaults to non-tsvector column', function (): void {
92+
$filter = Filter::fullText('content');
93+
94+
expect($filter->isTsVector())->toBeFalse();
95+
});
96+
97+
it('can disable tsvector mode', function (): void {
98+
$filter = Filter::fullText('search_vector')->useTsVector()->useTsVector(false);
99+
100+
expect($filter->isTsVector())->toBeFalse();
101+
});
102+
103+
it('can chain useTsVector with other configuration methods', function (): void {
104+
$filter = Filter::fullText('custom_fts')
105+
->useTsVector()
106+
->setFullTextLanguage('portuguese')
107+
->setDatabaseDriver('pgsql');
108+
109+
expect($filter->isTsVector())->toBeTrue()
110+
->and($filter->getFullTextLanguage())->toBe('portuguese')
111+
->and($filter->isUsingPostgreSQL())->toBeTrue();
112+
});
113+
85114
it('can configure language after creation', function (): void {
86115
$filter = Filter::fullText('content');
87116

0 commit comments

Comments
 (0)