- added logic to forward `order_by` parameter to the search engine
- modified mongodb query when no search engine is present to use "textMatchScore" name instead of "score" for text match scores -- the text score projection was shadowing the "score" attribute
This commit is contained in:
pushrbx 2022-12-22 15:45:54 +00:00
parent 651ae88616
commit 4aa3c4c617
6 changed files with 66 additions and 22 deletions

View File

@ -14,7 +14,7 @@ class WhereClause extends BaseClause
protected function validate($value): bool
{
return !is_null($value);
return !in_array(null, (array)$value);
}
private function orWhere($query, $filter, $values): Builder

View File

@ -4,6 +4,7 @@ namespace App\Http\QueryBuilder;
use App\Http\QueryBuilder\Traits\PaginationParameterResolver;
use App\Services\ScoutSearchService;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use JetBrains\PhpStorm\ArrayShape;
@ -70,7 +71,24 @@ abstract class SearchQueryBuilder implements SearchQueryBuilderService
$q = $requestParameters->get("q");
if ($this->isSearchIndexUsed() && !empty($q)) {
$builder = $this->scoutSearchService->search($modelClass, $q);
$orderBy = $requestParameters->get("order_by");
$sort = $requestParameters->get("sort");
$searchOptions = [];
// todo: validate whether the specified field exists on the model
if (!empty($orderBy)) {
$searchOptions["order_by"] = $orderBy;
} else {
$searchOptions["order_by"] = null;
}
if (!empty($sort) && in_array($sort, ["asc", "desc"])) {
$searchOptions["sort_direction_descending"] = $sort == "desc";
} else {
$searchOptions["sort_direction_descending"] = false;
}
$builder = $this->scoutSearchService->search($modelClass, $q, $searchOptions["order_by"],
$searchOptions["sort_direction_descending"]);
} else {
// If "q" is not set, OR search indexes are disabled, we just get a query builder for the model.
// This way we can have a single place where we get the query builder from.
@ -104,11 +122,11 @@ abstract class SearchQueryBuilder implements SearchQueryBuilderService
'$search' => $q
],
], [
'score' => [
'textMatchScore' => [
'$meta' => 'textScore'
]
])
->orderBy('score', ['$meta' => 'textScore']);
->orderBy('textMatchScore', 'desc');
}
// The ->filter() call is a local model scope function, which applies filters based on the query string
@ -184,7 +202,7 @@ abstract class SearchQueryBuilder implements SearchQueryBuilderService
];
}
public function paginateBuilder(Request $request, \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder $results): \Illuminate\Contracts\Pagination\LengthAwarePaginator
public function paginateBuilder(Request $request, \Laravel\Scout\Builder|\Illuminate\Database\Eloquent\Builder $results): LengthAwarePaginator
{
['limit' => $limit, 'page' => $page] = $this->getPaginateParameters($request);
@ -197,6 +215,9 @@ abstract class SearchQueryBuilder implements SearchQueryBuilderService
// In that method the "$limit" member variable is being check whether it's null or it has value.
// If it's set to a number then the result set will be limited which we do the pagination on.
// If it's set to null, then the pagination will be done on the whole result set.
/**
* @var LengthAwarePaginator $paginated
*/
$paginated = $scoutBuilder
->jikanPaginate(
$limit,

View File

@ -6,7 +6,8 @@ use App\Helpers\Guards;
class DefaultScoutSearchService implements ScoutSearchService
{
public function search(object|string $modelClass, string $q): \Laravel\Scout\Builder
public function search(object|string $modelClass, string $q, ?string $orderByField = null,
bool $sortDirectionDescending = false): \Laravel\Scout\Builder
{
Guards::shouldBeMongoDbModel($modelClass);

View File

@ -16,23 +16,28 @@ class ElasticScoutSearchService implements ScoutSearchService
* @throws \Elastic\Elasticsearch\Exception\ClientResponseException
* @throws \Elastic\Elasticsearch\Exception\MissingParameterException
*/
public function search(object|string $modelClass, string $q): \Laravel\Scout\Builder
public function search(object|string $modelClass, string $q, ?string $orderByField = null,
bool $sortDirectionDescending = false): \Laravel\Scout\Builder
{
return $modelClass::search($q, function(\Elastic\ElasticSearch\Client $client, \ONGR\ElasticsearchDSL\Search $body) use ($modelClass) {
return $modelClass::search($q, function(\Elastic\ElasticSearch\Client $client, \ONGR\ElasticsearchDSL\Search $body) use ($modelClass, $orderByField, $sortDirectionDescending) {
$modelInstance = new $modelClass;
if ($modelInstance instanceof JikanApiSearchableModel) {
// if the model specifies search index sort order, use it
$sortByFields = $modelInstance->getSearchIndexSortBy();
if (!is_null($sortByFields)) {
foreach ($sortByFields as $f) {
$direction = match ($f['direction']) {
'asc' => FieldSort::ASC,
'desc' => FieldSort::DESC,
};
if (!is_null($orderByField)) {
$body->addSort(new FieldSort($orderByField, ['order' => $sortDirectionDescending ? FieldSort::DESC : FieldSort::ASC]));
} else {
// if the model specifies search index sort order, use it
$sortByFields = $modelInstance->getSearchIndexSortBy();
if (!is_null($sortByFields)) {
foreach ($sortByFields as $f) {
$direction = match ($f['direction']) {
'asc' => FieldSort::ASC,
'desc' => FieldSort::DESC,
};
$sort = new FieldSort($f['field'], ['order' => $direction]);
$body->addSort($sort);
$sort = new FieldSort($f['field'], ['order' => $direction]);
$body->addSort($sort);
}
}
}
}

View File

@ -2,13 +2,18 @@
namespace App\Services;
use Laravel\Scout\Builder;
interface ScoutSearchService
{
/**
* Executes a search operation via Laravel Scout on the provided model class.
* @param object|string $modelClass
* @param string $q
* @return \Laravel\Scout\Builder
* @param string|null $orderByField
* @param bool $sortDirectionDescending
* @return Builder
*/
public function search(object|string $modelClass, string $q): \Laravel\Scout\Builder;
public function search(object|string $modelClass, string $q, ?string $orderByField = null,
bool $sortDirectionDescending = false): \Laravel\Scout\Builder;
}

View File

@ -3,6 +3,7 @@
namespace App\Services;
use App\JikanApiSearchableModel;
use Illuminate\Support\Str;
use Typesense\Documents;
class TypeSenseScoutSearchService implements ScoutSearchService
@ -25,9 +26,10 @@ class TypeSenseScoutSearchService implements ScoutSearchService
* @throws \Http\Client\Exception
* @throws \Typesense\Exceptions\TypesenseClientError
*/
public function search(object|string $modelClass, string $q): \Laravel\Scout\Builder
public function search(object|string $modelClass, string $q, ?string $orderByField = null,
bool $sortDirectionDescending = false): \Laravel\Scout\Builder
{
return $modelClass::search($q, function (Documents $documents, string $query, array $options) use ($modelClass) {
return $modelClass::search($q, function (Documents $documents, string $query, array $options) use ($modelClass, $orderByField, $sortDirectionDescending) {
// let's enable exhaustive search
// which will make Typesense consider all variations of prefixes and typo corrections of the words
// in the query exhaustively, without stopping early when enough results are found.
@ -57,6 +59,16 @@ class TypeSenseScoutSearchService implements ScoutSearchService
$sortBy = rtrim($sortBy, ',');
$options['sort_by'] = $sortBy;
}
// override ordering field
if (!is_null($orderByField)) {
$options['sort_by'] = "_text_match:desc,$orderByField:" . ($sortDirectionDescending ? "desc" : "asc");
}
// override overall sorting direction
if (is_null($orderByField) && $sortDirectionDescending && array_key_exists("sort_by", $options) && Str::contains($options["sort_by"], "asc")) {
$options["sort_by"] = Str::replace("asc", "desc", $options["sort_by"]);
}
}
return $documents->search($options);