Hybrid search
Hybrid search combines fulltext search and vector search into a single ranked result: each document is scored by both its text relevance and its embedding similarity, and the two rankings are fused into one. This brings together the precision of lexical matching and the recall of semantic similarity, and is a common building block for the retrieval stage of Retrieval-Augmented Generation (RAG).
For the general idea of hybrid search, see Hybrid search concept.
Hybrid search is not a separate index type. It reuses two existing indexes on the same table:
- a fulltext_relevance index over the text column — provides the BM25 relevance signal;
- a vector_kmeans_tree index over the embedding column — provides the nearest-neighbor (semantic) signal.
Preparing the indexes
Create a table with a text column and an embedding column, then add both indexes.
CREATE TABLE documents (
id Uint64,
text Utf8,
embedding String,
PRIMARY KEY (id)
);
Add a fulltext_relevance index over the text column (relevance scoring requires this type, not fulltext_plain):
ALTER TABLE documents
ADD INDEX ft_idx
GLOBAL USING fulltext_relevance
ON (text)
WITH (tokenizer=standard, use_filter_lowercase=true);
Add a vector_kmeans_tree index over the embedding column:
ALTER TABLE documents
ADD INDEX vec_idx
GLOBAL USING vector_kmeans_tree
ON (embedding)
WITH (distance=cosine);
For details on each index type, see Fulltext Indexes and Vector Indexes.
Running a hybrid query
A hybrid query is a regular SELECT over the base table (without VIEW) whose ORDER BY key is a single HybridRank call. HybridRank takes one scoring expression per branch: a FullTextScore for the text branch and a Knn distance or similarity for the vector branch.
PRAGMA ydb.KMeansTreeSearchTopSize = "10";
$queryText = "machine learning";
$queryVector = Knn::ToBinaryStringFloat([0.1, 0.2, 0.3, 0.4]);
SELECT id, text
FROM documents
ORDER BY HybridRank(
FullTextScore(text, $queryText),
Knn::CosineDistance(embedding, $queryVector))
LIMIT 10;
Both inputs come from the same user query: $queryText is the search text (matched lexically), and $queryVector is its embedding computed by the application (matched semantically). YDB cannot compute embeddings itself, so the vector is passed in — here it is built from a literal via Knn::ToBinaryStringFloat, but in an application it is the output of an embedding model.
YDB resolves each branch to its index automatically by the scored column (text → the fulltext relevance index, embedding → the vector index), retrieves a candidate pool from each, and fuses the two rankings. The result is the top LIMIT documents by the fused score.
Note that, unlike fulltext and vector search, a hybrid query does not use VIEW IndexName: the indexes are selected from the HybridRank arguments, and the query reads through the base table.
PRAGMA ydb.KMeansTreeSearchTopSize controls the recall of the vector branch — see KMeansTreeSearchTopSize. As with plain vector search, it should be set explicitly.
Tuning the fusion
The fusion method and its parameters are passed as named arguments to HybridRank. The most common ones:
Mode—"rrf"(default, Reciprocal Rank Fusion) or"linear"(weighted sum of normalized scores);Weights— a per-branch weight tuple, one value per scoring argument, to bias the ranking toward one signal;K— the RRF constant (default60.0);Indexes/Limits— explicit per-branch index names and candidate-pool sizes.
For example, to weight the vector branch twice as much as the text branch under RRF:
$queryText = "machine learning";
$queryVector = Knn::ToBinaryStringFloat([0.1, 0.2, 0.3, 0.4]);
SELECT id, text
FROM documents
ORDER BY HybridRank(
FullTextScore(text, $queryText),
Knn::CosineDistance(embedding, $queryVector),
(1, 2) AS Weights)
LIMIT 10;
For the full list of parameters and their semantics, see Hybrid search (HybridRank).
Limitations
- The table must have both a ready
fulltext_relevanceindex and a non-prefixedvector_kmeans_treeindex over the respective columns; otherwise the query fails with a clear message. - Prefixed vector indexes are not supported yet.
- If more than one fulltext (or vector) index matches a branch's column, the branch is ambiguous and must be disambiguated with an explicit
AS Indexesoverride. LIMITmust be a literal, because it sizes the per-branch candidate pools. To use a parameterizedLIMIT, pass explicitAS Limits.HybridRank(...)must be the entireORDER BYkey — it cannot be negated, wrapped, or combined with other sort keys.