Hybrid Search - Reranking Full-Text and Vector Search Results
On this page
Hybrid search combines multiple search methods into a single ranked result set.
Hybrid search is useful when exact terminology matters but semantic relevance is also important.
SingleStore provides both full-text and vector indexes that can accelerate each retrieval method independently.
Sparse and Dense Retrieval
Hybrid search, also referred to as hybrid retrieval, combines sparse and dense retrieval methods:
-
Full-text search is a form of sparse retrieval, where documents are represented by the terms they contain and matched on exact keywords and phrases.
-
Vector search is a form of dense retrieval, where documents are represented as embedding vectors and matched by semantic similarity.
These embeddings are dense because they have no zero entries.
Refer to Working with Full-text Search and Working with Vector Data for more information.
Reciprocal Rank Fusion
A common approach for combining two or more ranked result sets, such as full-text search and vector search rankings, is reciprocal rank fusion (RRF).
The RRF formula for N different ranked lists, where r is the rank of an item in list i, is shown as follows:
In this formula, k is a smoothing factor that reduces the impact of rank differences by making the decrease in weights more gradual.
-
Rank 1: 1.
0 -
Rank 2: 0.
5 -
Rank 3: 0.
33
Using a smoothing factor, such as k = 60, produces a more gradual decline:
-
Rank 1: 1/61 ≈ 0.
0164 -
Rank 2: 1/62 ≈ 0.
0161 -
Rank 10: 1/70 ≈ 0.
0143
RRF can be further refined by weighting each ranking differently.
Examples
The following examples show how to implement hybrid search in SQL by combining full-text search (VERSION 2), vector search, and RRF.
Hybrid search queries generally follow the same pattern:
-
Retrieve candidate results using full-text (sparse) retrieval.
-
Retrieve candidate results using vector (dense) retrieval.
-
Rank each result set independently.
-
Rerank the combined results using the RRF formula.
-
Return the highest-ranked combined results.
In these examples, Common Table Expressions (CTEs, WITH clauses) perform each retrieval method separately, and FULL OUTER JOIN combines their rankings.
You can extend this approach using user-defined functions (UDFs), such as SingleStore external functions or Wasm UDFs, or external application code, to implement additional ranking techniques, including late interaction models, cross-encoders, or other reranking approaches.
Example 1 - Hybrid Search of Wikipedia Articles
This example demonstrates indexed hybrid search over a dataset of Wikipedia articles using full-text search and vector search.
SingleStore has created a dataset of 160M vectors and associated paragraphs to simulate vector-based semantic search over all 6.
Create a table to store the vectors and paragraphs.
CREATE TABLE vecs(id BIGINT(20),url TEXT DEFAULT NULL,paragraph TEXT DEFAULT NULL,v VECTOR(1536) NOT NULL,SHARD KEY(id), KEY(id) USING HASH);
Load data into this table using the following pipeline.
-- since the bucket is open, you can leave the credentials clause as-isCREATE OR REPLACE PIPELINE wiki_pipeline ASload data S3's3://singlestore-docs-example-datasets/wikipedia-video-game-data/video-game-embeddings.csv'config '{"region":"us-east-1"}'credentials '{"aws_access_key_id": "", "aws_secret_access_key": ""}'SKIP DUPLICATE KEY ERRORSINTO TABLE vecsFORMAT csvFIELDS TERMINATED BY ','ENCLOSED BY '"'LINES TERMINATED BY '\r\n';START PIPELINE wiki_pipeline FOREGROUND;
Add a full-text index and a vector index on the table.
ALTER TABLE vecs ADD FULLTEXT ft_para(paragraph);ALTER TABLE vecs ADD VECTOR INDEX ivf_v(v)INDEX_OPTIONS '{"index_type":"IVF_FLAT"}';
Run OPTIMIZE TABLE to ensure the values are indexed.
OPTIMIZE TABLE vecs FLUSH;
The following query searches for rows with information about Super Mario Kart using full-text and vector search:
-
Full-text search finds paragraphs that match the string "
super mario kart". -
Vector search finds paragraphs semantically similar to the introduction of the Super Mario Kart Wikipedia page by comparing embedding vectors.
The variable are set prior to defining the query.
-
@v_stores the vector embedding for the Super Mario Kart Wikipedia page.mario_ kart
At a high-level, the query is structured as follows:
-
Full-text search and vector search retrieve candidate results separately using their respective indexes.
-
Each result set is ranked independently.
-
The rankings are combined using the RRF formula, and the top five results are returned.
The query uses CTEs to create intermediate result sets for each stage.
SET @v_mario_kart = (SELECT v FROM vecsWHERE URL = "https://en.wikipedia.org/wiki/Super_Mario_Kart"ORDER BY id LIMIT 1);WITH fts AS ( /* Find top full-text matches. */SELECTid,paragraph,MATCH(paragraph) AGAINST ("super mario kart") AS SCOREFROM vecsWHERE MATCH(paragraph) AGAINST ("super mario kart")ORDER BY SCORE descLIMIT 200),vs AS ( /* Find top vector search matches. <*> is dot product. */SELECTid,paragraph,v <*> @v_mario_kart AS SCOREFROM vecsORDER BY score DESCLIMIT 200),fts_ranked as (SELECT*,RANK() OVER (ORDER BY SCORE DESC) AS fts_rankFROM fts),vs_ranked as (SELECT*,RANK() OVER (ORDER BY SCORE DESC) as vs_rankFROM vs)SELECT*,0.7 * (1.0 / (NVL(fts_rank, 1000.0) + 60)) + 0.3 * (1.0 / (NVL(vs_rank, 1000.0) + 60)) as combined_scoreFROM fts_ranked f FULL OUTER JOIN vs_ranked v ON f.id = v.idORDER BY combined_score DESCLIMIT 5;
In more detail, this query works as follows:
-
The first two CTEs in the query (
ftsandvs) create ordered lists of 200 rows that best match the full-text and vector search criteria.-
The full-text search block (
fts) uses inverted index search based on Java Lucene. -
The vector search block (
vs) uses approximate nearest neighbor (ANN) indexing.
-
-
The next two CTEs use the
RANKwindow function to create ranked lists (fts_andranked vs_) of these 200 rows.ranked -
The ranked lists are created in separate CTEs to allow the initial CTEs to use indexed search.
-
If the
RANKcalculation were performed directly inside the initialftsandvs, it would prevent those CTEs from using index search.
-
-
The final query block combines the ranked result sets:
-
A
FULL OUTER JOINincludes all items appearing in either ranked list.Note that a standard INNER JOINwould discard items that do not appear in both lists. -
The
SELECTclause calculates the finalcombined_using RRF and returns the five highest-scoring results.score
-
This query uses a smoothing factor of 60 and weights full-text results at 0.NULL ranks, which can arise from the outer join, are given an artificially high rank of 1000 so they don't contribute meaningfully to the score.
RRF allows strong performance in one search method to offset weaker performance in the other.
Example 2 - Hybrid Search of Comments
This example combines hybrid search with a SQL filter.Food" category, and then combines the rankings using RRF.
Consider a table of comments that contains:
-
id– anINTid -
comment– aTEXTfield that stores the text of the comment -
comment_– a VECTOR that stores an embedding capturing the meaning of the comment.embedding as described in Working with Vector Data -
category– aVARCHARthat stores a category assigned to the comment
Create the comments table and insert insert sample data.
CREATE TABLE comments(id INT,comment TEXT,comment_embedding VECTOR(4) NOT NULL,category VARCHAR(256));
INSERT INTO comments VALUES(1, "The cafeteria in building 35 has a great salad bar",'[0.45, 0.55, 0.495, 0.5]',"Food"),(2, "I love the taco bar in the B16 cafeteria.",'[0.01111, 0.01111, 0.1, 0.999]',"Food"),(3, "The B24 restaurant salad bar is quite good.",'[0.1, 0.8, 0.2, 0.555]',"Food");
Add a full-text index to the table and run OPTIMIZE TABLE to ensure the values are indexed.
ALTER TABLE comments ADD FULLTEXT ft_comment(comment);OPTIMIZE TABLE comments FLUSH;
The following SQL query searches for comments that match the word "restaurant" and are similar to a query vector ('[0.) which is intended to represent an embedding of the user's request."Food".
The following variable are set prior to defining the query.
-
@query_stores the vector embedding from the user's request.vec
Similar to Example 1, the query is structured as follows:
-
Full-text search and vector search retrieve candidate comments separately using their respective indexes.
-
The search is restricted to comments in the "
Food" category. -
Each result set is ranked independently.
-
The rankings are combined using the RRF formula, and the top three results are returned.
The @query_ variable is cast to VECTOR to ensure that @query_ is a valid VECTOR and to improve performance.
The @query_ variable is cast to a VECTOR to ensure that @query_ is a valid VECTOR and to improve performance.
SET @query_vec = ('[0.44, 0.554, 0.34, 0.62]'):>VECTOR(4);WITH fts AS ( /* Find top full-text matches. */SELECTid,comment,MATCH(comment) AGAINST ("restaurant") AS ft_scoreFROM commentsWHERE MATCH(comment) AGAINST ("restaurant")AND category = "Food"ORDER BY ft_score descLIMIT 200),vs AS ( /* Find top vector search matches. <*> is dot product. */SELECTid,comment,@query_vec <*> comment_embedding AS vec_scoreFROM commentsWHERE category = "Food"ORDER BY vec_score descLIMIT 200),fts_ranked as (SELECT*,RANK() OVER (ORDER BY ft_score DESC) AS fts_rankFROM fts),vs_ranked as (SELECT*,RANK() OVER (ORDER BY vec_score DESC) as vs_rankFROM vs)SELECT*,0.7 * (1.0 / (NVL(fts_rank, 1000.0) + 60)) + 0.3 * (1.0 / (NVL(vs_rank, 1000.0) + 60)) AS combined_scoreFROM fts_ranked f FULL OUTER JOIN vs_ranked v ON f.id = v.idORDER BY combined_score DESCLIMIT 3;
*** 1. row ***
id: 3
comment: The B24 restaurant salad bar is quite good.
ft_score: 0.46706151962280273
fts_rank: 1
id: 3
comment: The B24 restaurant salad bar is quite good.
vec_score: 0.8993000388145447
vs_rank: 2
combined_score: 0.016314
*** 2. row ***
id: NULL
comment: NULL
ft_score: NULL
fts_rank: NULL
id: 1
comment: The cafeteria in building 35 has a great salad bar
vec_score: 0.9810000061988831
vs_rank: 1
combined_score: 0.005578
*** 3. row ***
id: NULL
comment: NULL
ft_score: NULL
fts_rank: NULL
id: 2
comment: I love the taco bar in the B16 cafeteria.
vec_score: 0.6644233465194702
vs_rank: 3
combined_score: 0.005422In the results, the row with id = 3 has the word "restaurant", so it receives a strong full-text ranking.
Ranking Functions
RRF requires a ranking function in the CTEs.RANK(), DENSE_, and ROW_.
Tied scores are common in full-text search, particularly for short or common query terms.
Note
SingleStore recommends using RANK() for the ranking function when using RRF.
The following table describes the ranking functions and their usage.
|
Function Name |
Functionality |
Usage |
|---|---|---|
|
|
Returns the rank of the current row as specified by the Assigns the same rank to tied rows. |
When ties matter and gaps in ranks are acceptable. |
|
|
Returns the rank of the current row as specified by the Assigns the same rank to tied rows. |
When ties matter and gaps in ranks are not acceptable. |
|
|
Returns the rank of the current row as specified by the The assignment of row numbers among ties is non-deterministic. |
When a unique ranking number on each output row is needed. |
The following table shows an example of the results of the three functions.
|
Score |
|
|
|
|---|---|---|---|
|
100 |
1 |
1 |
1 |
|
90 |
2 |
2 |
2 |
|
90 |
2 |
2 |
3 |
|
80 |
4 |
3 |
4 |
Related Topics
Last modified: