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.
Why the lexical leg matters for AEC
Section titled “Why the lexical leg matters for AEC”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.
Request
Section titled “Request”/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" }'import requests
r = requests.post( "https://api.flinq.ai/v1/search", headers={"Authorization": "Bearer flq_your_key_here"}, json={ "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", },)hits = r.json()["data"] # sorted by fused score, index points into documentsconst r = await fetch('https://api.flinq.ai/v1/search', { method: 'POST', headers: { Authorization: 'Bearer flq_your_key_here', 'Content-Type': 'application/json', }, body: JSON.stringify({ 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', }),});const { data: hits } = await r.json();Response
Section titled “Response”{ "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.
Parameters
Section titled “Parameters”| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
model | string | yes | — | Pilot model id used for the dense leg. Unknown ids return 404. |
query | string | yes | — | The search query, truncated at 512 tokens. |
documents | string[] | yes | — | Candidate 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_k | integer | no | 10 | Number of hits returned. Values above len(documents) return the whole pool. |
mode | string | no | "hybrid" | "hybrid" (dense + BM25, RRF-fused), "dense" (embeddings only), "bm25" (lexical only). See the section below for when to isolate a leg. |
dimensions | integer | no | 1024 | Matryoshka 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.
| Field | Type | Description |
|---|---|---|
data[i].index | integer | Position of the hit in your documents array. |
data[i].score | float | Ranking 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].text | string | The document text, echoed back for convenience. |
usage.total_tokens | integer | Billed 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.
Dense-only and BM25-only
Section titled “Dense-only and BM25-only”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.