Skip to main content
Version: Next

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:

  1. 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.
  2. The cache table — one keyed read (getMany) against the durable cache table for every ck still missing after self-copy. The ck is 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.
  3. 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:

ChangeEffect
Element content editonly the changed chunks/representations re-embed
prefixes.document changeall of that model's text vectors re-embed (it is part of the embedded input)
Renaming the model's config idall of that model's vectors re-embed (a new id is a new key)
serving_version bumpthat model's vectors re-embed
cache_version bumpevery vector re-embeds
Index recreationnothing — vectors are carried across (see below)
quantization changenothing — a reindex re-encodes the same vectors
prefixes.query changenothing stored re-embeds — it only re-keys the query cache below
Swapping model / endpoint / provider under the same config idnothing — bump serving_version yourself
Editing the model's thumbnail definitionall 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.

Two changes the key cannot see

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, or provider) under the same config id. If the replacement has a different dimension the 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.
Thumbnail changes are handled for you

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_database configured: 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, and InnoDB with 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:

  1. A replication guard runs first: if Galera, Group Replication, or Aurora is detected, InnoDB is forced — information_schema.ENGINES cannot see that such a cluster silently refuses to replicate a reported-supported non-InnoDB engine.
  2. 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:

GoalDo thisScope
Serving-side preprocessing changed (tokenizer, normalization, image resizing)bump that model's serving_versionone model
Swapped the served model, endpoint or provider under the same config idbump that model's serving_versionone model
Distrust everything (suspected corruption, a bad rollout)bump cache_versionevery model
Re-embed one model's elements now, without touching othersbpt:embeddings:reindex --regenerate --model=<id>one model
Rebuild indices and re-embed from scratchbpt:embeddings:recreate-indices --fresheverything
caution

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