Skip to content

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.

  • A GitHub Actions workflow that triggers on v* tags, runs the full indx build, and uploads handbook.indx as a versioned artifact.
  • An incremental build with --resume so that unchanged files don’t get re-processed on subsequent runs.
  • A --strict gate 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.
  1. 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_KEY comes from repository secrets — never from indx.toml. The workflow below wires it through the standard env: block.

  2. Write the release workflow.

    This workflow triggers on every v* tag — the natural moment a new docs version is authoritative.

    name: Build knowledge artifact
    on:
    push:
    tags:
    - 'v*'
    jobs:
    build-knowledge:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout
    uses: actions/checkout@v4
    - name: Set up Python
    uses: actions/setup-python@v5
    with:
    python-version: '3.12'
    - name: Install indx
    run: pip install "indx[cloud]"
    - name: Restore resume cache
    uses: actions/cache@v4
    with:
    path: .indx-cache
    key: indx-resume-${{ hashFiles('docs/**', 'indx.toml') }}
    restore-keys: indx-resume-
    - name: Build knowledge space
    env:
    OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
    run: |
    indx ./docs \
    --out ./ai-ready \
    --resume \
    --strict \
    --json
    - name: Upload artifact
    uses: actions/upload-artifact@v4
    with:
    name: handbook-${{ github.ref_name }}
    path: ./ai-ready/handbook.indx
    if-no-files-found: error
  3. Understand --resume — incremental CI builds.

    --resume tells 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/cache step 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.

  4. 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. --strict promotes any skip to a fatal error: the process exits 1 and the workflow fails.

    indx exit codes:

    | Code | Meaning | |------|---------| | 0 | Success — archive is complete. | | 1 | Fatal runtime error — includes any skip when --strict is active. | | 2 | Usage error — bad flags or missing arguments. | | 3 | Config error — invalid or unresolvable indx.toml. | | 4 | Archive error — corrupt or incompatible .indx file. |

    Without --strict, a silently-dropped file might go unnoticed for weeks. With it, the pipeline breaks loudly at the source.

  5. Read the --json build summary.

    --json prints 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 if chunks falls below a threshold.

    Terminal window
    indx ./docs --out ./ai-ready --resume --strict --json

    A representative run on a 142-file handbook:

    indx ./docs → ./ai-ready
    01 walk 142 files, 18 folders
    02 parse 142 ok, 0 skipped
    03 chunk 1180 chunks
    04 relate 412 relations
    05 enrich 142 documents (openai:gpt-5-mini)
    06 embed 1180 vectors → qdrant, sealed handbook.indx
    done: 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 stages object makes regressions obvious — if enrich suddenly doubles, something changed in the LLM call path.

  6. Mount the artifact in a serverless function.

    The .indx file 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 calls space.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 to space.search() are in-process; there is no network hop to a vector store.

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.

  • --resume makes CI builds incremental — unchanged files reuse cached stage outputs, so only diffs flow through the pipeline.
  • --strict turns silent skips into hard failures (exit 1), ensuring every file in the source tree makes it into the artifact.
  • --json surfaces 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.