Search
Hybrid search
Combine keyword search with vector similarity in a single request. Pass a pre-computed embedding vector alongside your text query, and the engine blends both signals.
Headband does not generate embeddings: compute the vector with your own model (OpenAI, Cohere and so on). The request is the same on Meilisearch, Typesense and Elasticsearch, but each index needs a vector field first: see Vector fields per engine.
/v1/hybrid-searchUnified keyword + vector search. Falls back to keyword-only when no vector is provided.
- Auth
- admin or search key
- Returns
- 200
{
"index": "products",
"q": "wireless headphones",
"vector": [0.12, -0.34, 0.56, ...],
"semanticRatio": 0.7,
"limit": 20,
"filter": "price < 200"
}{
"hits": [
{
"id": "42",
"title": "Wireless headphones",
"_scoring": {
"keyword": 11.2,
"semantic": 0.91,
"combined": 0.0328
}
}
],
"query": "wireless headphones",
"processingTimeMs": 18,
"estimatedTotalHits": 156,
"fallback": false
}Keyword-only fallback
"fallback": true in the response. On Typesense and Elasticsearch it does the same when you send no vector. If hybrid search seems to change nothing, check fallback before tuning semanticRatio.Vector fields per engine
What _scoring means
Each hit carries _scoring with three numbers. combined is the score the hits are ranked by. keyword and semantic are the two parts, but only where the engine reports them. A part the engine does not report is 0, so a 0 does not mean the hit failed to match.
- Elasticsearch, with a vector field:
keywordis the raw BM25 score,semanticthe kNN score, andcombinedthe reciprocal rank fusion score Headband computes from the two lists. - Typesense, with a vector field:
keywordistext_match,semanticis 1 minus the vector distance, andcombinedthe fusion score when Typesense reports one. - Meilisearch, with an embedder: only
combinedis set, to Meilisearch's ranking score (0 to 1).keywordandsemanticare always0, because Meilisearch does not report the two parts. - Keyword-only (
fallback: true):semanticis0andkeywordequalscombined.
The scales differ by engine, so compare scores within one index only, never across engines.
Elasticsearch vectors
Headband creates an Elasticsearch index with dynamic mapping, and its settings API has no mapping field. A float array sent in a document then becomes a plain float field, not a vector, and hybrid search keeps answering fallback: true. Add the dense_vector field on the engine itself, through its dedicated engine endpoint, before any document carries that field:
export ADMIN_KEY="hb_adm_..."
# 1. The engine's endpoint, and the
# index's name on it (engineIndexUid)
headband instances list
headband indexes list
# 2. Map the vector field. dims is your
# embedding model's size.
HOST="<id>.ewr1.search.headband.dev"
INDEX="<engineIndexUid>"
curl -X PUT \
"https://$HOST/$INDEX/_mapping" \
-H "Authorization: ApiKey $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"properties": {
"embedding": {
"type": "dense_vector",
"dims": 1536,
"index": true,
"similarity": "cosine"
}
}
}'
# 3. Add documents with an "embedding"
# array, then search with a vector
API=https://api.headband.dev/v1
curl -X POST \
"$API/hybrid-search" \
-H "Authorization: Bearer $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"index": "products",
"q": "wireless headphones",
"vector": [0.12,-0.34,...],
"semanticRatio": 0.5
}'- Use an admin key on the engine endpoint: an organization-wide one, or a project key of the index's branch. A search key can only search there, so it cannot change a mapping.
- The index's name on Elasticsearch is lower-cased, such as
hbp_<project id>_products. Copy it fromengineIndexUidrather than building it from the uid. - Map the field first. Once a document has put a float array under that name, the field is a number field and Elasticsearch refuses to change it (
mapper [embedding] cannot be changed from type [float]). Then use a new field name, or delete and re-create the index. - With the field mapped, the response carries
fallback: falseand each hit carries its keyword and semantic scores. headband indexes listshowsinstanceIdandengineIndexUidfor an index on a dedicated engine. An index on your own Elasticsearch cluster (a custom engine config) takes the same mapping on that cluster directly.