CoolCMS API

ProductSetDocumentSource
in package
implements HighlightableDocumentSourceInterface, VisibilityConstrainedDocumentSourceInterface

FinalYes

Elasticsearch documents of e-shop product cards: one document per (set, language), one index per language (`eshop_product_set_{alias}`), `_id` = `{productSetId}_{languageId}`.

  • Only sales cards (typeId = 1) are indexed; bundles are not (the facet universe is built from sales cards only).
  • A document is a card, not a variant (counts count product sets). Parameters are read from the set's parameterRels, the same table the SQL aggregation ProductSetManager::fetchParameterValueCounts() runs on.
  • Price is not indexed. The price snapshot is keyed by (set, time, quantity, group, currency) and changes when published_time arrives without any write, so a hook would never see the change. Ordering and filtering by price stay in the DB.

IMPORTANT: whoever adds price to the document must also add a cron trigger for the nearest future published_time, otherwise the index silently goes stale.

Parameters mirror the SQL 1:1 (facet counts are compared programmatically): raw value strings, no cleanup of orphaned links and no normalization; only null and empty values are skipped, exactly like the WHERE of the aggregation query. Mapping: a keyword field params.<parameterId> per parameter (dynamic template) plus a combined keyword field pv with <parameterId>:<value> for a one-off aggregation of all pairs. value2 (the upper bound of a range) is not indexed; ranges are filtered by the DB (ParameterRelRangeFilter).

Visibility contract (an empty build means delete): a deleted (deleted=1) or deactivated (activated=0) card and a bundle drop out of the index. The publish window (publishTime/unpublishTime) and the state of the owning product are in the document only as fields and are not guards. They express time-based visibility that changes without a write, so a guard would leave the index stale (a scheduled publication would reach ES only with the nightly reindex) and EsMonitor would report false drift. The DB guards result visibility (SearchIdFilter is phaseOneOnly), so a document never leaks.

Table of Contents

Interfaces

HighlightableDocumentSourceInterface
Source that can supply highlighted snippets for search results.
VisibilityConstrainedDocumentSourceInterface
Source that can pass its visibility conditions to the ES query.

Constants

KEY  : mixed = 'eshop_product_set'
Public: the facet aggregation ({@see \App\UI\Api\Eshop\Models\Managers\ProductSetManager::fetchParameterValueCounts()}) addresses the source by this key.
INCLUDES  : mixed = ['translations', 'parameterRels']
Fetch-join includes; `buildDocuments()` needs the texts and parameters.

Properties

$em  : EntityManagerInterface
$languageManager  : LanguageManager
$languages  : array<int, string>|null
$service  : ProductSetService

Methods

__construct()  : mixed
buildDocuments()  : array<string, array<string, array<string, mixed>>>
Builds the documents of an entity as `indexBase => (_id => document)`. An empty array means the entity must not be indexed (deactivated or without an active translation), so it is deleted.
expectedDocumentBounds()  : array{min: int, max: int}
Returns the expected document count bounds, mirroring the guards of `buildDocuments()`.
fetchExistingIds()  : array<string|int, int>
Returns which of the given ids really exist in the DB; orphan cleanup deletes from ES the documents of entities no longer in the DB.
fetchFresh()  : array<int, object>
Returns fresh entities after commit (service `find` with `useCache: false`). An entity from `completedSave` is not authoritative, because `afterSave` changes the DB through `updateColumn` after the flush. A missing id is omitted.
fetchIdsChangedSince()  : array<string|int, int>
Returns ids of entities changed or created since `$since` (DB, no cache) as the input of the incremental sync. It must cover both insert and update (`insertTime` or `updateDatetime`); mutations outside the ORM (`updateColumn`, raw SQL) are never covered and the nightly full reindex catches them up.
fetchReindexBatch()  : array<int, object>
Returns a batch of entities for a full reindex by key: `id > $afterId`, ordered ascending by id, at most `$limit` (with includes, no cache). The array key is the entity id; the last one becomes the cursor the next part of the reindex continues from.
getAllDocumentIds()  : array<string, array<string|int, string>>
Returns every possible `_id` of the entity across languages as `indexBase => [_id, …]`.
getEntityIdField()  : string
Returns the ES document field holding the entity id (e.g. `articleId`); sync sorts and pages (`search_after`) by it during the orphan scan.
getHighlightFields()  : array<string, array<int, string>>
Returns the fields to highlight in search results.
getIndexBaseForLanguage()  : string|null
Returns the index base for a language alias (`cs`), null when the language does not exist.
getIndexBases()  : array<string|int, string>
Returns index base names without prefix and version; one per language for translated entities (e.g. `['blog_article_cs', 'blog_article_en']`).
getIndexDefinition()  : array<string, mixed>
Returns the index definition (settings and mappings) for index creation.
getKey()  : string
Returns the source key for the `enabledSources` allowlist and `EsSearcher`, e.g. `blog_article`.
getSearchFields()  : array<string, int|float>
Returns the `multi_match` fields with boosts, e.g. `['name' => 3, 'perex' => 2, 'content' => 1]`.
getVisibilityFilters()  : array<int, array<string, mixed>>
Returns the visibility filters that narrow the ES query: the publish window.
buildParams()  : array{0: array>, 1: list}
Builds the card parameters for facets, mirroring the SQL aggregation 1:1: the same table (`parameterRels` of the set), raw `value` strings, `parameterId` from the scalar FK column the SQL groups by, only null and empty values skipped. There is no cleanup of orphaned links, no normalization and no deduplication (a terms aggregation counts documents, so a duplicate value in the field does not change the count, just like `COUNT(DISTINCT product_set_id)` in SQL).
getLanguages()  : array<int, string>
Loads the languages once. The cache is assigned only after a successful load, so an exception from `findAll()` cannot leave it "loaded" as empty.
toIds()  : array<string|int, int>
toPlainText()  : string
`description` and `shortDescription` are editor HTML; only plain text goes into the index.

