Semantic Search (Hybrid Search)
GET /v1/search performs chunk-level hybrid search over your tenant's
indexed documents: it combines vector (meaning/semantic) search with
keyword (full-text/FTS) search using RRF (Reciprocal Rank Fusion).
Results are page-attributed — they show which document, which page, and
which section they came from.
Ranking pipeline: results are fused with weighted RRF (vector + full-text), then re-ranked by a GPU cross-encoder (
bge-reranker-v2-m3) for final ordering. If the reranker is unavailable, RRF ordering is served — the response shape never changes.
This endpoint is a retrieval primitive: it does NOT perform LLM synthesis, it only ranks and returns relevant text chunks. It's the building block if you want to build your own RAG/context layer. If you want a ready-made, synthesized single answer, see Question-Answer.
For search to return results, the document must first be completed
(GET /v1/jobs/{id} -> completed) and the background AI-indexing
(chunk -> embed -> vector) step must have finished; this step runs
automatically and in the background after completed — see
Polling and Webhook.
Why hybrid search is necessary
Vector (semantic) search finds text that is close in meaning but doesn't use the same words as the query (e.g. a "payment term" query can catch a chunk containing "shall be remitted within 30 days"). But it's weak for terms that need an exact match, like B/L number, invoice amount, or company name.
Keyword (FTS, full-text) search has the opposite profile: it never misses exactly matching terms, but it can't find text that's close in meaning yet uses different words.
mode: hybrid (default) draws a wide candidate pool from both signals,
then reduces these two rankings into a single combined ranking using
RRF: if a chunk ranks highly in both lists, its combined score rises.
This way, one signal compensates for what the other misses on its own.
If you only want the semantic signal, you can disable the keyword side
with mode: semantic.
Auth
Like all M2M endpoints, it requires an Authorization: Bearer <api_key>
header (dip_live_... or dip_test_..., see Getting Started and Auth).
Query parameters
| Parameter | Required | Description |
|---|---|---|
q | yes | Search query (returns 400 invalid_request if missing/empty) |
top_k | no | Number of results to return, 1-20, default 5 |
document_id | no | If provided, search is scoped to a single document (d_...) |
mode | no | hybrid (default, vector + keyword) or semantic (vector only) |
Response
{
"query": "invoice payment terms",
"mode": "hybrid",
"results": [
{
"chunk_id": "8f14e45f-ceea-467e-9c2b-9d70a1cd0dc9",
"score": 0.031147,
"text": "Fatura tutari, teslim tarihinden itibaren 30 gun icinde odenir...",
"document": { "id": "d_9c1a4e21", "external_item_ref": "INV-2026-0142" },
"page_no": 3,
"section": "Payment Terms"
}
]
}
results[]is returned sorted so the most relevant result comes first; each element is not the whole document, but a single chunk.textis the chunk's full raw text — you can pass it directly as context to your own prompt (unlike/ask'scitations[].snippetfield, it is not truncated to 200 characters).page_no/sectionare the chunk's location within the source document (page attribution); on some chunks these fields may benull(when page/section info could not be extracted).document.idanddocument.external_item_refshow which document it came from.- The meaning of
scorechanges depending onmode: inhybridit is the combined ranking value computed via RRF (not an absolute similarity measure, it is only meaningful for ranking within your own result list); insemanticit is the raw cosine similarity score returned by Qdrant. Do not comparescorevalues across the two modes — their scales differ.
Credit and metering
The search module costs 0.02 credit per query (default pricing);
every successful call is deducted from the balance even if the result
list comes back empty — what's being charged is the retrieval running,
not the number of results returned. dip_test_ keys are exempt from
this charge; the call is still metered (usage is recorded), it's just
not deducted from the balance.
If the balance is insufficient, the request fails before it even runs
(at the precheck stage) with 402 insufficient_credit — a
failed/rejected request is not charged.
Errors
| HTTP | code | Description |
|---|---|---|
| 400 | invalid_request | q is missing/invalid, or document_id could not be resolved as an opaque id |
| 401 | unauthorized | Missing, invalid, or revoked key |
| 402 | insufficient_credit | Balance is insufficient for the per-query charge |
| 403 | service_not_enabled | search is not in the key's entitlements list |
| 429 | rate_limited | tenant_search throttle scope (default 300/min) exceeded |
When to use it
If you want to build your own RAG pipeline (building context for your
own LLM prompt, ranking/filtering results in your own UI, etc.),
/v1/search is the right choice — it's a retrieval primitive, it does
not synthesize. If you want a ready-made, page-attributed answer
synthesized by an LLM, the next step is the
Question-Answer (POST /v1/ask) endpoint; it uses the same
hybrid engine but synthesizes the results on your behalf.