added improvements for search

- better typesense 0.24.1 support
- exhaustive search disabled by default
- central place for the MAX_RESULTS_PER_PAGE option
- added class for getting searchable attributes of models in typesense
This commit is contained in:
pushrbx 2023-06-26 18:42:51 +01:00
parent d614a663f3
commit dd624d6872
11 changed files with 124 additions and 17 deletions

View File

@ -381,13 +381,17 @@ class Anime extends JikanApiSearchableModel
{
return [
[
"field" => "_text_match",
"field" => "_text_match(buckets:" . App::make("jikan-config")->maxResultsPerPage() . ")",
"direction" => "desc"
],
[
"field" => "members",
"direction" => "desc"
"field" => "popularity",
"direction" => "asc"
],
[
"field" => "rank",
"direction" => "asc"
]
];
}

View File

@ -2,11 +2,13 @@
namespace App\Concerns;
use Illuminate\Support\Facades\App;
trait ResolvesPaginatorParams
{
private function getPaginatorParams(?int $limit = null, ?int $page = null): array
{
$default_max_results_per_page = env('MAX_RESULTS_PER_PAGE', 25);
$default_max_results_per_page = App::make("jikan-config")->maxResultsPerPage();
$limit = $limit ?? $default_max_results_per_page;
$page = $page ?? 1;

View File

@ -4,6 +4,7 @@ namespace App\Dto\Concerns;
use Illuminate\Support\Collection;
use Illuminate\Support\Env;
use Illuminate\Support\Facades\App;
use \ReflectionClass;
use Spatie\LaravelData\Support\DataConfig;
@ -19,8 +20,8 @@ trait PreparesData
// let's always set the limit parameter to the globally configured default value
if (property_exists(static::class, "limit") && !$properties->has("limit")) {
/** @noinspection PhpUndefinedFieldInspection */
$properties->put("limit", Env::get("MAX_RESULTS_PER_PAGE",
property_exists(static::class, "defaultLimit") ? static::$defaultLimit : 25));
$properties->put("limit", App::make("jikan-config")->maxResultsPerPage(
property_exists(static::class, "defaultLimit") ? static::$defaultLimit : null));
}
// we want to cast "true" and "false" string values to boolean before validation, so let's take all properties

View File

@ -298,13 +298,17 @@ class Manga extends JikanApiSearchableModel
{
return [
[
"field" => "_text_match",
"field" => "_text_match(buckets:" . App::make("jikan-config")->maxResultsPerPage() . ")",
"direction" => "desc"
],
[
"field" => "members",
"direction" => "desc"
"field" => "popularity",
"direction" => "asc"
],
[
"field" => "rank",
"direction" => "asc"
]
];
}

View File

@ -81,11 +81,15 @@ class AppServiceProvider extends ServiceProvider
public function register(): void
{
$this->app->singleton(JikanConfig::class, fn() => new JikanConfig(config("jikan")));
$this->app->alias(JikanConfig::class, "jikan-config");
// cache options class is used to share the request scope level cache settings
$this->app->singleton(CacheOptions::class);
$this->app->singleton(CachedScraperService::class, DefaultCachedScraperService::class);
$this->app->singleton(PrivateFieldMapperService::class, DefaultPrivateFieldMapperService::class);
$this->app->bind(QueryBuilderPaginatorService::class, DefaultBuilderPaginatorService::class);
if (static::getSearchIndexDriver($this->app) === "typesense") {
$this->app->singleton(App\Services\TypesenseCollectionDescriptor::class);
}
$this->registerModelRepositories();
$this->registerRequestHandlers();
}
@ -101,7 +105,10 @@ class AppServiceProvider extends ServiceProvider
default => DefaultScoutSearchService::class
};
$scoutSearchService = new $serviceClass($repository);
$scoutSearchService = $this->app->make($serviceClass, [
"repository" => $repository,
"config" => $this->app->make("jikan-config")
]);
$result = new SearchEngineSearchService($scoutSearchService, $repository);
}
else {

View File

@ -5,12 +5,13 @@ namespace App\Rules\Attributes;
use App\Rules\MaxResultsPerPageRule;
use Attribute;
use Spatie\LaravelData\Attributes\Validation\Rule;
use Illuminate\Support\Facades\App;
#[Attribute(Attribute::TARGET_PROPERTY)]
final class MaxLimitWithFallback extends Rule
{
public function __construct()
{
parent::__construct(new MaxResultsPerPageRule());
parent::__construct(new MaxResultsPerPageRule(App::make("jikan-config")->maxResultsPerPage()));
}
}

View File

@ -4,6 +4,7 @@ namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
use Illuminate\Support\Env;
use Illuminate\Support\Facades\App;
final class MaxResultsPerPageRule implements Rule
{
@ -41,6 +42,6 @@ final class MaxResultsPerPageRule implements Rule
private function maxResultsPerPage(): int
{
return (int) Env::get("MAX_RESULTS_PER_PAGE", $this->fallbackLimit);
return (int) App::make("jikan-config")->maxResultsPerPage($this->fallbackLimit);
}
}

View File

@ -4,16 +4,19 @@ namespace App\Services;
use App\Contracts\Repository;
use App\JikanApiSearchableModel;
use App\Support\JikanConfig;
use Illuminate\Support\Str;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\App;
use Typesense\Documents;
class TypeSenseScoutSearchService implements ScoutSearchService
{
private int $maxItemsPerPage;
public function __construct(private readonly Repository $repository)
public function __construct(private readonly Repository $repository, JikanConfig $config)
{
$this->maxItemsPerPage = (int) env('MAX_RESULTS_PER_PAGE', 25);
$this->maxItemsPerPage = (int) $config->maxResultsPerPage();
if ($this->maxItemsPerPage > 250) {
$this->maxItemsPerPage = 250;
}
@ -33,11 +36,13 @@ class TypeSenseScoutSearchService implements ScoutSearchService
// 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.
$options['exhaustive_search'] = env('TYPESENSE_SEARCH_EXHAUSTIVE', "true");
$options['exhaustive_search'] = env('TYPESENSE_SEARCH_EXHAUSTIVE', "false");
$options['search_cutoff_ms'] = (int) env('TYPESENSE_SEARCH_CUTOFF_MS', 450);
// this will be ignored together with exhaustive_search set to "true"
$options['drop_tokens_threshold'] = (int) env('TYPESENSE_DROP_TOKENS_THRESHOLD', 1);
$options['typo_tokens_threshold'] = (int) env('TYPESENSE_TYPO_TOKENS_THRESHOLD', 1);
$options['enable_highlight_v1'] = 'false';
$options['infix'] = 'fallback';
// prevent `Could not parse the filter query: unbalanced `&&` operands.` error
// this adds support for typesense v0.24.1
if (array_key_exists('filter_by', $options) && ($options['filter_by'] === ' && ' || $options['filter_by'] === '&&')) {
@ -48,6 +53,16 @@ class TypeSenseScoutSearchService implements ScoutSearchService
$options['per_page'] = min($this->maxItemsPerPage, 250);
}
// skip typo checking for short queries
if (strlen($query) <= 3) {
$options['num_typos'] = 0;
$options['typo_tokens_threshold'] = 0;
$options['drop_tokens_threshold'] = 0;
$options['exhaustive_search'] = 'false';
$options['infix'] = 'off';
$options['prefix'] = 'false';
}
$modelInstance = $this->repository->createEntity();
// get the weights of the query_by fields, if they are provided by the model.
if ($modelInstance instanceof JikanApiSearchableModel) {
@ -69,9 +84,35 @@ class TypeSenseScoutSearchService implements ScoutSearchService
$options['sort_by'] = $sortBy;
}
// todo: try to avoid service lookup, resolve things via constructor instead.
// this is currently a workaround as the search service resolution in the service provider is complex,
// and it gives errors when you try to resolve the Typesense class from the LaraveTypesense driver package.
// here we'd like to get all the searchable attributes of the model, so we can override the sort order.
// we use these attribute names to validate the incoming field name against them, otherwise ignoring them.
$collectionDescriptor = App::make(TypesenseCollectionDescriptor::class);
$modelAttrNames = $collectionDescriptor->getSearchableAttributes($modelInstance);
// fixme: this shouldn't be here, but it's a quick fix for the time being
if ($orderByField === "aired.from") {
$orderByField = "start_date";
}
if ($orderByField === "aired.to") {
$orderByField = "end_date";
}
if ($orderByField === "published.from") {
$orderByField = "start_date";
}
if ($orderByField === "published.to") {
$orderByField = "end_date";
}
// fixme end
// override ordering field
if (!is_null($orderByField)) {
$options['sort_by'] = "$orderByField:" . ($sortDirectionDescending ? "desc" : "asc") . ",_text_match:desc";
if (!is_null($orderByField) && Arr::has($modelAttrNames, $orderByField)) {
$options['sort_by'] = "$orderByField:" . ($sortDirectionDescending ? "desc" : "asc") . ",_text_match(buckets:".$this->maxItemsPerPage."):desc";
}
// override overall sorting direction

View File

@ -0,0 +1,37 @@
<?php
namespace App\Services;
use Typesense\LaravelTypesense\Typesense;
use App\JikanApiSearchableModel;
use Typesense\Collection as TypesenseCollection;
use Illuminate\Support\Collection;
/**
* A service which helps to describe a collection in Typesense based on an eloquent model
*/
final class TypesenseCollectionDescriptor
{
private readonly Collection $cache;
public function __construct(private readonly Typesense $typesense)
{
$this->cache = collect();
}
public function getSearchableAttributes(JikanApiSearchableModel $model): array
{
$modelSearchableAs = $model->searchableAs();
if ($this->cache->has($modelSearchableAs)) {
return $this->cache->get($modelSearchableAs);
}
/**
* @var TypesenseCollection $collection
*/
$collection = $this->typesense->getCollectionIndex($model);
$collectionDetails = $collection->retrieve();
$fields = collect($collectionDetails["fields"]);
$searchableAttributeNames = $fields->pluck("name")->all();
$this->cache->put($modelSearchableAs, $searchableAttributeNames);
return $searchableAttributeNames;
}
}

View File

@ -18,12 +18,15 @@ final class JikanConfig
private bool $microCachingEnabled;
private Collection $config;
public function __construct(array $config)
{
$config = collect($config);
$this->perEndpointCacheTtl = $config->get("per_endpoint_cache_ttl", []);
$this->defaultCacheExpire = $config->get("default_cache_expire", 0);
$this->microCachingEnabled = in_array($config->get("micro_caching_enabled", false), [true, 1, "1", "true"]);
$this->config = $config;
}
public function cacheTtlForEndpoint(string $endpoint): ?int
@ -40,4 +43,9 @@ final class JikanConfig
{
return $this->microCachingEnabled;
}
public function maxResultsPerPage(?int $defaultValue = null): int
{
return $this->config->get("max_results_per_page", $defaultValue ?? 25);
}
}

View File

@ -1,6 +1,7 @@
<?php
return [
'max_results_per_page' => env('MAX_RESULTS_PER_PAGE', 25),
'micro_caching_enabled' => env('MICROCACHING', false),
'default_cache_expire' => env('CACHE_DEFAULT_EXPIRE', 86400),
'per_endpoint_cache_ttl' => [