Bring Your Own Stack
indx owns orchestration; you choose the components.
- Orchestration is the six-stage pipeline. indx owns it.
- Components are the expensive or opinionated capabilities: parsing, LLM, vision, embedding, vector storage, and output. You choose them.
Each component is a typed slot with a default that you can replace by name or by object. We call this Bring Your Own Stack (BYOS): the ingestion pipeline adapts to your environment instead of forcing one vendor stack.
The idea in one sentence
Section titled “The idea in one sentence”indx composes parsers, models, and stores. It does not replace them. A file parser is not a competitor; it is one swappable slot inside indx. The same holds for the LLM that writes summaries, the embedder that vectorizes chunks, the database that stores them, and the writer that serializes the result.
Each slot is a typed interface with a named default. Swapping a backend is a config change, never a rewrite. The pipeline you write today survives the model you will use next year. Re-embed, re-store, re-export with the same code.
The six slots
Section titled “The six slots”The six stages of the pipeline use six replaceable slots.
Each slot has one runtime default. The default stack is cloud-backed:
openai:gpt-5-mini for enrichment and openai:text-embedding-3-small for embeddings.
For offline work, use either --offline for the zero-dependency core stack or the opt-in
local profile (ollama:qwen2.5 + bge-m3) for richer local models.
| Slot | Default | Other options |
|---|---|---|
| Parser | docling | unstructured, llamaparse, markitdown, custom |
| LLM | openai:gpt-5-mini (cloud) | ollama, vllm, anthropic, azure |
| VLM | none | qwen-vl, gpt-4o, local |
| Embedding | openai:text-embedding-3-small (dim 1536) | bge-m3, e5, cohere |
| Vector store | qdrant | pgvector, chroma, lancedb, jsonl |
| Output | .indx | jsonl, langchain, llamaindex |
Each slot has a dedicated guide that covers the trade-offs in depth:
Three principles BYOS serves
Section titled “Three principles BYOS serves”The slot design expresses three product principles that recur across indx.
1. Offline-capable
Section titled “1. Offline-capable”There are two offline paths:
- The opt-in local profile runs with richer local components:
doclingparses locally,ollama:qwen2.5runs enrichment with no API key, andbge-m3embeds locally. - The zero-dependency floor ships in the core package:
plaintext,none,hash,jsonl, and.indx. Use it from the CLI with--offline, or select those slots explicitly in the SDK.
2. No lock-in
Section titled “2. No lock-in”Slots are defined as typing.Protocol classes, which means structural typing. A backend satisfies a slot by having the right methods and signatures, not by inheriting from an indx base class. Third-party authors never import or subclass anything from indx to plug in; they just implement the protocol.
The protocol names are stable contracts: Parser, LLM, VLM, Embedder, Store, OutputWriter, and the Stage protocol that the pipeline itself uses. See the full signatures in the protocols reference.
This is why a community package like indx-weaviate can ship a new store, advertise it through a Python entry point, and have store = "weaviate" simply work, with no fork of indx. First-party builtins always win on name collisions, so a plugin can never silently shadow a default.
3. Light core
Section titled “3. Light core”pip install indx stays small and fast. The core depends only on Typer, Rich, Click, Pydantic v2, and pydantic-settings (TOML parsing is stdlib).
Every heavy backend — Docling, Torch, vector-DB clients, cloud SDKs — is an optional extra, never a core dependency.
| Extra | Installs |
|---|---|
indx[docling] | the default Docling parser |
indx[bge] | local bge-m3 embeddings (FlagEmbedding + Torch) |
indx[qdrant] | the Qdrant client |
indx[openai] / indx[anthropic] | cloud LLM SDKs |
indx[cloud] | the default cloud runtime stack: Docling + OpenAI + Qdrant |
indx[local] / indx[defaults] | the opt-in local profile stack in one line — the two names are aliases for the same bundle |
indx[all] | every backend, for convenience or CI |
If you select a backend whose extra is not installed, indx fails fast with an actionable error. The error names the exact pip install "indx[...]" to run, and it never breaks an unrelated code path. See the extras reference for the full matrix.
Two ways to select a component
Section titled “Two ways to select a component”A slot can be filled either by name (in config or on the CLI) or by object (in code). The two are equivalent; the name simply resolves through the registry to a class.
By name — config or CLI
Section titled “By name — config or CLI”In indx.toml, each section names the backend for a slot. Names resolve through the registry to the right implementation:
[parser]backend = "docling" # docling | unstructured | llamaparse | markitdown | custom
[llm]backend = "ollama" # ollama | vllm | openai | anthropic | azuremodel = "qwen2.5"# api_key comes from $INDX_LLM__API_KEY, never the file
[embedding]backend = "bge-m3" # bge-m3 | e5 | openai | cohere
[store]backend = "qdrant" # qdrant | pgvector | chroma | lancedb | jsonl
[output]writer = "indx" # indx | jsonl | langchain | llamaindexThe same names work as CLI flags, which take precedence over the config file:
indx ./docs --out ./ai-ready --parser markitdown --store jsonl --format langchainPrecedence is CLI flag > indx.toml > built-in default. See the configuration guide and the configuration reference for the complete schema.
By object — code
Section titled “By object — code”In the SDK, pass a constructed component straight into the pipeline. This is the same selection, expressed as an object instead of a string:
from indx import DirectoryPipelinefrom indx.parsers.docling import DoclingParserfrom indx.store.qdrant import QdrantStore
# fully local, on-prem — nothing leaves the buildingpipeline = DirectoryPipeline( parser=DoclingParser(), llm="ollama:qwen2.5", embedder="bge-m3", store=QdrantStore(url="http://localhost:6333"),)space = pipeline.run("./docs", "./ai-ready")Custom objects only need to satisfy the protocol
Section titled “Custom objects only need to satisfy the protocol”Because slots use structural typing, your own class drops straight in as long as it matches the protocol. There is no base class and no registration ceremony:
from pathlib import Pathfrom indx.core import ParsedDoc
class MyParser: def parse(self, path: Path) -> ParsedDoc: ...
pipeline = DirectoryPipeline(parser=MyParser())For the full walkthrough of building components and stages of your own, see custom components and the protocols reference.
Why this matters
Section titled “Why this matters”A folder already encodes how an organization thinks: what sits next to what, what supersedes what, what points where. indx keeps that map, and BYOS makes sure the map is yours — built on a light core, runnable entirely offline, and free of any single vendor’s roadmap.
Whether you run on a laptop, a GPU server, or an air-gapped enterprise network, the same pipeline code applies. Only the slots change.
Next, explore the core objects that flow through these slots, or the pipeline and stages that drive them.