Build a Knowledge Space in CI and Ship It as a File
Sam runs platform engineering. Every quarter the docs team ships a new version of the product handbook — hundreds of Markdown files, PDFs, and API specs. Historically, whoever needed the knowledge space for a project would rebuild it locally, with whatever flags they happened to use that day. Two engineers, two archives, two slightly different chunk counts. Neither was auditable.
Sam’s requirement is simple: one canonical build per release, produced once, reused everywhere. CI builds it. Every service mounts the same file.
What you’ll build
Section titled “What you’ll build”- A GitHub Actions workflow that triggers on
v*tags, runs the full indx build, and uploadshandbook.indxas a versioned artifact. - An incremental build with
--resumeso that unchanged files don’t get re-processed on subsequent runs. - A
--strictgate that fails the pipeline if any file is silently dropped — no incomplete artifacts reach production. - A tiny serverless handler that loads the archive at cold start and serves
space.search()per request — no database, no rebuild.
The build
Section titled “The build”-
Install indx in your CI environment.
The cloud stack covers the default pipeline (docling parser, OpenAI embedder, qdrant store). Install it once; the same package runs locally and in CI.
Terminal window pip install "indx[cloud]"The
OPENAI_API_KEYcomes from repository secrets — never fromindx.toml. The workflow below wires it through the standardenv:block. -
Write the release workflow.
This workflow triggers on every
v*tag — the natural moment a new docs version is authoritative.name: Build knowledge artifacton:push:tags:- 'v*'jobs:build-knowledge:runs-on: ubuntu-lateststeps:- name: Checkoutuses: actions/checkout@v4- name: Set up Pythonuses: actions/setup-python@v5with:python-version: '3.12'- name: Install indxrun: pip install "indx[cloud]"- name: Restore resume cacheuses: actions/cache@v4with:path: .indx-cachekey: indx-resume-${{ hashFiles('docs/**', 'indx.toml') }}restore-keys: indx-resume-- name: Build knowledge spaceenv:OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}run: |indx ./docs \--out ./ai-ready \--resume \--strict \--json- name: Upload artifactuses: actions/upload-artifact@v4with:name: handbook-${{ github.ref_name }}path: ./ai-ready/handbook.indxif-no-files-found: error -
Understand
--resume— incremental CI builds.--resumetells indx to check its stage-output cache before re-running any work. If a file’s content and the resolved config haven’t changed since the last build, its parse, chunk, relate, and enrich outputs are reused. Only changed or new files flow through the full pipeline.The
actions/cachestep above preserves the cache directory between workflow runs using a key derived from the docs tree and config. On a release where only three files changed, the build skips all the unchanged work and finishes in a fraction of the time. -
Understand
--strict— no silent drops.By default, a file that can’t be parsed (password-protected PDF, corrupt archive, unsupported format) is logged as a skip and the build continues.
--strictpromotes any skip to a fatal error: the process exits1and the workflow fails.indx exit codes:
| Code | Meaning | |------|---------| |
0| Success — archive is complete. | |1| Fatal runtime error — includes any skip when--strictis active. | |2| Usage error — bad flags or missing arguments. | |3| Config error — invalid or unresolvableindx.toml. | |4| Archive error — corrupt or incompatible.indxfile. |Without
--strict, a silently-dropped file might go unnoticed for weeks. With it, the pipeline breaks loudly at the source. -
Read the
--jsonbuild summary.--jsonprints a machine-readable summary to stdout alongside the human-readable progress. Log it, store it as a build artifact, or add an assertion step that fails ifchunksfalls below a threshold.Terminal window indx ./docs --out ./ai-ready --resume --strict --jsonA representative run on a 142-file handbook:
indx ./docs → ./ai-ready01 walk 142 files, 18 folders02 parse 142 ok, 0 skipped03 chunk 1180 chunks04 relate 412 relations05 enrich 142 documents (openai:gpt-5-mini)06 embed 1180 vectors → qdrant, sealed handbook.indxdone: 1180 chunks, 142 docs, embed_dim=1536 (14.1s){"counts": { "files": 142, "chunks": 1180, "relations": 412, "documents": 142 },"components": { "parser": "docling", "llm": "openai:gpt-5-mini", "embedder": "openai:text-embedding-3-small", "store": "qdrant" },"elapsed": 14.1,"stages": {"walk": 0.2, "parse": 4.8, "chunk": 1.1,"relate": 2.3, "enrich": 4.4, "embed": 1.3}}The
stagesobject makes regressions obvious — ifenrichsuddenly doubles, something changed in the LLM call path. -
Mount the artifact in a serverless function.
The
.indxfile is the whole knowledge space: chunks, embeddings, metadata, and the relationship graph, sealed in a single ZIP. Download it at deploy time, then load it once at cold start. Each invocation callsspace.search()— no rebuild, no separate vector database to operate.from indx import KnowledgeSpace# Module scope — runs once per cold start.space = KnowledgeSpace.load("handbook.indx")def handler(event, context):query = event.get("query", "")hits = space.search(query, k=5)return {"results": [{"text": hit.chunk.text,"source": hit.source.path,"score": hit.score,}for hit in hits]}KnowledgeSpace.load()validates checksums and version compatibility, then memory-maps the embedding vectors — so a large archive doesn’t blow out the function’s memory budget. Subsequent calls tospace.search()are in-process; there is no network hop to a vector store.
Why the build is reproducible
Section titled “Why the build is reproducible”Every indx build records its resolved configuration — the exact model names, chunking parameters, and component choices — into two places: index.json under metadata.config, and manifest.json inside the .indx archive. That snapshot travels with the artifact.
When Sam hands handbook-v2.4.0.indx to a downstream team six months later, they can open manifest.json and see exactly which models produced those embeddings. There is no ambiguity about what gpt-5-mini meant on that day — it was resolved to a specific model at build time and recorded.
Combined with --resume’s content-addressed cache, the same source tree plus the same indx.toml always produces the same archive. That’s the contract CI enforces on every v* tag.
What you learned
Section titled “What you learned”--resumemakes CI builds incremental — unchanged files reuse cached stage outputs, so only diffs flow through the pipeline.--strictturns silent skips into hard failures (exit1), ensuring every file in the source tree makes it into the artifact.--jsonsurfaces a structured build summary with counts, components, elapsed time, and per-stage timings you can assert on or store.KnowledgeSpace.load()at cold start means serverless functions carry the full knowledge space as a file — no external database required.