Constants

KEY

Public: the facet aggregation ({@see \App\UI\Api\Eshop\Models\Managers\ProductSetManager::fetchParameterValueCounts()}) addresses the source by this key.

public mixed KEY = 'eshop_product_set'

INCLUDES

Fetch-join includes; `buildDocuments()` needs the texts and parameters.

private mixed INCLUDES = ['translations', 'parameterRels']

Properties

Methods

buildDocuments()

Builds the documents of an entity as `indexBase => (_id => document)`. An empty array means the entity must not be indexed (deactivated or without an active translation), so it is deleted.

public buildDocuments(object $entity) : array<string, array<string, array<string, mixed>>>
Parameters
$entity : object
Return values
array<string, array<string, array<string, mixed>>>

expectedDocumentBounds()

Returns the expected document count bounds, mirroring the guards of `buildDocuments()`.

public expectedDocumentBounds() : array{min: int, max: int}

A document exists only for a sales card (typeId = 1) that is not deleted, is activated and has at least one active translation. min is the number of such cards (the dot-path join on translations is not multiplied in COUNT DISTINCT), max is min times the number of languages (an upper estimate, not every card has all languages active). The publish window is deliberately absent because it is not a guard (time-based visibility, see the class docblock); with it the monitor would report false drift whenever a card passes publishTime/unpublishTime without a write.

Return values
array{min: int, max: int}

fetchExistingIds()

Returns which of the given ids really exist in the DB; orphan cleanup deletes from ES the documents of entities no longer in the DB.

public fetchExistingIds(array<string|int, mixed> $ids) : array<string|int, int>
Parameters
$ids : array<string|int, mixed>
Return values
array<string|int, int>

fetchFresh()

Returns fresh entities after commit (service `find` with `useCache: false`). An entity from `completedSave` is not authoritative, because `afterSave` changes the DB through `updateColumn` after the flush. A missing id is omitted.

public fetchFresh(array<string|int, mixed> $ids) : array<int, object>
Parameters
$ids : array<string|int, mixed>
Return values
array<int, object> —

keyed by id

fetchIdsChangedSince()

Returns ids of entities changed or created since `$since` (DB, no cache) as the input of the incremental sync. It must cover both insert and update (`insertTime` or `updateDatetime`); mutations outside the ORM (`updateColumn`, raw SQL) are never covered and the nightly full reindex catches them up.

public fetchIdsChangedSince(DateTimeImmutable $since) : array<string|int, int>
Parameters
$since : DateTimeImmutable
Return values
array<string|int, int>

fetchReindexBatch()

Returns a batch of entities for a full reindex by key: `id > $afterId`, ordered ascending by id, at most `$limit` (with includes, no cache). The array key is the entity id; the last one becomes the cursor the next part of the reindex continues from.

