Inference Service
The bundle does not ship, load, or download any model. Every configured model points at an HTTP inference endpoint — any service, self-hosted or third-party, that fulfils the contract below. This page covers what works out of the box; to support an endpoint whose request or response shape differs, see Customization.
Getting and serving a model
Three hosting options, all equally supported by the shipped provider:
| Option | When it fits |
|---|---|
| A managed embeddings API | Fastest to start. Check that it serves the model you picked, and that sending your content off-site is acceptable — the composed object text and image thumbnails leave your infrastructure. |
| A self-hosted OpenAI-compatible server | Several open-source inference servers expose exactly this API for a model id from a public model hub. |
| A custom service | When you need your own preprocessing, or an image tower a generic server does not expose. |
Model weights themselves come from a model hub (Hugging Face and comparable registries); the server you run fetches them, not Pimcore.
Pimcore provides a shared Hugging Face Space (pimcore/local-inference-service) that fulfils the
HTTP contract below for the default models. It exists for development and testing purposes only
and is not intended for production use — there are no availability, capacity, or
model-stability guarantees, cold starts can take minutes, and everything you embed (composed object
text, image thumbnails, search queries) is transmitted to a public, shared endpoint. It can be used
at your own risk; for production, serve a model yourself using one of the hosting options above.
Base URL https://pimcore-local-inference-service.hf.space, endpoints per the contract below:
| Endpoint | Modality | Serves (default models) |
|---|---|---|
POST /text-embedding | text | nomic-ai/nomic-embed-text-v2-moe, google/siglip2-base-patch16-256 (text tower) |
POST /image-embedding | image | google/siglip2-base-patch16-256 (vision tower) |
The Space publishes its full API at /openapi.json. The first request per model may cold-start the
Space and download weights — allow minutes, then retry. The Getting Started
example config points at these endpoints.
Three operational tips:
- Pin the model revision. A model name is not a version. If the weights behind a name change, the
vectors change with them — silently, because the bundle's identity key is computed from the input,
not the weights. When that happens (a pinned upgrade, or a provider-side change you learn about
afterward), bump that model's
serving_version, which exists precisely for this. - Pre-cache the weights into your image layer or a persistent volume. Otherwise, the first request after every deployment blocks on a multi-gigabyte download, and cold-start timeouts look like inference failures.
- Keep the runtime offline. A request naming an arbitrary model must not be able to trigger a download — both for predictable start-up and because many models load with remote code enabled. Cache the models you trust, then disable fetching.
Using a third-party API is a deliberate trade: you avoid running GPUs, and in exchange your content
is transmitted to that provider and its model versioning is outside your control. For an env-backed
secret, configure an
embedding_client_profiles entry rather than
auth_token (which only accepts a literal value), prefer https, and treat a provider-side model
upgrade as a serving_version bump.
HTTP contract
The default provider (provider: default) POSTs to the model's endpoint URL (http/https only):
{ "model": "nomic-ai/nomic-embed-text-v2-moe", "input": ["text one", "text two"] }
and expects the OpenAI-compatible embeddings shape back:
{ "data": [ { "index": 0, "embedding": [0.01, ...] }, { "index": 1, "embedding": [...] } ] }
- Batching: up to 96 texts or 16 images per request (image inputs are base64-encoded thumbnails, so their request bodies are large).
- Retries: transient failures (network, 429, 5xx) are retried with backoff; a permanent failure degrades the affected elements and is repaired by the next backfill run.
- Partial results: if the service cannot embed one input, it should omit that
indexrather than fail the batch — the bundle then degrades only that element. - Auth: a configured
auth_token(or a matching client profile's token) is sent asAuthorization: Bearer <token>. Endpoints with a different auth scheme (e.g. Azure'sapi-keyheader) or extra header requirements use the model'soptions.headers, which override same-named defaults — see custom request headers. Extra request body parameters can be added viaoptions.params.
A different request/response shape is supported by writing your own provider — see Writing a Custom Embedding Provider.
To check an endpoint against this contract, press Test on the model card in System > Embedding Configuration. It sends one probe request and reports the round-trip time and the dimension the endpoint returned, see Testing a model.
Serving invariants
These are correctness requirements, not suggestions — each one was the root cause of a real, hard-to-see quality failure during development:
- Return unit-length vectors. Every embedding must be L2-normalized, from every code path of the service. The kNN fields score in an L2/cosine space, and unnormalized vectors rank partly by their length: the symptom is uniformly tiny, near-identical scores with irrelevant top results — nothing errors, the search just gets quietly wrong.
- Preprocess deterministically, and version the preprocessing. Stored vectors are identified
by the text as sent. Any serving-side change — a tokenizer fix, text canonicalization, image
resizing — changes vectors without changing that text, leaving every stored vector silently
stale. Whenever such a change ships, bump that model's
serving_versionin the bundle config; it is part of the vector identity key exactly for this. - Keep the dimension fixed per model.
dimensionis required in the model config, and the bundle refuses to index vectors of the wrong size instead of poisoning the index. - Do not add instruction prefixes server-side. The bundle prepends the configured
prefixes.document/prefixes.querybefore sending; a service adding its own would double them.
Cross-modal models (text → image search)
Image search uses a two-tower model: a vision tower embeds the indexed images, a text tower embeds the incoming queries, and both meet in one latent space.
In Pimcore Studio, add the two towers as two model cards with the same Shared space, then switch Cross-modal query on for the Image search usage and pick the text tower as the Query model. As configuration keys:
models:
my-vision-model:
modality: 'image'
shared_space: 'siglip2-base'
# ...
my-vision-text-model:
modality: 'text'
shared_space: 'siglip2-base'
# ...
usages:
image_search:
model: my-vision-model
query_model: my-vision-text-model
The registry rejects a pairing whose spaces differ or are unset — matching dimensions alone would let two unrelated models produce confidently wrong results. Both towers must be served with the same preprocessing discipline (invariant 2 applies to the pair as a whole).