Pipeline Collection
Goal: stand up a searchable vector collection where every uploaded document gets chunked + embedded + indexed automatically — no manual ingestion code.
Template used: text_embedding_index — the canonical text/PDF/docx/HTML/audio/video RAG-ingest template. The same pattern works for any *_embedding_index template in the vector catalog.
Shape:
document upload ──► bucket ──► YOUR ingest rule matches
│
▼
index pipeline runs in Scriptum
(chunks + embeds via Ignite)
│
▼
vector collection
(lazy-materialized on first ingest)
│
▼
POST /:bucket/search/vector
(the bound *_embedding_search pipeline)Prerequisites
dodilCLI +dodil auth logindone — CLI Basics- A bucket —
kb-prod:dodil data bucket create kb-prod -d "RAG corpus"
Vector is enabled by default — no engine to configure; collections provision on demand.
Check this flow end-to-end before you rely on it. K3’s half is fully cut over to the tabled plane — both the ingest and search dispatch envs carry only
K3_DB_ID+ the bucket SA pair + the embed shape, and a test asserts the retiredMILVUS_*/VBASE_*keys are absent (crates/k3-ingest/src/dispatch_env.rs:22-37,crates/k3-scriptum/src/env.rs:211-213). What is not confirmable fromdodil-k3ordodil-tablesis whether Scriptum’svector_store_*native tools have been repointed at tabled;dodil-k3/docs/vector_search_readpath_followup.mdtracks that as the outstanding item. Until it lands, ingest and search fail softly — as a job error or a per-collectionfailReason— not with a clear signal that the tool is pointed at a decommissioned plane.
1. Browse + pick a template
dodil data vector templates -o json | jq '.templates[] | {id, modalities, acceptedExtensions}'For text + PDF + docx ingest, pick text_embedding_index. Inspect its contract to see what (if any) template_inputs it needs:
dodil data template get text_embedding_index -o json | jq '.contract.inputs'text_embedding_index has no required runtime inputs — everything has a contract default. Other templates may require inputs:
| Template | Required template_inputs | Notes |
|---|---|---|
text_embedding_index | none | Chunk size + embed model come from contract defaults |
code_embedding_index | none | Language auto-detected from content_type / extension |
visual_embedding_index | none | Modality auto-routed (image / video / audio / pdf) |
face_embedding_index | none | SCRFD face detection then embed each face crop |
object_embedding_index | labels: [string] | Open-vocabulary detection — you supply the label vocabulary |
2. Create the collection
For text_embedding_index (no required inputs), the CLI works directly:
export PIPELINE_ID=$(dodil data vector collection add docs -b kb-prod \
--description "PDF / docx / HTML embeddings" \
--template text_embedding_index \
-o json | jq -r '.pipelineId')For object_embedding_index — whose labels input is an array, and --set sends only strings — use the API:
curl -sS -X POST "https://api.data.dodil.io/kb-prod/pipelines/vector" \
-H "Authorization: Bearer $DODIL_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"bucket": "kb-prod",
"name": "products",
"description": "Product image objects",
"templateId": "object_embedding_index",
"templateInputs": {
"labels": ["bottle", "bag", "shoe", "watch", "jewelry"]
}
}'What K3 created
CreateVectorPipeline writes three rows and talks to no data plane:
| Created | Row | What it does |
|---|---|---|
docs destination | store_entities (kind='vector') | Written active immediately; the physical collection materializes on first ingest |
| The index pipeline | pipelines (role=index) | Bound to text_embedding_index; destination = the new collection. This is the id you captured. |
| The search pipeline | pipelines (role=search) | Derived as text_embedding_search and bound via store_entities.search_pipeline_id. Best-effort — a failure leaves the collection indexable but unsearchable. |
Inspect what you got — get takes the pipeline id:
dodil data vector collection get "$PIPELINE_ID" -b kb-prod \
| jq '{pipelineId, scriptumTemplate, name: .destination.name, status: .destination.status, vector: .destination.vector}'3. Create the ingest rule
CreateVectorPipelinedoes not derive a rule. Callers own rule scope — that is stated in the proto (k3_pipeline.proto:523-524) and there is no rule-creation call anywhere underbin/api/src/services/pipeline/. Skip this step and your uploads will sit in the bucket, unembedded.
dodil data ingest add docs-corpus -b kb-prod \
-c "$PIPELINE_ID" \
-i '**/*.pdf' -i '**/*.txt' -i '**/*.md'
RULE_ID=$(dodil data ingest list -b kb-prod -p "$PIPELINE_ID" -o json | jq -r '.rules[0].ruleId')-c / --collection is a historical flag name — it sets CreateRuleRequest.pipeline_id. Mirror the globs on the template’s acceptedExtensions from step 1; a rule whose patterns don’t match your keys never fires.
4. Upload documents
# A PDF
curl -sSL https://arxiv.org/pdf/1706.03762.pdf -o attention.pdf
dodil data object create ./attention.pdf -b kb-prod -k papers/attention.pdf
# Plain text
cat > intro.txt <<'EOF'
The Transformer architecture introduced multi-head attention as a way for
the model to jointly attend to information from different representation
subspaces at different positions.
EOF
dodil data object create ./intro.txt -b kb-prod -k papers/intro.txtEach upload matches your rule → spawns an ingest job → runs text_embedding_index → writes embeddings to the docs collection.
5. Watch the ingest jobs
dodil data ingest jobs -b kb-prod -p "$PIPELINE_ID" -o json \
| jq '.jobs[] | {object: .object.key, status, chunksCreated, embeddingsWritten}'Status path: PENDING → PROCESSING → COMPLETED; the full IngestStatus set also has FAILED, PARTIAL and RETRYING (the last carries attempt N/M: <last error> in error). dodil data ingest watch <job-id> -b kb-prod blocks until a job is terminal. When done:
[
{"object": "papers/attention.pdf", "status": "INGEST_STATUS_COMPLETED", "chunksCreated": 47, "embeddingsWritten": 47},
{"object": "papers/intro.txt", "status": "INGEST_STATUS_COMPLETED", "chunksCreated": 1, "embeddingsWritten": 1}
]chunksCreated == embeddingsWritten is the happy path. If embeddingsWritten is lower, see Pipelines → Replay & Retry.
6. Search
Semantic text search over HTTP — the bound text_embedding_search pipeline embeds the query server-side and runs retrieval (see Recipes → Hybrid Search). This route is the search path for pipeline collections; the wire adapters are not, because the physical table is named k3_<uuid> and you never named its vector column.
Python (requests)
import os, requests
BASE = "https://api.data.dodil.io"
HEADERS = {"Authorization": f"Bearer {os.environ['DODIL_TOKEN']}"}
resp = requests.post(
f"{BASE}/kb-prod/search/vector",
headers=HEADERS,
json={
"text": "what is multi-head attention",
"collectionNames": ["docs"],
"topK": 5,
},
).json()
for w in resp.get("warnings", []):
print("WARN:", w)
for r in resp["results"]:
print(round(r["score"], 4), r["object"]["key"], (r.get("content") or "")[:200])Read warnings before concluding you have no data: a collection with no bound search pipeline is skipped and named there, not in collectionStatuses.
Sending
{"vector": [...]}here returnsUNIMPLEMENTED— the pre-embedded fast path went with Milvus and the tabled KNN read-path is still pending. For KNN by vector, own the table and the column: see External Collection.
7. Inspect the resolved schema
Every schema fact came from the template’s ScriptContract at create time — inspect it before doing multi-collection search, because the compatibility group key is (embedModel, dimensions, embeddingType):
dodil data vector collection get "$PIPELINE_ID" -b kb-prod \
| jq '{
status: .destination.status,
v: (.destination.vector | {dimensions, embeddingType, distanceMetric, sparseMode, embedModel, modality, physicalName})
}'Shape of the answer (the exact values come from the template you picked, not from K3):
{
"status": "DESTINATION_STATUS_ACTIVE",
"v": {
"dimensions": 1024,
"embeddingType": "EMBEDDING_TYPE_FLOAT",
"distanceMetric": "DISTANCE_METRIC_COSINE",
"sparseMode": "SPARSE_MODE_BM25",
"embedModel": "jina-embeddings-v4",
"modality": "text",
"physicalName": "k3_a1b2c3d4…"
}
}K3’s own fallbacks, when a contract declares nothing, are cosine / float / sparse_mode = none / modality = text — embed_model and dimensions have no fallback and a template that can’t resolve them is rejected with FAILED_PRECONDITION at create time.
Common gotchas
| Symptom | Cause | Fix |
|---|---|---|
| Upload succeeds but no ingest job spawns | No rule exists, or its globs don’t match the path/extension | Create one (step 3); dodil data ingest list -b kb-prod -p "$PIPELINE_ID" and check includePatterns |
Jobs COMPLETED but search returns nothing | Index still building (the first ingest is slower), or the search pipeline never spawned | Wait 10–30 s and re-search; check the response’s warnings[] for “no bound search pipeline” |
INVALID_ARGUMENT naming a missing template input | The template requires it and it has no contract default | Use the API with a typed templateInputs map — --set sends strings only |
| Need to pause ingestion without deleting | Use the rule’s enabled flag | dodil data ingest update $RULE_ID -b kb-prod --enabled=false |
| Re-uploaded same key → duplicate embeddings | The pipeline fires on every PUT, including overwrites | For exact-control dedup, use the external-collection flow (idempotent upserts by point id) |
Variations — other vector templates
Same recipe, different template:
| Want to index | Template | Notes |
|---|---|---|
| Source code | code_embedding_index | AST-aware chunking via tree-sitter (rust / python / js / ts / go / java / cpp / …) |
| Mixed media (images / video / audio / PDF page renders) | visual_embedding_index | Multimodal — useful for combined visual + textual recall |
| Faces in photos | face_embedding_index | SCRFD detection + per-face crops + embed |
| Objects in images (open-vocab) | object_embedding_index | Requires labels runtime input |
All four are created the same way — three rows, no rule — and all four need a rule of your own.
Cleanup
# 1. Disable the rule first (stops new ingests)
dodil data ingest update "$RULE_ID" -b kb-prod --enabled=false
# 2. Delete the rule, then the pipeline
dodil data ingest delete "$RULE_ID" -b kb-prod
dodil data vector collection delete "$PIPELINE_ID" -b kb-prod
# 3. (Optional) Delete the objects and the bucket
dodil data object remove papers/attention.pdf -b kb-prod
dodil data object remove papers/intro.txt -b kb-prod
dodil data bucket delete kb-proddodil data vector collection delete and dodil data pipeline delete are the same RPC (DeletePipeline) — you need one, not both. For a whole recipe-installed chain, POST /:bucket/pipelines/_batch-delete unwinds rules → pipelines → destinations in dependency order, idempotently.
Deleting a pipeline removes wiring, not data. Dropping the physical vector collection on the plane is something tenants do themselves through the tables-gateway (
k3_pipeline.proto:149-152).
See also
- External Collection — opposite shape: you own the embedding pipeline, push vectors directly
- Multi-collection Search — search docs + code + assets in one query
- Hybrid + Rerank — improve precision on the pipeline-mode collection from this recipe
- Pipelines → Replay & Retry — recover from failed ingests
- Templates — API Reference — vector catalog spec