alive = $builder->default()->ping()->asBool(); } catch (Exception $exception) { Log::error($exception->getMessage()); $this->alive = false; } } /** * Is the ES instance reachable? */ public function isAlive(): bool { return $this->alive; } /** * Should the search be performed? */ public function shouldSearch(): bool { return $this->isAlive(); } /** * Perform the search. */ public function searchViaJSONAPI( Query $query, EloquentSchema $schema, PaginationStrategy $paginationStrategy ): Collection|Paginator { $elasticSchema = $this->elasticSchema($schema); // initialize builder for matches $searchBuilder = static::elasticQuery($elasticSchema->model()::class)->build($this->criteria); $this->elasticBuilder = $searchBuilder; // load aggregate fields $this->elasticBuilder->refineModels(function (EloquentBuilder $searchModelBuilder) use ($query, $schema): void { $this->withAggregates($searchModelBuilder, $query, $schema); }); // eager load relations with constraints $this->elasticBuilder = $this->elasticBuilder->load($this->constrainEagerLoads($query, $schema)); // apply filters $filterBuilder = new BoolQueryBuilder(); $scope = ScopeParser::parse($schema->type()); foreach ($query->getFilterCriteria() as $filterCriterion) { $elasticFilterCriteria = FilterParser::parse($filterCriterion); if ($elasticFilterCriteria instanceof Criteria) { foreach ($elasticSchema->filters() as $filter) { if ($filterCriterion->shouldFilter($filter, $scope)) { $filterBuilder = $elasticFilterCriteria->filter($filterBuilder, $filter, $query); } } } } try { $this->elasticBuilder->postFilter($filterBuilder); } catch (QueryBuilderValidationException) { // There doesn't appear to be a way to check if any filters have been set in the filter builder } // apply sorts $sorts = []; foreach ($query->getSortCriteria() as $sortCriterion) { $elasticSortCriteria = SortParser::parse($sortCriterion); if ($elasticSortCriteria instanceof Api\Criteria\Sort\Criteria) { foreach ($elasticSchema->sorts() as $sort) { if ($sortCriterion->shouldSort($sort, $scope)) { $sorts[] = $elasticSortCriteria->sort($sort); } } } } if ($sorts !== []) { $this->elasticBuilder->sortRaw($sorts); } // paginate $paginationCriteria = $query->getPagingCriteria($paginationStrategy); $elasticPaginationCriteria = PagingParser::parse($paginationCriteria); return $elasticPaginationCriteria instanceof Api\Criteria\Paging\Criteria ? $elasticPaginationCriteria->paginate($this->elasticBuilder) : $this->elasticBuilder->execute()->models(); } /** * @param Closure(EloquentBuilder): void $callback * @param array $sorts */ public function search(?Closure $callback = null, int $perPage = 15, int $page = 1, array $sorts = []): LengthAwarePaginator { throw_unless($this->model instanceof Model, RuntimeException::class, 'Model must be provided for search.'); $searchBuilder = static::elasticQuery($this->model::class)->build($this->criteria); $this->elasticBuilder = $searchBuilder; // Resolve callback. if ($callback instanceof Closure) { $this->elasticBuilder->refineModels($callback); } // Resolve sorting. $sortRaw = []; foreach ($sorts as $column => $data) { $nested = Arr::get($data, 'direction'); if (Arr::get($data, 'isString') === true) { if ($relation = Arr::get($data, 'relation')) { $column = $relation.'.'.$column.'_keyword'; $nested = [ 'order' => $nested, 'nested' => [ 'path' => $relation, ], ]; } else { $column .= '.keyword'; } } $sortRaw[$column] = $nested; } if ($sortRaw !== []) { $this->elasticBuilder->sortRaw($sortRaw); } return $this->elasticBuilder->paginate($perPage, page: $page)->onlyModels(); } /** * @param class-string $model */ public static function elasticQuery(string $model): ElasticQuery { return match ($model) { Playlist::class => new PlaylistQuery(), AnimeThemeEntry::class => new EntryQuery(), AnimeTheme::class => new ThemeQuery(), Anime::class => new AnimeQuery(), Artist::class => new ArtistQuery(), Series::class => new SeriesQuery(), Song::class => new SongQuery(), Studio::class => new StudioQuery(), Synonym::class => new SynonymQuery(), Video::class => new VideoQuery(), default => throw new RuntimeException("No ElasticQuery defined for model: {$model}"), }; } /** * Resolve Elasticsearch schema from Eloquent schema. */ private function elasticSchema(EloquentSchema $schema): Schema { $elasticSchemaClass = Str::of($schema::class) ->replace('Http', 'Scout\\Elasticsearch') ->__toString(); return new $elasticSchemaClass(); } }