Skip to Content
We are live but in Staging 🎉

Search — API Reference

HTTP-only, on the control plane. There is no Search RPC anywhere on the K3 proto surface — it retired with the Milvus read path, and the package that used to carry it (dodil.data.vector.v1) no longer exists. The route is a thin dispatcher over the per-modality *_embedding_search Scriptum templates: each template embeds the query, searches the matching collections, and k3-api RRF-merges the ranked lists across collection groups.

The request has no proto message. The response does — dodil.data.common.v1.SearchResponse, carrying SearchResult and CollectionSearchStatus.

RouteContent-TypeBody limit
POST /:bucket/search/vectorapplication/json — text query1 MiB
POST /:bucket/search/vectormultipart/form-data — file query (image / audio / video)50 MiB

Retrieval and reranking are owned by the search template, not the caller — there are no caller-selectable search modes and no rerank flag that changes behavior. The searchMode, rerank and minScore fields are still parsed for wire compatibility but do not feed the search or filter the results.

Searching by a pre-embedded vector? That’s a data-plane operation — see Pre-embedded KNN below. Sending a vector field to this route returns UNIMPLEMENTED: pre-embedded vector search retired with Milvus (#83) — pending the tabled KNN read-path.

Known limitation — this route returns no results today. The vector_store_search Scriptum native tool still targets the decommissioned Milvus backend and has not been repointed at the tabled data plane. The dispatch shape is correct and the route answers 200, but every participating collection comes back with a non-empty failReason at the search step until that tool moves. Tracked in dodil-k3/docs/vector_search_readpath_followup.md. Until then, use the data-plane KNN paths.

Text query (JSON)

dodil data search "what is multi-head attention" \ -b kb-prod \ --table docs \ --top-k 10

--table (alias --collection) is repeatable and maps onto the collectionNames wire field. -b/--bucket is required.

FieldTypeDefaultDescription
textstringThe query. Required for JSON requests (or send vector, which returns UNIMPLEMENTED).
collectionNamesstring[][]Pin the search to these collections. Empty = all eligible collections in the bucket. Names that match nothing return NOT_FOUND. (Legacy singular collectionName also accepted; snake_case spellings are accepted too.)
topKint10Result count after the merge. <= 0 is coerced to 10.
minScorefloat0Parsed but not applied. Kept for wire compatibility; no score floor is enforced today.

File query (multipart)

For collections built from a visual_embedding_index / face_embedding_index / object_embedding_index template, search by uploading a file — K3 stages it at a temp S3 key, the search template embeds it, then searches:

curl -sS -X POST "https://api.data.dodil.io/kb-prod/search/vector" \ -H "Authorization: Bearer $DODIL_TOKEN" \ -F "file=@./example-bag.jpg;type=image/jpeg" \ -F "top_k=20" \ -F "collection_name=product-images"

Multipart fields: file (required), text (optional accompanying text, used for reranking), top_k, collection_name / collection_names (comma-separated), plus the inert search_mode / rerank. The file’s content type routes it to the right modality (image/*, audio/*, video/* → visual; text/x-* or application/javascript → code; else text). Max upload: 50 MiB.

The upload is staged at .k3-tmp/search/{uuid}/{filename} in the bucket, handed to the template as a 5-minute presigned SigV4 URL, and reaped by a drop guard when the search returns.

How dispatch works

  1. K3 loads the bucket’s kind='vector' destinations and filters by request shape — explicit collectionNames win; a file query auto-filters to its modality; else all collections participate.
  2. Collections that are not active, or that have no bound search pipeline (store_entities.search_pipeline_id), are dropped with a warnings[] entry.
  3. The survivors are grouped by (embed_model, dimensions, embedding_type) — one search-template thread per group embeds the query once and fans out to every member.
  4. Ranked lists from the groups are RRF-merged (k=60) into the final topK. A single group is a pass-through, trimmed to topK — no RRF rescoring happens.

The template owns everything inside a group — retrieval and any reranking are the template’s business, not request knobs. There is no metadata pre-filter: k3-api sends the template a hardcoded empty filter.

Hybrid is not live. Every shipping *_embedding_* template pins enable_bm25 = false and notes that hybrid is unavailable on tabled — the ingest tool hard-errors if it is set true. The plane has no BM25 implementation at all, and while an RRF primitive (k=60) exists in dodil-tables, nothing calls it. Searches today are dense-only. The RRF merge described above is k3-api’s own, across collection groups, and is unrelated.

Response

{ "results": [ { "object": { "bucket": "kb-prod", "key": "papers/attention.pdf" }, "score": 0.0163, "metadata": { "collection": "docs" }, "chunkId": "papers/attention.pdf#c4", "chunkIndex": 4, "source": "SEARCH_SOURCE_VECTOR", "content": "Multi-head attention lets the model..." } ], "pagination": null, "tookMs": "812", "searchModeUsed": "vector", "warnings": [], "collectionStatuses": [ { "collection": "docs", "embeddingCompleted": true, "searchCompleted": true, "failReason": "" } ] }
  • results[] — merged across groups, ranked by RRF score. object points back to the source S3 object; the key is derived from the chunk’s artifact_id ({source_short}:{object_key}).
  • metadata carries the originating collection name. chunkIndex is parsed off the #c<n> suffix of chunkId.
  • source is always SEARCH_SOURCE_VECTOR, and searchModeUsed is always the literal "vector" — neither reflects whether the template ran hybrid retrieval internally.
  • collectionStatuses[] — one entry per participating collection; a non-empty failReason explains why a collection contributed nothing. This is the first place to look when a search returns fewer results than expected.
  • warnings[] — non-fatal issues (e.g. a skipped collection with no search pipeline).
  • pagination is never populated by this route.

Pre-embedded KNN — data plane

The control-plane route will not take a vector. KNN over vectors you already hold is a tabled data-plane operation, reached with a stock client:

dodil data vsearch \ -b kb-prod \ -t chunks \ --column embedding \ --vector "0.12,0.03,…" \ --metric cosine \ --top-k 10

--text embeds client-side via dodil ignite models embed first (default model arctic-embed-m-v2) and then runs the same KNN. --metric accepts cosine | euclidean | dot. There is no -c shorthand — --collection is a long alias for --table.

Endpoints, credentials and the Qdrant / Pinecone SDK forms: Connect & wire adapters.

When the request fails

SymptomCauseFix
INVALID_ARGUMENT: either 'text' or 'vector' field is requiredEmpty JSON bodySend text (or a multipart file)
UNIMPLEMENTED: pre-embedded vector search retired with Milvus (#83)Pre-embedded search is not served by this routeUse Pre-embedded KNN on the data plane
NOT_FOUND: no collections matched: …Every name in collectionNames is absent from the bucketCheck dodil data vector collection list -b $BUCKET
RESOURCE_EXHAUSTED: too many concurrent search requestsPer-pod concurrency limit protecting ScriptumBack off and retry
FAILED_PRECONDITION: bucket SA: …The bucket’s service account was never provisioned (IAM was down at bucket create)Retry — the search path self-heals the provisioning on the next call
Empty results + failReason on every collectionExpected today — vector_store_search still targets Milvus (see the limitation note above)None on the caller’s side; use the data-plane KNN paths
Empty results + a warnings[] entry naming a collectionThat collection has no bound search pipelineRe-create it from a *_embedding_index template so the *_embedding_search counterpart is spawned

See also