EsSearcher
in package
Reads full-text search results from ES.
Returns only ordered ids (plus the total count); hydration and visibility stay in the DB
(two-phase fetch, SearchIdFilter phase one only). Fail-open: an ES outage opens the circuit
breaker (see EsCircuitBreaker) and for its duration unavailable() is returned without
waiting for timeouts, so the caller switches to the MySQL FulltextFilter.
Table of Contents
Constants
- MAX_FACET_BUCKETS : mixed = 10000
- Cap of the `size` of the terms aggregation. When the buckets do not fit (`sum_other_doc_count > 0`) the answer is incomplete and `unavailable` is returned: incomplete counts would silently lie, while the SQL fallback is always exact.
- MAX_IDS : mixed = 10000
- ES `from + size` window limit; a deeper page above it returns an error.
- MIN_QUERY_LENGTH : mixed = 2
Properties
- $circuitBreaker : EsCircuitBreaker
- $client : Client
- $config : EsConfig
- $sources : array<string, DocumentSourceInterface>
Methods
- __construct() : mixed
- facetCounts() : EsFacetCountsResult
- Returns facet counts in one query: a `terms` aggregation over a keyword field, narrowed to the given entities.
- searchIds() : EsSearchResult
- searchPaged() : EsSearchResult
- Searches one page ordered by relevance.
- buildQuery() : array<string, mixed>
- Builds the `query` body of both search methods: `multi_match` (never `query_string`), wrapped in `bool` with `filter` clauses when the source asks for visibility pre-narrowing ({@see VisibilityConstrainedDocumentSourceInterface}).
- extractEntityIds() : array<int, int>
- Extracts entity ids from `_id` (`{entityId}_{languageId}`) in the order the hits came (by score). Shared by `searchIds()` and `searchPaged()`.
- extractHighlights() : array<int, array<string, string>>
- Extracts highlighted snippets of the hits as entity id, then field, then safe HTML.
- isPartial() : bool
- Detects a partial answer.
Constants
MAX_FACET_BUCKETS
Cap of the `size` of the terms aggregation. When the buckets do not fit (`sum_other_doc_count > 0`) the answer is incomplete and `unavailable` is returned: incomplete counts would silently lie, while the SQL fallback is always exact.
public
mixed
MAX_FACET_BUCKETS
= 10000
MAX_IDS
ES `from + size` window limit; a deeper page above it returns an error.
public
mixed
MAX_IDS
= 10000
MIN_QUERY_LENGTH
private
mixed
MIN_QUERY_LENGTH
= 2
Properties
$circuitBreaker read-only
private
EsCircuitBreaker
$circuitBreaker
$client read-only
private
Client
$client
$config read-only
private
EsConfig
$config
$sources
private
array<string, DocumentSourceInterface>
$sources
= []
Methods
__construct()
public
__construct(EsConfig $config, Client $client, EsCircuitBreaker $circuitBreaker[, array<string|int, DocumentSourceInterface> $sources = [] ]) : mixed
Parameters
- $config : EsConfig
- $client : Client
- $circuitBreaker : EsCircuitBreaker
- $sources : array<string|int, DocumentSourceInterface> = []
facetCounts()
Returns facet counts in one query: a `terms` aggregation over a keyword field, narrowed to the given entities.
public
facetCounts(string $sourceKey, string|null $languageAlias, array<string|int, int> $entityIds, string $field) : EsFacetCountsResult
ES does not decide visibility: $entityIds are already DB-resolved visible ids (the single
source of truth) and ES only replaces the aggregating SQL.
IMPORTANT: fromEs = true promises exactness, not just availability. unavailable is
returned also when:
sum_other_doc_count > 0(the buckets did not fit into self::MAX_FACET_BUCKETS, so the counts would be incomplete),- the number of matched documents differs from
count($entityIds)(the index lacks a document for some visible id, e.g. a stale index after hooks failed or a missing active language translation; SQL would count such entities and ES would not, and the counts would silently diverge).
Parameters
- $sourceKey : string
- $languageAlias : string|null
- $entityIds : array<string|int, int>
-
DB-resolved visible ids (non-empty, deduplicated)
- $field : string
-
keyword field of the aggregation (e.g.
pv=<parameterId>:<value>)
Return values
EsFacetCountsResultsearchIds()
public
searchIds(string $sourceKey, string|null $languageAlias, string $search[, int $offset = 0 ][, int|null $limit = null ]) : EsSearchResult
Parameters
- $sourceKey : string
- $languageAlias : string|null
- $search : string
- $offset : int = 0
- $limit : int|null = null
Return values
EsSearchResultsearchPaged()
Searches one page ordered by relevance.
public
searchPaged(string $sourceKey, string|null $languageAlias, string $search[, int $from = 0 ][, int $size = 20 ]) : EsSearchResult
Unlike searchIds(), which returns the whole (capped) id set and leaves paging and ordering
to the DB, ES returns only one page of ids already ordered by score plus the exact total of
matches (track_total_hits: true; the default cap of 10 000 would lie to the paginator).
The query is always multi_match over the source fields (name^3, perex^2, content),
never query_string: user input must not become query syntax.
A source implementing HighlightableDocumentSourceInterface also gets
EsSearchResult::$highlights: escaped HTML with <em>/</em> as the only allowed
tags (EsHighlighter). Other sources send an identical query without highlights.
IMPORTANT: this method does not resolve visibility. The returned ids are candidates and
eligibility is decided by DB phase one over SearchIdFilter.
A stale document therefore never leaks an invisible entity; at most it costs a slot in
the page and totalCount can be higher by such entities (an upper estimate).
Parameters
- $sourceKey : string
- $languageAlias : string|null
- $search : string
- $from : int = 0
-
page offset (0-based)
- $size : int = 20
-
page size;
from + sizemust not exceed self::MAX_IDS (the ES window limit; a deeper page would end with a 4xx, so the DB path that can reach it is used instead)
Return values
EsSearchResultbuildQuery()
Builds the `query` body of both search methods: `multi_match` (never `query_string`), wrapped in `bool` with `filter` clauses when the source asks for visibility pre-narrowing ({@see VisibilityConstrainedDocumentSourceInterface}).
private
static buildQuery(DocumentSourceInterface $source, string $search, array<int, string> $fields) : array<string, mixed>
filter (not must) matters: the clauses only narrow and do not affect the score, so the
relevance order stays exactly as without them. A source without the interface gets a bare
multi_match.
Parameters
- $source : DocumentSourceInterface
- $search : string
- $fields : array<int, string>
-
fields with boosts (
name^3)
Return values
array<string, mixed>extractEntityIds()
Extracts entity ids from `_id` (`{entityId}_{languageId}`) in the order the hits came (by score). Shared by `searchIds()` and `searchPaged()`.
private
static extractEntityIds(array<string|int, mixed> $hits) : array<int, int>
Parameters
- $hits : array<string|int, mixed>
-
the
hitsblock of the ES response
Return values
array<int, int>extractHighlights()
Extracts highlighted snippets of the hits as entity id, then field, then safe HTML.
private
static extractHighlights(array<string|int, mixed> $hits, array<string, array<int, string>> $highlightFields) : array<int, array<string, string>>
The conversion is done by EsHighlighter; the raw ES fragment never leaves the searcher,
so nobody further down the chain can skip escaping. A hit without a match in the highlighted
fields (the match was only in content) is absent from the map, and the caller prints the
original text from the DB.
Parameters
- $hits : array<string|int, mixed>
-
the
hitsblock of the ES response - $highlightFields : array<string, array<int, string>>
Return values
array<int, array<string, string>>isPartial()
Detects a partial answer.
private
static isPartial(array<string, mixed> $response, string $sourceKey, string $context) : bool
HTTP 200 does not mean a complete answer: timed_out: true (server-side search limit) or
_shards.failed > 0 (a shard failed; allow_partial_search_results defaults to true)
returns only part of the matches or nothing. Such an answer must not pass as an exact result
(it would drop matches from the DB phase and lie to the paginator), so unavailable is
returned and the DB fallback runs. The breaker is not opened: the cluster is alive.
Parameters
- $response : array<string, mixed>
- $sourceKey : string
- $context : string