CoolCMS API

EsSearcher
in package

FinalYes

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

Methods

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
EsFacetCountsResult

searchIds()

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
EsSearchResult

searchPaged()

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 + size must 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
EsSearchResult

buildQuery()

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 hits block 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 hits block 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
Return values
bool
On this page

Search results