public fetchReindexBatch(int $afterId, int $limit) : array<int, object>
Parameters
$afterId : int
$limit : int
Return values
array<int, object> —

keyed by id, ascending

getAllDocumentIds()

Returns every possible `_id` of the entity across languages as `indexBase => [_id, …]`.

public getAllDocumentIds(int $id) : array<string, array<string|int, string>>

It serves targeted deletion of orphan language documents (orphanRemoval on translations) and delete; delete_by_query must never be used.

Parameters
$id : int
Return values
array<string, array<string|int, string>>

getEntityIdField()

Returns the ES document field holding the entity id (e.g. `articleId`); sync sorts and pages (`search_after`) by it during the orphan scan.

public getEntityIdField() : string
Return values
string

getHighlightFields()

Returns the fields to highlight in search results.

public getHighlightFields() : array<string, array<int, string>>

Only name: the product card in the list (ProductSet/Items/_item.latte) prints just the name (.product-card__name), and a highlight field with nowhere to render would only make the ES query more expensive (number_of_fragments: 0 loads the _source of the page hits).

Both the base field and .folded are listed, because the Czech analysis has stopwords and a stemmer.

Return values
array<string, array<int, string>>

getIndexBaseForLanguage()

Returns the index base for a language alias (`cs`), null when the language does not exist.

public getIndexBaseForLanguage(string|null $languageAlias) : string|null
Parameters
$languageAlias : string|null
Return values
string|null

getIndexBases()

Returns index base names without prefix and version; one per language for translated entities (e.g. `['blog_article_cs', 'blog_article_en']`).

public getIndexBases() : array<string|int, string>
Return values
array<string|int, string>

getIndexDefinition()

Returns the index definition (settings and mappings) for index creation.

public getIndexDefinition(string $indexBase) : array<string, mixed>
Parameters
$indexBase : string
Return values
array<string, mixed>

getKey()

Returns the source key for the `enabledSources` allowlist and `EsSearcher`, e.g. `blog_article`.

public getKey() : string
Return values
string

getSearchFields()

Returns the `multi_match` fields with boosts, e.g. `['name' => 3, 'perex' => 2, 'content' => 1]`.

public getSearchFields() : array<string, int|float>
Return values
array<string, int|float>

getVisibilityFilters()

Returns the visibility filters that narrow the ES query: the publish window.

public getVisibilityFilters() : array<int, array<string, mixed>>

The class docblock explains why publishTime/unpublishTime are not guards in buildDocuments(). That is exactly why they belong here as query-time conditions: evaluated on every query, without a reindex, so the index cannot go stale.

Mirrors the two OrFilter branches of ProductSetEligibilityFilters::build() (a missing value means no restriction), hence should with must_not exists.

NOTE: deleted, activated and typeId stay out of here because the guards of buildDocuments() already keep such documents out of the index. The visibility of the owning product stays in the DB too: the document has only productId, not the product state. The facet path is not affected: EsSearcher::facetCounts() builds its own query and does not read this interface.

Return values
array<int, array<string, mixed>>

buildParams()

Builds the card parameters for facets, mirroring the SQL aggregation 1:1: the same table (`parameterRels` of the set), raw `value` strings, `parameterId` from the scalar FK column the SQL groups by, only null and empty values skipped. There is no cleanup of orphaned links, no normalization and no deduplication (a terms aggregation counts documents, so a duplicate value in the field does not change the count, just like `COUNT(DISTINCT product_set_id)` in SQL).

private buildParams(ProductSet $entity) : array{0: array>, 1: list}
Parameters
$entity : ProductSet
Return values
array{0: array>, 1: list} —

[params, pv]

getLanguages()

Loads the languages once. The cache is assigned only after a successful load, so an exception from `findAll()` cannot leave it "loaded" as empty.

private getLanguages() : array<int, string>
Return values
array<int, string> —

languageId => alias

toIds()

private toIds(array<string|int, object> $entities) : array<string|int, int>
Parameters
$entities : array<string|int, object>
Return values
array<string|int, int>

toPlainText()

`description` and `shortDescription` are editor HTML; only plain text goes into the index.

private toPlainText(string|null $html) : string
Parameters
$html : string|null
Return values
string
On this page

Search results