Full-text search matches exact keywords but misses synonyms and paraphrases. Vector search captures semantic similarity but can miss precise terms. Hybrid search runs both in parallel and merges the results using Reciprocal Rank Fusion (RRF), producing a ranked list that outperforms either method alone — especially when queries mix specific terms with semantic intent.
This guide walks through setting up dual-channel retrieval in Hologres and applying RRF to combine the results.
When to use hybrid search
Choose your search strategy based on your query pattern:
| Query type | Recommended strategy | Example |
|---|---|---|
| Exact keyword match | Full-text search only | "find all documents mentioning 'error 404'" |
| Semantic similarity | Vector search only | "find documents about login failures" |
| Keywords + semantic meaning | Hybrid search | "find troubleshooting guides for authentication errors" |
Hybrid search is most effective in customer support portals, knowledge bases, and product catalogs where users mix exact product names with descriptive language. For purely exact-term needs, full-text search alone is simpler and faster.
How RRF works
Hybrid search runs two independent queries, then merges the ranked result lists:
Full-text search ranks documents by keyword relevance using
text_search().Vector search ranks documents by embedding similarity using
approx_cosine_distance().RRF fusion assigns each document a combined score based on its rank in each list, then re-sorts by that score.
RRF score formula
rrf_score = 1/(rrf_rank_constant + full_text_rank) + 1/(rrf_rank_constant + vector_rank)The constant rrf_rank_constant = 60 smooths scores so top-ranked documents don't dominate with extreme values. For example, a document that ranks 1st in vector search and 3rd in full-text search scores:
1/(60 + 1) + 1/(60 + 3) = 0.01639 + 0.01587 = 0.03226A document that appears in only one result list — even if it ranks first there — scores lower:
1/(60 + 1) + 0 = 0.01639Documents that rank highly in both searches consistently outperform documents that rank highly in only one.
Prerequisites
Before you begin, ensure that you have:
A Hologres instance with HGraph vector index support enabled
The
CREATE TABLEandINSERTpermissions on the target schema
Set up the table and indexes
Create a table with both a vector index and a full-text index. The vector index uses HGraph with cosine distance; the full-text index covers the text field.
DROP TABLE IF EXISTS documents;
-- Create a table with a text field, a vector field, and scalar filter fields.
-- The WITH clause configures the HGraph vector index on vector1.
CREATE TABLE documents (
id INT PRIMARY KEY,
text_field TEXT, -- Field for full-text search
vector1 float4[] CHECK (array_ndims(vector1) = 1 AND array_length(vector1, 1) = 3), -- 3-dimensional vector; adjust dimensions for your data
field1 INT, -- Scalar field for filtering
field2 TEXT -- Scalar field for filtering
)
WITH (
vectors = '{
"vector1": {
"algorithm": "HGraph",
"distance_method": "Cosine",
"builder_params": {
"base_quantization_type": "sq8_uniform",
"max_degree": 64,
"ef_construction": 400,
"precise_quantization_type": "fp32",
"use_reorder": true
}
}
}'
);
-- Create a full-text index on the text field.
CREATE INDEX doc_text_field_idx1 ON documents USING FULLTEXT (text_field);Load data
Insert test data, then run VACUUM to flush index buffers, update statistics, and optimize retrieval performance.
INSERT INTO documents (id, text_field, vector1, field1, field2) VALUES
(1, 'hello test5', '{2.5, 2.3, 2.4}', 1, 'flag1'),
(2, 'hello test6 test5', '{2.6, 2.3, 2.4}', 2, 'flag1'),
(3, 'hello test7', '{2.7, 2.3, 2.4}', 3, 'flag1'),
(4, 'hello test8 test7', '{2.8, 2.3, 2.4}', 4, 'flag2'),
(5, 'hello test9', '{2.9, 2.3, 2.4}', 5, 'flag2');
-- Clean up fragments, update statistics, and trigger index optimization.
VACUUM documents;Verify single-channel retrieval
Run each search channel independently to confirm both return expected results before combining them.
Vector search
The following query finds the top 3 documents most similar to vector {2.8, 2.3, 2.4}, filtered to rows where field1 > 2.
SELECT
id,
approx_cosine_distance(vector1, '{2.8, 2.3, 2.4}') AS distance,
ROW_NUMBER() OVER (ORDER BY approx_cosine_distance(vector1, '{2.8, 2.3, 2.4}') DESC) AS vec_rank
FROM documents
WHERE
field1 > 2
AND approx_cosine_distance(vector1, '{2.8, 2.3, 2.4}') > 0
ORDER BY distance DESC
LIMIT 100;Expected output:
id | distance | vec_rank
----+---------------+---------
4 | 1.00000012 | 1
5 | 0.999849677 | 2
3 | 0.999840617 | 3Full-text search
The following query finds documents containing the keyword test5.
SELECT
id,
text_search(text_field, 'test5') AS d
FROM documents
WHERE text_search(text_field, 'test5') > 0
ORDER BY d
LIMIT 10;Expected output:
id | d
----+-------------
1 | 0.287682116
2 | 0.640724242The two channels return different document sets. Hybrid search merges both lists to surface documents relevant to both the keyword and the vector query.
Run hybrid search with RRF
The following query runs both searches as CTEs, then applies RRF fusion to produce a single ranked list. FULL JOIN ensures documents that appear in only one result set are still included.
WITH
-- Step 1: Full-text search with scalar filtering
fulltext_search AS (
SELECT
id,
text_search(text_field, 'test5 test6 test7 test8 test9') AS score,
ROW_NUMBER() OVER (
ORDER BY text_search(text_field, 'test5 test6 test7 test8 test9') DESC
) AS ft_rank
FROM documents
WHERE
field1 > 2
AND text_search(text_field, 'test5 test6 test7 test8 test9') > 0
LIMIT 100
),
-- Step 2: Vector search with scalar filtering
vector_search AS (
SELECT
id,
approx_cosine_distance(vector1, '{2.8, 2.3, 2.4}') AS score,
ROW_NUMBER() OVER (
ORDER BY approx_cosine_distance(vector1, '{2.8, 2.3, 2.4}') DESC
) AS vec_rank
FROM documents
WHERE
field1 > 2
AND approx_cosine_distance(vector1, '{2.8, 2.3, 2.4}') > 0
ORDER BY approx_cosine_distance(vector1, '{2.8, 2.3, 2.4}') DESC
LIMIT 100
)
-- Step 3: Calculate the RRF score and return the top results
SELECT
COALESCE(ft.id, vec.id) AS doc_id,
-- RRF formula: sum(1 / (rrf_rank_constant + rank)); rrf_rank_constant = 60
(CASE WHEN ft.ft_rank IS NOT NULL THEN 1.0 / (60 + ft.ft_rank) ELSE 0 END)
+
(CASE WHEN vec.vec_rank IS NOT NULL THEN 1.0 / (60 + vec.vec_rank) ELSE 0 END)
AS rrf_score,
d.text_field,
d.field1,
d.field2
FROM fulltext_search ft
FULL JOIN vector_search vec ON ft.id = vec.id
LEFT JOIN documents d ON COALESCE(ft.id, vec.id) = d.id
ORDER BY rrf_score DESC
LIMIT 10;Expected output:
doc_id | rrf_score | text_field | field1 | field2
-------+---------------------+-------------------+--------+-------
4 | 0.032522474881016 | hello test8 test7 | 4 | flag2
5 | 0.032522474881016 | hello test9 | 5 | flag2
3 | 0.031746031746032 | hello test7 | 3 | flag1Documents 4 and 5 score highest because they appear in both result sets. Document 4 (hello test8 test7) ranked 1st in vector search and matched the multi-term full-text query, earning the highest combined score.
Adapt this pattern for your data
Replace the test values with your actual data:
| Placeholder | What to replace |
|---|---|
'{2.8, 2.3, 2.4}' | Your query vector, generated from an embedding model |
'test5 test6 test7 test8 test9' | Your user's search terms |
field1 > 2 | Your scalar filter condition (remove if not needed) |
3 in the vector dimension check | The actual dimension count of your embedding model |
LIMIT 100 per CTE | Candidate pool size; increase for higher recall at the cost of latency |
In production, generate query vectors from the same embedding model used to populatevector1. The hardcoded test vectors like{2.8, 2.3, 2.4}are placeholders that verify the query structure only.
What's next
Create a vector index — configure HGraph index parameters for your dataset size and accuracy requirements
Full-text search overview — learn about tokenization options and index tuning for full-text search