Skip to content

Running Fully Local & Air-Gapped

indx ships cloud defaults out of the box, but it can also run the entire pipeline on your own hardware with nothing leaving the network.

This guide walks the air-gapped path end to end: pick an offline path, pre-stage model weights, choose a local store, produce a self-contained .indx, and record an audit-grade manifest.

There are two offline paths:

  • Zero-dependency offline core: pass --offline. This uses plaintext, none, none, hash, jsonl, and .indx. It needs no extras and is best for smoke tests, plain text, Markdown, and policy-constrained machines.
  • Local semantic profile: install local extras and select docling, ollama:qwen2.5, bge-m3, and a local store. This gives stronger parsing and semantic retrieval, but requires staged model weights.

A bare zero-config run (indx <dir> --out <dir>) uses the cloud defaults: docling + openai:gpt-5-mini + openai:text-embedding-3-small (dim 1536) + qdrant. That run needs an OPENAI_API_KEY.

The local profile is opt-in instead. Select it one of two ways:

  • pip install "indx[local]", or
  • name the local backends explicitly through flags or indx.toml.

Once selected, every value is local-capable, so the run never touches the internet.

SlotZero-dependency --offlineLocal semantic profileWhere it runs
ParserplaintextdoclingLocal — no API key, no network
LLMnoneollama:qwen2.5Local, or disabled
VLMnonenoneNo vision calls by default
Embedderhashbge-m3 (dim 1024)Local
Storejsonlqdrant embedded or jsonlLocal on-disk
Output.indx.indxLocal self-contained archive

On top of the local profile, the core install ships zero-dependency fallbacks: a plaintext parser, a jsonl store, the none VLM, and the .indx + jsonl writers. Even a bare pip install indx can complete a full run offline.

For the core path, no installation beyond pip install indx is required:

Terminal window
indx ./docs --out ./ai-ready --offline

--offline fills only slots you did not set explicitly, so you can override one piece:

Terminal window
# Offline core, but use Docling if its extra is installed.
indx ./docs --out ./ai-ready --offline --parser docling

Use the local semantic profile when you need better document parsing and semantic embeddings.

On a machine that still has internet (your build/staging box), install the curated offline bundle:

Terminal window
pip install "indx[local]"

indx[local] is the one-line install of the recommended air-gapped stack. It is aliased as indx[defaults], and both resolve to the same local bundle. The bundle includes:

  • The Docling parser
  • The local Ollama LLM client
  • The local embedding runtime (FlagEmbedding / sentence-transformers + Torch)
  • The Qdrant client

You can also assemble the stack yourself — for example, using the no-DB JSONL store and skipping the Qdrant client entirely. Install the pieces explicitly:

Terminal window
# Minimal offline floor: docling + local bge-m3 + jsonl no-DB store
pip install "indx[docling]" "indx[bge]"

The bare core (pip install indx) already includes the jsonl store and .indx/jsonl writers, so the command above gives you a complete offline pipeline with no database installed.

Step 2 — Pre-stage model weights for offline boxes

Section titled “Step 2 — Pre-stage model weights for offline boxes”

An air-gapped target has no internet, so the LLM, embedder, and parser model weights must be present before you disconnect. Stage them once on a connected machine, then copy the caches across.

The local profile LLM is ollama:qwen2.5, served by a local Ollama daemon. Pull the model while online:

Terminal window
ollama pull qwen2.5

On the air-gapped box, run an Ollama daemon. Then either pull from an internal mirror or copy the Ollama model directory (~/.ollama/models on Linux/macOS) from the staging machine.

indx talks only to the local Ollama endpoint. It never reaches out to a model registry itself.

Pre-download the embedder and parser weights

Section titled “Pre-download the embedder and parser weights”

The local profile embedder bge-m3 and the Docling parser download model weights from Hugging Face on first use. Warm those caches while online, then transport them:

Terminal window
# Trigger downloads once on a connected machine, then copy the caches.
export HF_HOME=/srv/indx-models/hf # parser + embedder weights
# Name the local backends so the warm-up exercises local weights, not the cloud defaults.
indx ./sample-docs --out ./warmup --embedder bge-m3 --llm ollama:qwen2.5

On the offline box, point the same environment variables at the copied caches and enable offline mode so the libraries never attempt a network fetch:

Terminal window
export HF_HOME=/srv/indx-models/hf
export HF_HUB_OFFLINE=1 # fail fast instead of reaching out
export TRANSFORMERS_OFFLINE=1

Step 3 — Choose your store: zero-dependency floor vs. embedded Qdrant

Section titled “Step 3 — Choose your store: zero-dependency floor vs. embedded Qdrant”

Both local store options keep vectors on disk and never contact a server. Pick based on corpus size.

JSONL — the absolute zero-dependency floor

