Schema Indexing
Every project builds a persistent, searchable schema index before its first discovery run. The index lets discovery work on warehouses of any size (ERPs with 2K+ tables) without dumping every table's columns into every prompt.
How the index is used:
- Discovery — the agent receives a compact Level-0 catalog up front
(one line per table) and pulls per-table column lists + sample rows
on demand via the
lookup_schemaandsearch_tablesactions during exploration.search_tablesis the path that actually queries the Qdrant collection. See On-Demand Schema. - /ask — gates on the schema index being
ready(so we know discovery can run) but performs its own retrieval against the insights/recommendations vector store, not the schema collection.
What gets indexed
For every table in the project's configured datasets:
- A 2-4 sentence natural-language description ("blurb") generated by
the project's
blurb_llmmodel, grounded in column metadata and sample rows — no hallucinations. - An embedding of the blurb (via the project's embedding provider).
- A compact payload carrying dataset, table name, row count, column count, domain-pack keywords, and the model IDs used (for audit).
All points live in a per-project Qdrant collection named
decisionbox_schema_{project_id}. Collections are dropped and
rebuilt on every user-triggered re-index.
Default models
Defaults were validated against a real 2K-table ERP warehouse:
- Blurb model: the project's analysis LLM unless a project-specific blurb LLM is configured in Settings → Schema Index. (Earlier installs defaulted to
bedrock/qwen.qwen3-32b-v1:0— perfect MRR on curated queries, ~$0.004 / 50 tables, 830 ms/table — which remains a strong opt-in choice.) - Embedding model:
openai/text-embedding-3-large— top R@5 across every combination tested; Turkish and English queries are equivalent.
Fallbacks by preference:
| Blurb model | Cost/50 tables | Latency (avg) | Quality (R@5) |
|---|---|---|---|
| Qwen3-32B (Bedrock) | ~$0.004 | 830 ms | 0.897 |
| GPT-4.1-nano (OpenAI) | ~$0.003 | 1.7 s | 0.795 |
| Claude Haiku 4.5 (Anthropic) | ~$0.045 | 3.5 s | 0.750 |
Reasoning-class models (DeepSeek R1, o1/o3/o4-mini, Claude extended-thinking modes) are rejected at save time — their output channel doesn't carry user-visible text through Converse/Chat, which produced empty blurbs in every spike combo.
Lifecycle
Six states on project.schema_index_status:
pending_indexing ──┬─> indexing ──┬─> ready ── Run discovery works
│ ├─> failed ── Retry indexing button
│ └─> cancelled ── User stopped an in-flight run
├── (user clicks Re-index at any time)
└── needs_reindex ── Set when warehouse / dataset config drifts
pending_indexing— project needs indexing; worker hasn't claimed it yet. This is the state right after project create (with a warehouse configured), after a manual retry, or after a re-index.indexing— worker is running. Progress observable viaGET /api/v1/projects/{id}/schema-index/status.ready— collection populated; discovery +/askwork.failed— last attempt failed. Error surfaced in the dashboard; user clicks Retry indexing → back topending_indexing.cancelled— the user stopped the in-flight indexer. Partial progress is discarded; user clicks Retry to start over.needs_reindex— the platform detected a drift (warehouse swap, dataset list change) that invalidates the current index. The dashboard surfaces a banner; user clicks Re-index to rebuild.
Triggering a re-index
Re-indexing is user-triggered only. None of these auto-reindex:
- Warehouse config change
- Dataset / filter change
- Embedding-model swap
- Blurb-model swap
Why: an auto-reindex on every save would drain credits for users tweaking config. The dashboard surfaces a "schema may be stale" nudge after edits that plausibly invalidate the index.
# Re-index a specific project (drops and rebuilds).
curl -X POST http://localhost:8080/api/v1/projects/{id}/reindex
# Retry after a failure (only valid from `failed`).
curl -X POST http://localhost:8080/api/v1/projects/{id}/schema-index/retry
# Poll current status (dashboard uses this every 2 s).
curl http://localhost:8080/api/v1/projects/{id}/schema-index/status
When does an embedding-model change force a rebuild?
Always. Qdrant collections are bound to a fixed vector dimension, so
swapping embedding models (e.g. text-embedding-3-small →
text-embedding-3-large, 1536 → 3072 dims) means the collection is
incompatible. POST /reindex drops and recreates it automatically.
How the dimension is resolved
The collection is sized from the embedding model's vector dimension,
resolved in this order: an explicit dimensions value in the project's
embedding config (an escape hatch for a model the provider catalog
doesn't know — e.g. a managed gateway alias), then the provider's known
dimension for catalogued models, and otherwise a probe — the agent
embeds one short string and measures the returned vector. The probe means
any provider/model indexes correctly, including an OpenAI-compatible
gateway alias whose dimension the catalog can't know up front.
Cost and wall-clock envelope
With the defaults (Bedrock Qwen3-32B + OpenAI text-embedding-3-large)
and 8 parallel blurb workers (BLURB_WORKERS=8):
| Warehouse size | Blurb cost | Embed cost | Wall-clock |
|---|---|---|---|
| Small (40 tables) | ~$0.01 | ~$0.0004 | ~30 s |
| Medium (500) | ~$0.15 | ~$0.005 | ~2 min |
| Large (2K) | ~$0.60 | ~$0.02 | ~6 min |
| Huge (10K) | ~$3.00 | ~$0.10 | ~30 min |
Override worker count for faster rebuilds on big warehouses:
export BLURB_WORKERS=16
(Mind Bedrock / OpenAI rate limits — increasing past ~16 workers usually trips them on a single account.)
Failure modes
Indexing runs are all-or-nothing: on failure the collection is left dropped and the next user-triggered retry starts from a clean slate. Partial progress is thrown away. A 30-min rebuild that dies at minute 25 costs roughly $0.60 + 6 min to redo on a representative ERP warehouse — the simplicity of full rebuilds is worth the occasional redo.
Qdrant is required
Qdrant is a hard dependency for the platform. Without QDRANT_URL set, schema indexing fails and discovery stays blocked until the index is ready; vector search and /ask return 503 Service Unavailable with an explicit error body. The API server logs a warning at startup when QDRANT_URL is not set.
Relevant env vars
| Variable | Default | Purpose |
|---|---|---|
QDRANT_URL | — | Required. host or host:port for Qdrant gRPC |
QDRANT_API_KEY | empty | Optional; set on secured Qdrant instances |
BLURB_WORKERS | 8 | Parallel blurb-generation goroutines |
The Level-0 catalog rendered into the discovery prompt is capped at
schema_render.DefaultCatalogBudgetTokens (150K). This is a constant
in the agent today; if a per-deployment override becomes useful it
should be wired through RenderOptions.Budget rather than re-introduced
as an env var.
The legacy knobs SCHEMA_RETRIEVAL_TOP_K, SCHEMA_RETRIEVAL_TOP_K_NOTOOL,
and the per-project project.schema_retrieval.top_k field were retired
when discovery moved to on-demand schema actions — discovery no longer
performs an upfront top-K retrieval at all, and search_tables
clamps its TopK at the constant ai.MaxSearchTopK (30).