Skip to content

Commit a8fea31

Browse files
Full-text improvements (#947)
* better full text search in database engine, automatic relevance ranking * simplify code * Apply fixes from StyleCI * fix check * update method name * add engine helper * Apply fixes from StyleCI * fix conditional * restructure code * comment * Update DatabaseEngine.php --------- Co-authored-by: StyleCI Bot <bot@styleci.io>
1 parent 90fd0ac commit a8fea31

File tree

3 files changed

+107
-32
lines changed

3 files changed

+107
-32
lines changed

src/Builder.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,4 +579,12 @@ protected function engine()
579579
{
580580
return $this->model->searchableUsing();
581581
}
582+
583+
/**
584+
* Get the connection type for the underlying model.
585+
*/
586+
public function modelConnectionType(): string
587+
{
588+
return $this->model->getConnection()->getDriverName();
589+
}
582590
}

src/Engines/DatabaseEngine.php

Lines changed: 90 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,34 @@ public function search(Builder $builder)
6161
];
6262
}
6363

64+
/**
65+
* Get the Eloquent models for the given builder.
66+
*
67+
* @param \Laravel\Scout\Builder $builder
68+
* @param int|null $page
69+
* @param int|null $perPage
70+
* @return \Illuminate\Database\Eloquent\Collection
71+
*/
72+
protected function searchModels(Builder $builder, $page = null, $perPage = null)
73+
{
74+
return $this->buildSearchQuery($builder)
75+
->when(! is_null($page) && ! is_null($perPage), function ($query) use ($page, $perPage) {
76+
$query->forPage($page, $perPage);
77+
})
78+
->when($builder->orders, function ($query) use ($builder) {
79+
foreach ($builder->orders as $order) {
80+
$query->orderBy($order['column'], $order['direction']);
81+
}
82+
})
83+
->when(! $this->getFullTextColumns($builder), function ($query) use ($builder) {
84+
$query->orderBy($builder->model->getTable().'.'.$builder->model->getScoutKeyName(), 'desc');
85+
})
86+
->when($this->shouldOrderByRelevance($builder), function ($query) use ($builder) {
87+
$this->orderByRelevance($builder, $query);
88+
})
89+
->get();
90+
}
91+
6492
/**
6593
* Paginate the given search on the engine.
6694
*
@@ -94,6 +122,9 @@ public function paginateUsingDatabase(Builder $builder, $perPage, $pageName, $pa
94122
->when(! $this->getFullTextColumns($builder), function ($query) use ($builder) {
95123
$query->orderBy($builder->model->getTable().'.'.$builder->model->getScoutKeyName(), 'desc');
96124
})
125+
->when($this->shouldOrderByRelevance($builder), function ($query) use ($builder) {
126+
$this->orderByRelevance($builder, $query);
127+
})
97128
->paginate($perPage, ['*'], $pageName, $page);
98129
}
99130

@@ -129,32 +160,10 @@ public function simplePaginateUsingDatabase(Builder $builder, $perPage, $pageNam
129160
->when(! $this->getFullTextColumns($builder), function ($query) use ($builder) {
130161
$query->orderBy($builder->model->getTable().'.'.$builder->model->getScoutKeyName(), 'desc');
131162
})
132-
->simplePaginate($perPage, ['*'], $pageName, $page);
133-
}
134-
135-
/**
136-
* Get the Eloquent models for the given builder.
137-
*
138-
* @param \Laravel\Scout\Builder $builder
139-
* @param int|null $page
140-
* @param int|null $perPage
141-
* @return \Illuminate\Database\Eloquent\Collection
142-
*/
143-
protected function searchModels(Builder $builder, $page = null, $perPage = null)
144-
{
145-
return $this->buildSearchQuery($builder)
146-
->when(! is_null($page) && ! is_null($perPage), function ($query) use ($page, $perPage) {
147-
$query->forPage($page, $perPage);
148-
})
149-
->when($builder->orders, function ($query) use ($builder) {
150-
foreach ($builder->orders as $order) {
151-
$query->orderBy($order['column'], $order['direction']);
152-
}
153-
})
154-
->when(! $this->getFullTextColumns($builder), function ($query) use ($builder) {
155-
$query->orderBy($builder->model->getTable().'.'.$builder->model->getScoutKeyName(), 'desc');
163+
->when($this->shouldOrderByRelevance($builder), function ($query) use ($builder) {
164+
$this->orderByRelevance($builder, $query);
156165
})
157-
->get();
166+
->simplePaginate($perPage, ['*'], $pageName, $page);
158167
}
159168

160169
/**
@@ -196,9 +205,11 @@ protected function initializeSearchQuery(Builder $builder, array $columns, array
196205
return $query;
197206
}
198207

199-
return $query->where(function ($query) use ($builder, $columns, $prefixColumns, $fullTextColumns) {
200-
$connectionType = $builder->model->getConnection()->getDriverName();
208+
[$connectionType] = [
209+
$builder->modelConnectionType(),
210+
];
201211

212+
return $query->where(function ($query) use ($connectionType, $builder, $columns, $prefixColumns, $fullTextColumns) {
202213
$canSearchPrimaryKey = ctype_digit($builder->query) &&
203214
in_array($builder->model->getKeyType(), ['int', 'integer']) &&
204215
($connectionType != 'pgsql' || $builder->query <= PHP_INT_MAX) &&
@@ -212,11 +223,7 @@ protected function initializeSearchQuery(Builder $builder, array $columns, array
212223

213224
foreach ($columns as $column) {
214225
if (in_array($column, $fullTextColumns)) {
215-
$query->orWhereFullText(
216-
$builder->model->qualifyColumn($column),
217-
$builder->query,
218-
$this->getFullTextOptions($builder)
219-
);
226+
continue;
220227
} else {
221228
if ($canSearchPrimaryKey && $column === $builder->model->getScoutKeyName()) {
222229
continue;
@@ -229,9 +236,60 @@ protected function initializeSearchQuery(Builder $builder, array $columns, array
229236
);
230237
}
231238
}
239+
240+
if (count($fullTextColumns) > 0) {
241+
$query->orWhereFullText(
242+
array_map(fn ($column) => $builder->model->qualifyColumn($column), $fullTextColumns),
243+
$builder->query,
244+
$this->getFullTextOptions($builder)
245+
);
246+
}
232247
});
233248
}
234249

250+
/**
251+
* Determine if the query should be ordered by relevance.
252+
*/
253+
protected function shouldOrderByRelevance(Builder $builder): bool
254+
{
255+
// MySQL orders by relevance by default, so we will only order by relevance on
256+
// Postgres with no developer-defined orders. If there is developer defined
257+
// order by clauses we will let those take precedence over the relevance.
258+
return $builder->modelConnectionType() === 'pgsql' &&
259+
count($this->getFullTextColumns($builder)) > 0 &&
260+
empty($builder->orders);
261+
}
262+
263+
/**
264+
* Add an "order by" clause that orders by relevance (Postgres only).
265+
*
266+
* @param \Laravel\Scout\Builder $builder
267+
* @param \Illuminate\Database\Eloquent\Builder $query
268+
* @return \Illuminate\Database\Eloquent\Builder
269+
*/
270+
protected function orderByRelevance(Builder $builder, $query)
271+
{
272+
$fullTextColumns = $this->getFullTextColumns($builder);
273+
274+
$language = $this->getFullTextOptions($builder)['language'] ?? 'english';
275+
276+
$vectors = collect($fullTextColumns)->map(function ($column) use ($builder, $language) {
277+
return sprintf("to_tsvector('%s', %s)", $language, $builder->model->qualifyColumn($column));
278+
})->implode(' || ');
279+
280+
return $query->orderByRaw(
281+
sprintf(
282+
'ts_rank('.$vectors.', %s(?)) desc',
283+
match ($this->getFullTextOptions($builder)['mode'] ?? 'plainto_tsquery') {
284+
'phrase' => 'phraseto_tsquery',
285+
'websearch' => 'websearch_to_tsquery',
286+
default => 'plainto_tsquery',
287+
},
288+
),
289+
[$builder->query]
290+
);
291+
}
292+
235293
/**
236294
* Add additional, developer defined constraints to the search query.
237295
*

src/Scout.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Laravel\Scout;
44

5+
use Laravel\Scout\Engines\Engine;
56
use Laravel\Scout\Jobs\MakeSearchable;
67
use Laravel\Scout\Jobs\RemoveFromSearch;
78

@@ -28,6 +29,14 @@ class Scout
2829
*/
2930
public static $removeFromSearchJob = RemoveFromSearch::class;
3031

32+
/**
33+
* Get a Scout engine instance.
34+
*/
35+
public static function engine(string $engine): Engine
36+
{
37+
return app(EngineManager::class)->engine($engine);
38+
}
39+
3140
/**
3241
* Specify the job class that should make models searchable.
3342
*

0 commit comments

Comments
 (0)