CoolCMS API

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); fromEs is true;
  • search with orderBys (manual user choice): the database ranks, paginates and counts; fromEs is false;
  • ES disabled, down, breaker open or a short query: the database does everything; fromEs is false.

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: true means the page order and totalCount come 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; null means 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, ...})

getEsSourceKey()

Key of the module {@see \App\Core\Integrations\Elasticsearch\Sources\DocumentSourceInterface}.

protected abstract getEsSourceKey() : string
Return values
string

getFulltextFallbackFields()

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|null

highlightsForItems()

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|null

orderItemsByIds()

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
Return values
int|null
On this page

Search results