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 | 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 |
chunk_chars change | that model's long text vectors re-embed (chunk texts move); texts at or below max_text_chars keep their keys |
max_text_chars change | texts that cross the new threshold switch between whole and chunked and re-embed; everything else keeps its key |
| 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
Recreate the indices with the plain generic-data-index command — there is no embeddings-specific wrapper:
bin/console generic-data-index:update:index -r
Every element's vector repopulates from the durable cache table as the queue drains, which is why recreation over
unchanged content costs no inference calls. Scoping the command (-c <classId>, -a, -d) works
the same way for the affected index only.
Recreate with embeddings enabled: an index created while enabled: false lacks the k-NN index
settings, and a later mapping update cannot retrofit them — you would have to recreate again.
Forcing a full re-embed
Recreation alone never re-embeds unchanged content. If you distrust every stored vector, empty the cache table first, then recreate:
TRUNCATE TABLE bundle_backend_power_tools_embedding_cache;
Prefix the table with the configured schema if embedding_cache.database is set. With the cache
empty, every vector misses and re-embeds from the inference service as the queue drains. There is
no undo: an aborted run leaves a partially embedded index, and recovery means finishing the
re-embed. The query-embedding cache pool is unaffected;
those entries expire within 24 h on their own.
Before reaching for the truncate, check whether a narrower tool covers your reason — see Bypassing the cache.
Cache operations
Provisioning the table
The cache table's schema and storage engine are deploy-time-only settings, in a separate,
container-only yaml tree — never stored, never editable through Studio, the same as
embedding_client_profiles:
pimcore_backend_power_tools:
embedding_cache:
database: '' # optional schema for the cache table; empty = Pimcore's main schema
table_storage_engine: '' # explicit engine; empty = auto-detect at table creation
-
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 <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
<database>with the configured schema name, andInnoDBwith your chosen engine (see Storage engine below).
Storage engine
Controlled by table_storage_engine in the deploy-time embedding_cache tree above (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) | Save & Full Re-embed in Studio — fresh vectors for all current content; cache rows overwritten in place | every configured model |
| Re-embed one model's elements now, without touching others | bpt:embeddings:reindex --regenerate --model=<id> | one model |
| Rebuild indices and purge the cache table | TRUNCATE the cache table, then generic-data-index:update:index -r (see Forcing a full re-embed) | everything, including retired-model rows |
<id> above is the model's canonical config id: dashes are folded to underscores, so a model
configured as nomic-text-v2 is addressed with --model=nomic_text_v2. The dashed spelling from
your own YAML or Studio key is not accepted.
Truncating the cache table and recreating the indices re-embeds everything from the inference service, including content whose vectors were perfectly fine. There is no undo: an aborted run leaves a partially embedded index, and recovery means finishing the re-embed. The query-embedding cache pool is not affected; those entries expire within 24 h on their own.
Studio's Save & Full Re-embed is different: it re-embeds every element and model fresh and overwrites the durable cache rows in place, but it does not empty the table, so rows for retired models or content outside the current scope remain until normal eviction. It does clear the query-embedding cache pool at the start of the run.
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.