Skip to content

Hybrid search

POST /v1/search ranks a set of candidate documents against a query. It runs two retrievers and fuses their results:

  • A dense retriever that scores by embedding similarity, so it understands meaning and paraphrase.
  • A BM25 lexical retriever that scores by exact term overlap, so it rewards the precise code, number or unit appearing verbatim.

The two ranked lists are combined with reciprocal rank fusion (RRF), which merges by rank position rather than by raw score. That keeps the fusion robust when the two retrievers live on different scales, and lets each leg promote a document the other missed.

You can isolate a leg with the mode parameter: hybrid (default), dense, or bm25.

Pure dense retrieval is strong at meaning and weak at exactness. In construction, exactness is the job. The lexical leg is what makes that reliable:

  • Standard and position codes. A query that names a specific standard reference (a DIN code in Germany, an ÖNORM in Austria) or catalog code must surface the line that carries that exact code, not a semantically adjacent one.
  • Position numbers. Bidding documents across Europe are organized by position numbers (01.02.030, Positionsnummern in a German LV). These are identifiers, not prose, and BM25 matches them precisely.
  • Units and dimensions. DN 100, C25/30, 230 V, 60 s, 5 bis 1000 lx: the lexical leg ensures the literal value carries weight, so the result with the right number ranks above the one that merely reads similarly.

Dense retrieval supplies recall and paraphrase tolerance. BM25 supplies the exact-match precision the domain demands. RRF gives you both in one ranked list.

/v1/search is a flinq-native endpoint (not part of the OpenAI schema), so Python and JavaScript call it with a plain HTTP client. model is required.

curl https://api.flinq.ai/v1/search \
-H "Authorization: Bearer flq_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"model": "flinq-pilot-otter",
"query": "Dämmerungsschalter 230 V AC, Einstellbereich 5 bis 1000 lx",
"documents": [
"Dämmerungsschalter 230 V AC, Einstellbereich 5-1000 lx, Einschaltverzögerung 60 s",
"Bewegungsmelder Aufputz 360 Grad, Reichweite 8 m",
"Zeitschaltuhr digital, Wochenprogramm, 230 V"
],
"top_k": 3,
"mode": "hybrid"
}'
{
"object": "search",
"model": "flinq-pilot-otter",
"mode": "hybrid",
"data": [
{
"index": 0,
"score": 0.0328,
"text": "Dämmerungsschalter 230 V AC, Einstellbereich 5-1000 lx, Einschaltverzögerung 60 s"
},
{
"index": 2,
"score": 0.0161,
"text": "Zeitschaltuhr digital, Wochenprogramm, 230 V"
},
{
"index": 1,
"score": 0.0159,
"text": "Bewegungsmelder Aufputz 360 Grad, Reichweite 8 m"
}
],
"usage": { "prompt_tokens": 84, "total_tokens": 84 }
}

Each hit carries the original index into your documents array, the fused score, and the text. Scores are RRF scores, so read them as a ranking rather than as a calibrated probability.

ParameterTypeRequiredDefaultDescription
modelstringyesPilot model id used for the dense leg. Unknown ids return 404.
querystringyesThe search query, truncated at 512 tokens.
documentsstring[]yesCandidate pool to rank. Per-model cap: 2048 (otter) / 256 (wombat), 422 over it. Ranking happens within this pool only/v1/search is stateless and stores nothing; bring the candidates on every call.
top_kintegerno10Number of hits returned. Values above len(documents) return the whole pool.
modestringno"hybrid""hybrid" (dense + BM25, RRF-fused), "dense" (embeddings only), "bm25" (lexical only). See the section below for when to isolate a leg.
dimensionsintegerno1024Matryoshka slice for the dense leg (1024/768/512/256/128). Ignored by the BM25 leg.

Headers. Authorization: Bearer flq_... (required); Idempotency-Key (optional) deduplicates the charge on retries.

Response fields.

FieldTypeDescription
data[i].indexintegerPosition of the hit in your documents array.
data[i].scorefloatRanking score — its meaning depends on mode: RRF fusion value (hybrid, typically ~0.01–0.03, read as rank not probability), cosine similarity (dense, comparable across requests of the same model), or raw BM25 (bm25, corpus-relative).
data[i].textstringThe document text, echoed back for convenience.
usage.total_tokensintegerBilled tokens: tokens(query) + tokens(documents) — the whole pool is embedded, regardless of top_k or mode.

Billing. (tokens(query) + Σ tokens(documents)) × EUR 0.40 / 1M. Large candidate pools dominate the cost — pre-filter cheaply where you can, then rank the shortlist here.

Hybrid is the default because it wins on mixed workloads, but both legs are available on their own via mode — the endpoint does not force BM25 on you:

  • "mode": "dense" — pure embedding similarity, scores are cosine values. Use this when you run your own lexical layer (your search engine’s BM25, a keyword filter, a code-exact index) and only want flinq’s semantic ranking to fuse yourself, or when your queries are natural-language paraphrases with no exact codes.
  • "mode": "bm25" — pure lexical ranking, no embedding call. Useful as a cheap baseline or when the query is nothing but exact identifiers.
curl https://api.flinq.ai/v1/search \
-H "Authorization: Bearer flq_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"model": "flinq-pilot-wombat",
"query": "Leuchte schaltet bei Daemmerung automatisch",
"documents": ["...", "..."],
"mode": "dense"
}'

Dense-mode scores are plain cosine similarities (comparable across requests of the same model), while hybrid scores are RRF rank fusions.