mirror of
https://github.com/jikan-me/jikan-rest.git
synced 2025-02-20 11:23:35 +08:00
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:
parent
d614a663f3
commit
dd624d6872
@ -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"
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
37
app/Services/TypesenseCollectionDescriptor.php
Normal file
37
app/Services/TypesenseCollectionDescriptor.php
Normal 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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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' => [
|
||||
|
Loading…
x
Reference in New Issue
Block a user