Section titled “JSONL — the absolute zero-dependency floor”

The jsonl store ships in the core and needs no database and no extra. It writes vectors as newline-delimited records, and its similarity search is a brute-force linear scan.

That is fine for small and medium corpora, and it maximizes portability inside the .indx archive.

Terminal window
indx ./docs --out ./ai-ready --store jsonl

This is the most defensible choice for an air-gapped deployment. There is nothing to install, nothing to operate, and the resulting archive is fully self-contained.

Qdrant’s embedded mode runs in-process against a local on-disk path, with no server. You keep local-first guarantees and get a real ANN index that scales to larger corpora. If you later grow into a self-hosted server, the same client code points at it.

Terminal window
indx ./docs --out ./ai-ready --store qdrant
StoreSearchBest forNetwork
jsonl (no DB)Brute-force linear scanSmall/medium corpora; maximum portabilityNone
qdrant (embedded)ANN index, on-diskLarger corpora; same code scales to a serverNone (embedded)

Step 4 — Produce a self-contained .indx with vectors inline

Section titled “Step 4 — Produce a self-contained .indx with vectors inline”

The default output writer is .indx, a Zip container holding manifest.json, the index.json knowledge graph, JSONL chunk shards, optional vector blobs, and enrichments.

When built with the JSONL store, vectors are stored inline. The archive is then completely self-contained: it re-opens and answers queries with no external service.

Terminal window
# Fully offline build → portable, self-contained archive
indx ./docs --out ./ai-ready --store jsonl
# Sanity-check structure and retrieval, still offline
indx inspect ./ai-ready/handbook.indx
indx query ./ai-ready/handbook.indx "onboarding checklist"

The same flow from the SDK:

from indx import DirectoryPipeline, KnowledgeSpace
space = DirectoryPipeline(store="jsonl").run("./docs", "./ai-ready")
print(space.stats)
# Re-load later on any machine, no re-processing, no network
space = KnowledgeSpace.load("./ai-ready/handbook.indx")
hits = space.search("onboarding checklist", k=5)

Because the archive carries the embedder’s name and dim (1024 for bge-m3) in its manifest, a consumer can detect a model/dimension mismatch before querying. See The .indx archive and index.json for the on-disk layout.

Step 5 — Capture the reproducibility manifest for audit

Section titled “Step 5 — Capture the reproducibility manifest for audit”

Every run records its provenance into the .indx manifest:

  • The producing build (tool_version)
  • The schema version (indx_version)
  • The chosen slot backends
  • The embedder name and dim
  • Per-member checksums

This is what makes a knowledge space auditable and re-creatable. Given the same inputs, config, and model versions, a run is reproducible, and the manifest tells you exactly how each space was produced.

// manifest.json (illustrative excerpt)
{
"indx_version": "1.0",
"tool_version": "indx 0.4.2",
"slots": {
"parser": "docling",
"llm": "ollama:qwen2.5",
"vlm": "none",
"embedder": "bge-m3",
"store": "jsonl",
"output": "indx"
},
"embedder": { "name": "bge-m3", "dim": 1024 }
}

For a fully byte-stable index.json, seed any randomness and pin model identifiers. See Reproducibility for the full recipe (seeding, deterministic serialization, and golden-file verification).

The safe pattern if a cloud component is ever introduced

Section titled “The safe pattern if a cloud component is ever introduced”

If policy later allows a single cloud backend — say, a hosted LLM for higher-quality enrichment — keep egress controlled by inserting a redaction stage before Enrich.

Redaction is a first-class extension point, so sensitive content can be stripped before any egress-capable component sees it.

Stages obey a uniform run(ctx: SpaceContext) -> SpaceContext contract and communicate only through the shared SpaceContext. A redaction stage therefore drops cleanly into the ordered pipeline:

01 Walk → 02 Parse → 03 Chunk → 04 Relate → [Redact] → 05 Enrich → 06 Embed+Pack

This keeps Walk, Parse, Chunk, and Relate fully local. The only stage that could egress then receives already-sanitized text. See Custom stages for how to author and insert one, and Enrichment with LLM/VLM for the enrichment slot itself.

  • Install indx[local] (or indx[docling] + indx[bge] for the no-DB floor) on a connected staging box.
  • ollama pull qwen2.5; copy ~/.ollama/models to the target.
  • Warm and copy the Hugging Face cache (HF_HOME); set HF_HUB_OFFLINE=1 / TRANSFORMERS_OFFLINE=1 on the target.
  • Choose --store jsonl (zero-dependency floor) or embedded --store qdrant.
  • Build → inspectquery entirely offline; confirm no outbound traffic.
  • Confirm indx.toml names only local backends — no openai/anthropic/hosted-Qdrant.
  • Archive the .indx plus its manifest for audit and reproducibility.