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
8 changes: 8 additions & 0 deletions src/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -579,4 +579,12 @@ protected function engine()
{
return $this->model->searchableUsing();
}

/**
* Get the connection type for the underlying model.
*/
public function modelConnectionType(): string
{
return $this->model->getConnection()->getDriverName();
}
}
122 changes: 90 additions & 32 deletions src/Engines/DatabaseEngine.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,34 @@ public function search(Builder $builder)
];
}

/**
* Get the Eloquent models for the given builder.
*
* @param \Laravel\Scout\Builder $builder
* @param int|null $page
* @param int|null $perPage
* @return \Illuminate\Database\Eloquent\Collection
*/
protected function searchModels(Builder $builder, $page = null, $perPage = null)
{
return $this->buildSearchQuery($builder)
->when(! is_null($page) && ! is_null($perPage), function ($query) use ($page, $perPage) {
$query->forPage($page, $perPage);
})
->when($builder->orders, function ($query) use ($builder) {
foreach ($builder->orders as $order) {
$query->orderBy($order['column'], $order['direction']);
}
})
->when(! $this->getFullTextColumns($builder), function ($query) use ($builder) {
$query->orderBy($builder->model->getTable().'.'.$builder->model->getScoutKeyName(), 'desc');
})
->when($this->shouldOrderByRelevance($builder), function ($query) use ($builder) {
$this->orderByRelevance($builder, $query);
})
->get();
}

/**
* Paginate the given search on the engine.
*
Expand Down Expand Up @@ -94,6 +122,9 @@ public function paginateUsingDatabase(Builder $builder, $perPage, $pageName, $pa
->when(! $this->getFullTextColumns($builder), function ($query) use ($builder) {
$query->orderBy($builder->model->getTable().'.'.$builder->model->getScoutKeyName(), 'desc');
})
->when($this->shouldOrderByRelevance($builder), function ($query) use ($builder) {
$this->orderByRelevance($builder, $query);
})
->paginate($perPage, ['*'], $pageName, $page);
}

Expand Down Expand Up @@ -129,32 +160,10 @@ public function simplePaginateUsingDatabase(Builder $builder, $perPage, $pageNam
->when(! $this->getFullTextColumns($builder), function ($query) use ($builder) {
$query->orderBy($builder->model->getTable().'.'.$builder->model->getScoutKeyName(), 'desc');
})
->simplePaginate($perPage, ['*'], $pageName, $page);
}

/**
* Get the Eloquent models for the given builder.
*
* @param \Laravel\Scout\Builder $builder
* @param int|null $page
* @param int|null $perPage
* @return \Illuminate\Database\Eloquent\Collection
*/
protected function searchModels(Builder $builder, $page = null, $perPage = null)
{
return $this->buildSearchQuery($builder)
->when(! is_null($page) && ! is_null($perPage), function ($query) use ($page, $perPage) {
$query->forPage($page, $perPage);
})
->when($builder->orders, function ($query) use ($builder) {
foreach ($builder->orders as $order) {
$query->orderBy($order['column'], $order['direction']);
}
})
->when(! $this->getFullTextColumns($builder), function ($query) use ($builder) {
$query->orderBy($builder->model->getTable().'.'.$builder->model->getScoutKeyName(), 'desc');
->when($this->shouldOrderByRelevance($builder), function ($query) use ($builder) {
$this->orderByRelevance($builder, $query);
})
->get();
->simplePaginate($perPage, ['*'], $pageName, $page);
}

/**
Expand Down Expand Up @@ -196,9 +205,11 @@ protected function initializeSearchQuery(Builder $builder, array $columns, array
return $query;
}

return $query->where(function ($query) use ($builder, $columns, $prefixColumns, $fullTextColumns) {
$connectionType = $builder->model->getConnection()->getDriverName();
[$connectionType] = [
$builder->modelConnectionType(),
];
Copy link

@MeiKatz MeiKatz Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry to bother you again but why are you converting the model connection type to an array with one element just to retrieve the first and only element right after? Wouldn't it be easier to assign the connection type directly?

$connectionType = $builder->modelConnectionType();

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I was assigning multiple variables here in an earlier version.


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

foreach ($columns as $column) {
if (in_array($column, $fullTextColumns)) {
$query->orWhereFullText(
$builder->model->qualifyColumn($column),
$builder->query,
$this->getFullTextOptions($builder)
);
continue;
} else {
if ($canSearchPrimaryKey && $column === $builder->model->getScoutKeyName()) {
continue;
Expand All @@ -229,9 +236,60 @@ protected function initializeSearchQuery(Builder $builder, array $columns, array
);
}
}

if (count($fullTextColumns) > 0) {
$query->orWhereFullText(
array_map(fn ($column) => $builder->model->qualifyColumn($column), $fullTextColumns),
$builder->query,
$this->getFullTextOptions($builder)
);
}
});
}

/**
* Determine if the query should be ordered by relevance.
*/
protected function shouldOrderByRelevance(Builder $builder): bool
{
// MySQL orders by relevance by default, so we will only order by relevance on
// Postgres with no developer-defined orders. If there is developer defined
// order by clauses we will let those take precedence over the relevance.
return $builder->modelConnectionType() === 'pgsql' &&
count($this->getFullTextColumns($builder)) > 0 &&
empty($builder->orders);
}

/**
* Add an "order by" clause that orders by relevance (Postgres only).
*
* @param \Laravel\Scout\Builder $builder
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function orderByRelevance(Builder $builder, $query)
{
$fullTextColumns = $this->getFullTextColumns($builder);

$language = $this->getFullTextOptions($builder)['language'] ?? 'english';

$vectors = collect($fullTextColumns)->map(function ($column) use ($builder, $language) {
return sprintf("to_tsvector('%s', %s)", $language, $builder->model->qualifyColumn($column));
})->implode(' || ');

return $query->orderByRaw(
sprintf(
'ts_rank('.$vectors.', %s(?)) desc',
match ($this->getFullTextOptions($builder)['mode'] ?? 'plainto_tsquery') {
'phrase' => 'phraseto_tsquery',
'websearch' => 'websearch_to_tsquery',
default => 'plainto_tsquery',
},
),
[$builder->query]
);
}

/**
* Add additional, developer defined constraints to the search query.
*
Expand Down
9 changes: 9 additions & 0 deletions src/Scout.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Laravel\Scout;

use Laravel\Scout\Engines\Engine;
use Laravel\Scout\Jobs\MakeSearchable;
use Laravel\Scout\Jobs\RemoveFromSearch;

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

/**
* Get a Scout engine instance.
*/
public static function engine(string $engine): Engine
{
return app(EngineManager::class)->engine($engine);
}

/**
* Specify the job class that should make models searchable.
*
Expand Down