ProductSetDocumentSource
in package
implements
HighlightableDocumentSourceInterface, VisibilityConstrainedDocumentSourceInterface
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_timearrives 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
$em read-only
private
EntityManagerInterface
$em
$languageManager read-only
private
LanguageManager
$languageManager
$languages
private
array<int, string>|null
$languages
= null
languageId => alias
$service read-only
private
ProductSetService
$service
Methods
__construct()
public
__construct(ProductSetService $service, LanguageManager $languageManager, EntityManagerInterface $em) : mixed
Parameters
- $service : ProductSetService
- $languageManager : LanguageManager
- $em : EntityManagerInterface
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
stringgetHighlightFields()
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|nullgetIndexBases()
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
stringgetSearchFields()
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[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