Docsfra
API Documentation
v1

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

ParameterRequiredDescription
qyesSearch query (returns 400 invalid_request if missing/empty)
top_knoNumber of results to return, 1-20, default 5
document_idnoIf provided, search is scoped to a single document (d_...)
modenohybrid (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.
  • text is the chunk's full raw text — you can pass it directly as context to your own prompt (unlike /ask's citations[].snippet field, it is not truncated to 200 characters).
  • page_no / section are the chunk's location within the source document (page attribution); on some chunks these fields may be null (when page/section info could not be extracted). document.id and document.external_item_ref show which document it came from.
  • The meaning of score changes depending on mode: in hybrid it is the combined ranking value computed via RRF (not an absolute similarity measure, it is only meaningful for ranking within your own result list); in semantic it is the raw cosine similarity score returned by Qdrant. Do not compare score values 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

HTTPcodeDescription
400invalid_requestq is missing/invalid, or document_id could not be resolved as an opaque id
401unauthorizedMissing, invalid, or revoked key
402insufficient_creditBalance is insufficient for the per-query charge
403service_not_enabledsearch is not in the key's entitlements list
429rate_limitedtenant_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.