SearchTrait
Fulltext search as an Api endpoint.
Module-neutral: a module only implements the abstract hooks below.
Two paths, one endpoint:
- search without
orderBys: ES ranks (score), paginates (from/size) and counts (track_total_hits);fromEsistrue; - search with
orderBys(manual user choice): the database ranks, paginates and counts;fromEsisfalse; - ES disabled, down, breaker open or a short query: the database does everything;
fromEsisfalse.
Relevance is the default order only for a filled search; a manual order (newest, cheapest) overrides the score. If ES paginated in the manual order too, the user would get "newest" only within a page of relevant hits, so in that branch only the id set is taken from ES (cap EsSearcher::MAX_IDS) and the database orders, paginates and counts.
Visibility stays in the database: the caller $filters (eligibility: activated,
published <= now, categories, paid access) apply on both paths and ES ids are only narrowing
candidates. A stale index therefore never leaks an invisible entity; on the relevance path it can
at most shorten a page, and the ES totalCount is an upper estimate (documents of invisible
entities; guarded by the nightly reindex and EsMonitor).
Response contract: the same as internalActionGetAll() ({items, totalCount, _references} from
extractAll) plus the keys:
fromEs:truemeans the page order andtotalCountcome from ES. The front end learns from it that the list is ordered by relevance; configuration says nothing about ES availability.highlights:entity id => field => HTML snippet. Values are ready, safe HTML (EsHighlighter): the whole text is escaped and the only tags are<em>/</em>without attributes, so a template may print them with|noescape. The key is always present; outside the relevance path it is empty (manual order and ES outage give a list without highlighting, never an error).
IMPORTANT: the query cache is deliberately not used. SearchIdFilter is a
NonCacheableFilter (the key would have to contain the whole id set) and the ES answer changes
without a database write (reindex, score change); a cache would serve stale orders.
NOTE: no external HTTP action is introduced (like parameterCounts/currentPrices); the
endpoint serves the front end through ApiBridge::internalCall. The RBAC guard is still in
internalAction*(), so it applies to both paths.
Table of Contents
Constants
- SEARCH_DEFAULT_LIMIT : mixed = 20
- Fallback page size when the caller sends no `limit`.
Methods
- internalActionSearch() : array<string|int, mixed>
- Endpoint `api/{module}/{entity}/search` (internal bridge to `internalActionSearch`).
- getEsSearcher() : EsSearcher
- getEsSourceKey() : string
- Key of the module {@see \App\Core\Integrations\Elasticsearch\Sources\DocumentSourceInterface}.
- getFulltextFallbackFields() : array<int, string>
- Returns the fields for the MySQL `MATCH ... AGAINST` fallback (without ES). They must match the fields the source indexes, otherwise results diverge between the paths.
- getFulltextTranslationsPath() : string|null
- Returns the path to translations when the fallback fields live in them (`translations`); fulltext is then limited to the request language ({@see TranslationScopeFilters}). `null` means the source is not translated (advert, message). A presenter of a translated source overrides it.
- highlightsForItems() : array<int, array<string, string>>
- Narrows the ES snippet map to ids that are really in the extracted list.
- itemId() : int|null
- Returns the id of an extracted item. `extractAll` emits an item as a reference `['id' => <scalar|[field => value]>]` (see `OrmEntityHydrator::scalarizedIdentifierValues`): a single-column identity arrives wrapped in an array, a composite identity as several pairs (unsupported, `null`).
- orderItemsByIds() : array<int, mixed>
- Reorders extracted items into the order ES returned the ids (i.e. by score).
- resolveSearchLanguageId() : int|null
- Returns the language id for the request alias; an unknown alias gives `null` (no restriction).
Constants
SEARCH_DEFAULT_LIMIT
Fallback page size when the caller sends no `limit`.
private
mixed
SEARCH_DEFAULT_LIMIT
= 20
Methods
internalActionSearch()
Endpoint `api/{module}/{entity}/search` (internal bridge to `internalActionSearch`).
public
internalActionSearch([string $search = '' ][, string|null $languageAlias = null ][, array<int, Filter|array<string, mixed>> $filters = [] ][, array<string, mixed>|null $orderBys = null ][, int|null $offset = null ][, int|null $limit = null ][, string|array<int, string> $includes = [] ]) : array<string|int, mixed>
Parameters
- $search : string = ''
-
user query (empty or shorter than 2 characters: ES is not called at all)
- $languageAlias : string|null = null
-
language alias (
cs); the index is per language, an unknown alias falls back - $filters : array<int, Filter|array<string, mixed>> = []
-
visibility/context filters (ready objects or arrays for FilterBuilder)
- $orderBys : array<string, mixed>|null = null
-
manual user order;
nullmeans relevance orders - $offset : int|null = null
- $limit : int|null = null
- $includes : string|array<int, string> = []
Return values
array<string|int, mixed> —{items, totalCount, _references, fromEs, highlights}
from extractAll, or the guard DENY envelope ({success: false, ...})
getEsSearcher()
protected
abstract getEsSearcher() : EsSearcher
Return values
EsSearchergetEsSourceKey()
Key of the module {@see \App\Core\Integrations\Elasticsearch\Sources\DocumentSourceInterface}.
protected
abstract getEsSourceKey() : string
Return values
stringgetFulltextFallbackFields()
Returns the fields for the MySQL `MATCH ... AGAINST` fallback (without ES). They must match the fields the source indexes, otherwise results diverge between the paths.
protected
abstract getFulltextFallbackFields() : array<int, string>
Return values
array<int, string>getFulltextTranslationsPath()
Returns the path to translations when the fallback fields live in them (`translations`); fulltext is then limited to the request language ({@see TranslationScopeFilters}). `null` means the source is not translated (advert, message). A presenter of a translated source overrides it.
protected
getFulltextTranslationsPath() : string|null
Return values
string|nullhighlightsForItems()
Narrows the ES snippet map to ids that are really in the extracted list.
private
static highlightsForItems(array<int, array<string, string>> $highlights, array<int, mixed> $items) : array<int, array<string, string>>
IMPORTANT: ES highlights all its hits, but visibility is held by the database: a stale
document of a deactivated article drops out of items, yet its title and perex would stay in
the response without this filter, i.e. text of an entity the caller must not see. The
condition is "the item is in the list", not "the id is in ES".
Parameters
- $highlights : array<int, array<string, string>>
- $items : array<int, mixed>
Return values
array<int, array<string, string>>itemId()
Returns the id of an extracted item. `extractAll` emits an item as a reference `['id' => <scalar|[field => value]>]` (see `OrmEntityHydrator::scalarizedIdentifierValues`): a single-column identity arrives wrapped in an array, a composite identity as several pairs (unsupported, `null`).
private
static itemId(mixed $item) : int|null
Parameters
- $item : mixed
Return values
int|nullorderItemsByIds()
Reorders extracted items into the order ES returned the ids (i.e. by score).
private
static orderItemsByIds(array<int, mixed> $items, array<int, int> $ids) : array<int, mixed>
The hydration phase 2 does not keep order (hydrateByIdentifiers orders as the database
returns). The semantics match sortResultsByIdentifiers() (repository), only over an already
extracted payload, so it needs no entity metadata. An item whose id is not in the ES order
(composite PK, unexpected shape) keeps its relative order after the known ones and never vanishes.
Parameters
- $items : array<int, mixed>
- $ids : array<int, int>
Return values
array<int, mixed>resolveSearchLanguageId()
Returns the language id for the request alias; an unknown alias gives `null` (no restriction).
private
resolveSearchLanguageId(string|null $languageAlias) : int|null
Parameters
- $languageAlias : string|null