Caching
Embedding an element costs resources and an HTTP round trip to a model. The bundle therefore treats vectors as reusable content: re-indexing unchanged content performs zero inference calls, including across a full index rebuild.
Where vectors live
Vectors are stored in two places: the element's search index document, next to its regular
fields, and a durable MySQL/MariaDB cache table (bundle_backend_power_tools_embedding_cache).
Each stored vector, in either place, carries an identity key (ck) alongside it — a hash of:
model config id | cache_version | serving_version | the exact embedded input
The key is the cache key. Whenever a document is composed, the bundle knows which key each vector should have and reuses any vector whose key still matches.
model config id is the key you chose under models: — not the model name sent to the service.
Deliberately not in the key: model, endpoint, provider, dimension, and prefixes.query.
Changing any of those does not invalidate anything by itself, which is what serving_version is for
(see Bypassing the cache).
The resolution ladder
Composing a document consults up to two sources before deciding to embed, once per element per batch — a miss is the outcome when neither has the vector, not a third source:
- Self-copy — a realtime read of the element's own current document. This covers the common case: an element is saved, one field changed, and every unaffected chunk keeps its vector.
- The cache table — one keyed read (
getMany) against the durable cache table for everyckstill missing after self-copy. Theckis the cache key, so a vector cached once satisfies every other request for the same content — not just the element that first produced it, and not just during recreation. This is what makes a full index rebuild free. - Miss — anything still unresolved is queued for generation, asynchronously.
A failure at any rung degrades to a miss: a lookup problem costs inference calls, never a broken index. Missing vectors never break search — the element is indexed without them, generation is re-enqueued, and the vectors are written into the document (and the cache table) once generated.
When vectors are regenerated
Exactly when the identity key changes:
| Change | Effect |
|---|---|
| Element content edit | only the changed chunks/representations re-embed |
prefixes.document change | all of that model's text vectors re-embed (it is part of the embedded input) |
| Renaming the model's config id | all of that model's vectors re-embed (a new id is a new key) |
serving_version bump | that model's vectors re-embed |
cache_version bump | every vector re-embeds |
| Index recreation | nothing — vectors are carried across (see below) |
quantization change | nothing — a reindex re-encodes the same vectors |
prefixes.query change | nothing stored re-embeds — it only re-keys the query cache below |
Swapping model / endpoint / provider under the same config id | nothing — bump serving_version yourself |
| Editing the model's thumbnail definition | all of that model's image vectors re-embed (its pixel-affecting settings are fingerprinted into the key) |
A version-aware key means a stale vector cannot be restored for the inputs the key covers: after a content, prefix, or version change, the keys no longer match, so exactly the affected vectors miss and re-embed and nothing else does.
Both of these alter the vectors a service returns while leaving the key identical, so stored vectors
stay silently stale until you bump that model's serving_version:
- Swapping the served model (
model,endpoint, orprovider) under the same config id. If the replacement has a differentdimensionthe mismatch is rejected loudly; a same-dimension swap is the dangerous case, because the field then mixes vectors from two latent spaces and only shows up as confidently wrong results.
Image keys include a fingerprint of the pixel-affecting settings of the model's thumbnail definition
(format, quality, transformations, high-resolution flags). Resize or re-encode it and exactly those
images re-embed on the next indexing pass — no serving_version bump needed. Cosmetic edits
(description, group) are deliberately excluded and change nothing. If the definition cannot be read at
all, the fingerprint folds to empty rather than invalidating a catalogue.
Index recreation
Use the BPT wrapper. The new indices start empty, so self-copy finds nothing there — every
element's vector instead repopulates from the durable cache table as the queue drains, which is
why recreation over unchanged content costs no inference calls. The wrapper additionally holds a
MySQL advisory lock (bpt_embeddings_recreate) so two recreations cannot overlap:
bin/console bpt:embeddings:recreate-indices
It refuses to run while embeddings are disabled (enabled: false) — recreating then would leave
every vector empty with nothing to repopulate from — and points at the plain command instead:
bin/console generic-data-index:update:index -r
To force a full re-embed instead of repopulating from the cache:
bin/console bpt:embeddings:recreate-indices --fresh
--fresh (-f) flushes the entire cache table before recreating the indices,
so every vector re-embeds from the inference service.
Cache operations
Provisioning the table
-
cache_databaseconfigured: cross-schema DDL is not something either the Installer's diff-based approach or a Doctrine migration on this connection can express, so both skip the table and print a note. Create it manually in that schema:CREATE TABLE <cache_database>.bundle_backend_power_tools_embedding_cache (
cache_key VARCHAR(64) NOT NULL,
model_id VARCHAR(190) NOT NULL,
modality VARCHAR(16) NOT NULL,
dimension INT NOT NULL,
vector MEDIUMBLOB NOT NULL,
created_at BIGINT NOT NULL,
last_used_at BIGINT NOT NULL,
PRIMARY KEY (cache_key),
INDEX idx_embedding_cache_model (model_id),
INDEX idx_embedding_cache_last_used (last_used_at)
) ENGINE = InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;Replace
<cache_database>with the configured schema name, andInnoDBwith your chosen engine (see Storage engine below).
Storage engine
Controlled by cache_table_storage_engine (see Configuration). Left at
its default (empty), the engine is auto-detected once, at table-creation time:
- A replication guard runs first: if Galera, Group Replication, or Aurora is detected, InnoDB
is forced —
information_schema.ENGINEScannot see that such a cluster silently refuses to replicate a reported-supported non-InnoDB engine. - Otherwise, the ladder tries Aria, then MyISAM, then falls back to InnoDB.
An explicit value is verified against the engines available on the server and bypasses the replication guard — that is the operator's decision to make deliberately; not one auto-detect will make for you.
Eviction
Rows are pruned by age and by a row cap:
bin/console bpt:embeddings:reindex --evict
Uses cache_eviction.max_age_days (default 90) and cache_eviction.max_rows (default
1000000, see Configuration): rows untouched for longer than the age
cutoff are removed first; if the table is still over the row cap afterward, the
least-recently used rows are removed down to it. Reading a row refreshes its last_used_at
(throttled to once per hour per row), so a row still in active use stays warm regardless of age.
Query embeddings are cached separately
The text a user searches for is embedded too, and that is cached independently of the index: a
Symfony cache pool (pimcore_backend_power_tools.embedding_query_cache_pool, on the cache.app
adapter, 24 h TTL). Repeated identical queries skip the inference service. Nothing about it is stored
in the index, and it needs no maintenance — entries simply expire. To clear it:
bin/console cache:pool:clear pimcore_backend_power_tools.embedding_query_cache_pool
Bypassing the cache
Reuse is keyed on content, so the honest way to "invalidate" is to change the key. Pick the smallest scope that covers your reason:
| Goal | Do this | Scope |
|---|---|---|
| Serving-side preprocessing changed (tokenizer, normalization, image resizing) | bump that model's serving_version | one model |
Swapped the served model, endpoint or provider under the same config id | bump that model's serving_version | one model |
| Distrust everything (suspected corruption, a bad rollout) | bump cache_version | every model |
| Re-embed one model's elements now, without touching others | bpt:embeddings:reindex --regenerate --model=<id> | one model |
| Rebuild indices and re-embed from scratch | bpt:embeddings:recreate-indices --fresh | everything |
--fresh (-f) flushes the entire durable cache table, then re-embeds everything from the
inference service. There is no undo: an aborted run leaves a partially embedded index, and
recovery means finishing the re-embed.
Retired models
Vectors of a model that is no longer configured disappear with the next index recreation because the new mapping omits their fields. Their cache rows are not removed by this — they simply age out through eviction like any other row, since they can never be matched again.
To refresh a configured active model and its vectors without touching the others, use
--regenerate --model=<id> as above.
Verifying reuse
The reconciler reports how many elements are missing a vector — and re-enqueues them:
bin/console bpt:embeddings:reindex --reconcile
It prints N element(s) missing a vector … re-enqueued N. On a healthy index that is 0 and nothing is
written. When something is missing, the re-enqueued elements restore from self-copy or the durable
cache table, so only genuinely absent vectors reach the inference service.