Skip to content

Choosing an Embedder

The embedder turns chunk text into vectors during stage 06, Embed+Pack. Those vectors are what make a knowledge space searchable.

This guide helps you pick an embedder and explains what happens when you change it. The model’s identity is recorded into the archive, so consumers always know which model produced the vectors.

  • Default: openai:text-embedding-3-small. Cloud-backed, light to install, dim 1536. Use bge-m3 when you need a fully local profile.
  • Lighter local English: e5.
  • No local GPU, or already paying for an API: openai or cohere.
  • Local embedders are the heaviest optional path, since they pull Torch. API embedders stay light.
  • The model identity (name) and dim are pinned into the archive manifest. Changing the embedder requires a full re-embed.

Every embedder satisfies the same typed protocol, whether built-in or third-party. The pipeline never needs to know which one is active.

@runtime_checkable
class Embedder(Protocol):
"""Turns text into vectors. Default: openai:text-embedding-3-small."""
name: str
dim: int
def embed(self, texts: list[str]) -> list[list[float]]: ...

Three members matter for selection:

  • name is the embedder’s stable identifier, for example text-embedding-3-small. It is recorded into the manifest as embedding_model.
  • embed(texts) takes a list of strings and returns one vector (list[float]) per input. indx always calls it in batches (see Batching).
  • dim is the vector dimensionality. It is read once and pinned into the manifest as embedding_dim. That lets indx validate query-time compatibility.
EmbedderRunsStrengthsBest forExtra
openai:text-embedding-3-small (default)APINo local GPU, no model download, dim 1536Cloud-backed default and lightweight installsindx[openai]
bge-m3LocalMultilingual, long inputs, strong open-license retrieval, dim 1024Local / air-gapped profile; mixed-language and document-heavy corporaindx[bge] (pulls Torch)
e5LocalLighter than BGE-M3, strong English retrievalEnglish-only corpora where you want a smaller local footprintindx[e5] (pulls Torch)
openaiAPINo local GPU, no model download, managed qualityTeams already on OpenAI, or machines without a GPUindx[openai] (light, HTTP only)
cohereAPINo local GPU, strong multilingual API modelsTeams already on Cohereindx[cohere] (light, HTTP only)
litellm:<provider/model>API / localOne adapter for 100+ providers — on-prem (Ollama, vLLM) and AWS/Azure/GCP/cloudTeams standardized on LiteLLM, or multi-vendor setupsindx[litellm] (light, HTTP only)

Why BGE-M3 is the local profile’s embedder

Section titled “Why BGE-M3 is the local profile’s embedder”

BGE-M3 anchors indx’s opt-in local profile, and it is also a strong general embedder:

  • Fully local. It needs no API key and works air-gapped (see Local & air-gapped).
  • Multilingual, and supports long inputs. This suits arbitrary directory contents such as code, docs, and mixed languages.
  • Strong retrieval quality among openly licensed models. It uses native dense embeddings, with hybrid and multi-vector modes available. It works well as a default without per-corpus tuning.
  • Dim 1024. That is what space.stats.embed_dim and the manifest report on a local-profile build. The cloud default records dim 1536.
  • Choose e5 if your corpus is English-only and you want a lighter local model than BGE-M3.
  • Choose openai or cohere in three cases: the build machine has no GPU, you don’t want to download model weights, or your team already pays for those APIs. API embedders avoid the Torch dependency entirely.

This is the single biggest practical difference between the options.

  • Local embedders (bge-m3, e5) load through FlagEmbedding (BGE’s reference implementation) or sentence-transformers. Install them via indx[bge] / indx[e5], or get the whole air-gapped profile via the indx[local] bundle (docling + ollama + bge + qdrant). They pull in Torch plus model weights — the heaviest optional path in the whole project.
  • API embedders (openai, cohere) just make HTTP calls. Their extras (indx[openai], indx[cohere]) stay light, with no Torch and no weights.
Terminal window
# Recommended local default stack (docling + local embeddings + qdrant):
pip install "indx[local]"
# Just a local embedder runtime (Torch comes with it):
pip install "indx[bge]" # BGE-M3
pip install "indx[e5]" # E5
# Light API embedders — no Torch:
pip install "indx[openai]"
pip install "indx[cohere]"

If you select an embedder whose extra is not installed, indx raises a MissingDependencyError naming the exact pip install "indx[...]" to run. See the full extras matrix.

The embedder slot is resolved with the standard precedence: explicit code argument / use() → CLI flag → indx.toml → documented default.

Terminal window
indx ./docs --out ./ai-ready --embedder e5
[embed]
model = "openai:text-embedding-3-small" # any registered embedder name; use "bge-m3" for local
from indx import DirectoryPipeline
# By name string
pipeline = DirectoryPipeline(embedder="bge-m3", store="qdrant")
# Or swap later
pipeline.use(embedder="openai")
# Or pass a custom object satisfying the Embedder protocol
class MyEmbedder:
name = "my-embedder"
dim = 768
def embed(self, texts: list[str]) -> list[list[float]]:
...
pipeline.use(embedder=MyEmbedder())

For authoring your own embedder backend, see Custom components and Adding a backend.

Embedding is batched, and this is the single biggest performance lever for the stage. Stage 06 hands the full chunk list to Embedder.embed(list[str]) in one call; each adapter then sub-batches internally with its own default batch size — OpenAI 256, BGE-M3 and E5 12 (the local models tune this via the adapter sub-table). The resulting vectors are then written to the store with batched upsert calls (Qdrant upserts 256 points per request).

ParamDefault
Embed sub-batch size (OpenAI)256
Embed sub-batch size (BGE-M3 / E5)12
Embed max concurrency--jobs

Local models are far more efficient on batches, thanks to CPU/GPU vectorization. API embedders amortize round-trips the same way, and use a bounded concurrency limit to respect rate limits.

Tune batch size via the embedder’s adapter sub-table or kwargs. See Performance.

When stage 06 seals the .indx archive, the embedder’s identity is written into two places:

The archive-root manifest.json (the serialized Manifest model):

{
"embedding_model": "text-embedding-3-small",
"embedding_dim": 1536,
"components": { "embedder": "openai:text-embedding-3-small", "store": "qdrant" }
}

and a dedicated embeddings/manifest.json alongside the raw vector matrix in the expanded output directory:

ai-ready/
├── handbook.indx # sealed archive (manifest + documents/chunks/relations jsonl + checksums)
├── index.json
├── chunks/
└── embeddings/
├── manifest.json # { model, dim, count, backend }
└── vectors.f32 # contiguous little-endian float32 matrix (count × dim)

This makes the archive self-describing: a consumer knows exactly which model produced the vectors. Vectors are stored as little-endian float32, so the matrix is count × dim.

Vectors from one model are not comparable with vectors from another. So changing the embedder requires a full re-embed. This is reflected in the cache and resume behavior:

  • With --resume, changing the embedder invalidates only the Embed stage. Walk, Parse, Chunk, Relate, and Enrich outputs are reused from cache. Changing the parser, by contrast, invalidates Parse and everything downstream.
  • The resolved config snapshot (including the embedder name) is recorded in index.json.metadata and the manifest for auditability.
Terminal window
# Switch embedders; everything upstream is reused, only vectors are recomputed.
indx ./docs --out ./ai-ready --embedder e5 --resume