All Products
Search
Document Center

Hologres:Implement hybrid full-text and vector search with RRF

Last Updated:Mar 26, 2026

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 typeRecommended strategyExample
Exact keyword matchFull-text search only"find all documents mentioning 'error 404'"
Semantic similarityVector search only"find documents about login failures"
Keywords + semantic meaningHybrid 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:

  1. Full-text search ranks documents by keyword relevance using text_search().

  2. Vector search ranks documents by embedding similarity using approx_cosine_distance().

  3. 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.03226

A document that appears in only one result list — even if it ranks first there — scores lower:

1/(60 + 1) + 0 = 0.01639

Documents 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 TABLE and INSERT permissions 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   | 3

Full-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.640724242

The 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      | flag1

Documents 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:

PlaceholderWhat 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 > 2Your scalar filter condition (remove if not needed)
3 in the vector dimension checkThe actual dimension count of your embedding model
LIMIT 100 per CTECandidate pool size; increase for higher recall at the cost of latency
In production, generate query vectors from the same embedding model used to populate vector1. The hardcoded test vectors like {2.8, 2.3, 2.4} are placeholders that verify the query structure only.

What's next