SereneDB
> use case

Hybrid Search with SereneDB

Hybrid search here is one index and one SQL query: BM25 over the text column and ANN over the embedding column, in the same statement.

No separate vector database, no sync pipeline between two systems, and nothing to keep consistent — the two signals are columns in one inverted index.

one query, two signals
keyword · bm25trail running shoeroad running shoe
+
vector · anntrail running shoemarathon racer
▼ fused
1trail running shoe2road running shoe3marathon racer
Keyword search ranks products with BM25; vector search ranks them by embedding similarity with ANN. The two lists are fused into one ranking: trail running shoe, road running shoe, then marathon racer.
> overview

What Is Hybrid Search?

Hybrid search runs two retrieval signals over the same corpus and combines their results. Keyword search — BM25 over a full-text search index — matches the words the user actually typed and ranks by term statistics. Vector search compares embeddings and ranks by distance, so it matches meaning rather than spelling. A hybrid search engine keeps both and merges the two ranked lists into one.

The example from our own cookbook: a shopper searches running. Keyword search returns trail running shoe and road running shoe and misses marathon racer entirely — the word is not there. Vector search ranks marathon racer second by distance, because the embedding knows what a marathon is. Hybrid search returns all three, with the two documents that scored on both signals at the top.

query: running
keyword
trail running shoeroad running shoe— no match
vector
trail running shoemarathon racerroad running shoe
▼ rrf · Σ 1/(60 + rank)
1trail running shoe2road running shoe3marathon racer
For the query running, lexical BM25 and vector ANN produce separate ranked lists. Reciprocal Rank Fusion combines their ranks using score = sum(1 / (60 + rank)). The resulting list ranks trail running shoe first, road running shoe second, and marathon racer third.
> the problem

Why Hybrid Search Matters

Each signal on its own has a failure mode that the other one does not have. A hybrid search engine exists because those two failure modes do not overlap.

01 · recall

The keyword gap

Keyword matching does not know synonyms or meaning. A search for laptop does not find notebook computer; a search for running does not find marathon racer. The query was correct and the document exists, and the user still gets zero results — which they read as "the feature is not there".

02 · precision

The vector gap

Vector search always returns something, and near-neighbours are not always relevant. Ask for the SKU AB-1234 and an embedding has no idea what that string means — it returns ten plausible products, none of them the one. Exact matches on identifiers, error codes and function names are exactly what distance loses.

03 · both

How hybrid search closes both

Either one signal constrains and the other ranks — keyword filters to the exact matches, vector orders them by meaning — or both signals rank and their ranks are fused. Under Reciprocal Rank Fusion a document that placed well on both lists ends up above a document that only placed well on one.

> architecture

How SereneDB Does Hybrid Search

As a hybrid search database SereneDB has one structure to explain: a single inverted index over one table covers the text column for BM25, the vector column for IVF ANN, and any verbatim or structured columns you filter on. Both query strategies below read that same index.

table
items
id INTEGER name VARCHAR emb FLOAT[3]
one inverted index
items_idx
name en → bm25 emb ivf → ann id → verbatim
two query paths
Filtered ANN
WHERE @@ … ORDER BY <->
RRF
two ranked branches → Σ 1/(k + rank)
The items table contains id, name, and emb columns. One inverted index, items_idx, indexes name with an English dictionary for BM25, emb with IVF for ANN, and id verbatim. Both filtered ANN and Reciprocal Rank Fusion query this same index. Filtered ANN applies a text predicate before ranking by vector distance; RRF combines two ranked branches with score = sum(1 / (k + rank)).

One index, two signals

One CREATE INDEX … USING inverted() names the text column with a dictionary for BM25 and the vector column with ivf for ANN. It is one index over one table, not two services with a pipeline between them.

sql
CREATE INDEX items_idx ON items
    USING inverted (id, name en, emb ivf (metric = 'l2'));
SQL creates one inverted index over the items table, combining an English text dictionary for BM25, IVF embeddings for ANN, and a verbatim id column.
strategy 1

Filtered ANN

One signal is a hard filter, the other ranks. The shopper wants shoes and nothing else, ordered by semantic closeness: a full-text predicate constrains the candidate set, and vector distance sorts what survives.

sql
SELECT id, name FROM catalog_idx
WHERE  name @@ ts_phrase('shoes')
ORDER  BY emb <-> [1.0, 0.0, 0.0]::FLOAT[3]
LIMIT  2;
The SQL query restricts candidates with a full-text predicate on the phrase shoes, then orders matching items by embedding distance. Text is the filter and vector similarity determines the ranking.
strategy 2

Reciprocal Rank Fusion (RRF)

Both signals rank. BM25 produces one ordered list, vector distance another, and the ranks are fused as 1/(k + rank) — so scales never have to be calibrated against each other, and a document good on both lists rises.

sql
WITH fused AS (
    SELECT id, RANK() OVER (ORDER BY s DESC) AS rank FROM (
        SELECT id, BM25(items_idx.tableoid) AS s
        FROM   items_idx WHERE name @@ 'running'
        ORDER  BY s DESC LIMIT 100) lex
    UNION ALL
    SELECT id, RANK() OVER (ORDER BY dist) AS rank FROM (
        SELECT id, emb <-> [1.0,0.0,0.0]::FLOAT[3] AS dist
        FROM   items_idx
        ORDER  BY dist LIMIT 100) vec
)
SELECT id FROM fused GROUP BY id
ORDER  BY SUM(1.0 / (60 + rank)) DESC LIMIT 4;
The SQL query ranks BM25 and vector results separately, combines them with UNION ALL, groups by item, and orders by the sum of 1 / (60 + rank), returning the top four items.
> build it

Step-by-Step: Build a Hybrid Search in 5 Minutes

01

Create the dictionary and the table

The dictionary decides how text is tokenized. position = true is what makes phrase queries possible later.

sql
CREATE TEXT SEARCH DICTIONARY en (
    template = 'text', locale = 'en_US.UTF-8',
    case = 'lower', stemming = false,
    accent = false, frequency = true, position = true
);

CREATE TABLE items (
    id   INTEGER PRIMARY KEY,
    name VARCHAR,
    emb  FLOAT[3]
);
Step 1: Create the dictionary and the table.
02

Create the hybrid index

Text column, vector column and key in one statement. This is the whole of "two systems" collapsing into one line.

sql
CREATE INDEX items_idx ON items
    USING inverted (id, name en, emb ivf (metric = 'l2'));
Step 2: Create the hybrid index.
03

Insert the data

Index visibility is eventually consistent, so force the refresh instead of waiting for it. In production, ai_embed can produce the vectors inline at write time.

sql
INSERT INTO items VALUES
    (1, 'trail running shoe',  [1.0,  0.0,  0.0]::FLOAT[3]),
    (2, 'road running shoe',   [0.9,  0.1,  0.0]::FLOAT[3]),
    (3, 'leather dress shoe',  [0.0,  1.0,  0.0]::FLOAT[3]),
    (4, 'wireless earbuds',    [0.0,  0.0,  1.0]::FLOAT[3]),
    (5, 'marathon racer',      [0.95, 0.05, 0.0]::FLOAT[3]);

VACUUM (REFRESH_TABLE) items;
Step 3: Insert the data.
04

Filtered ANN — keyword plus vector

Shoes only, ordered by distance. Returns the two running shoes — marathon racer is closer in vector space but has no matching term, so the filter excludes it.

sql
SELECT id, name FROM items_idx
WHERE  name @@ 'shoe'
ORDER  BY emb <-> [1.0, 0.0, 0.0]::FLOAT[3], id
LIMIT  2;
Step 4: Filtered ANN — keyword plus vector.
05

RRF — fuse the two rankings

Now nothing is excluded: both lists rank independently and the fused score decides. marathon racer comes back — third, behind the two documents that scored on both signals.

sql
WITH fused AS (
    SELECT id, RANK() OVER (ORDER BY s DESC) AS rank FROM (
        SELECT id, BM25(items_idx.tableoid) AS s
        FROM   items_idx WHERE name @@ 'running'
        ORDER  BY s DESC LIMIT 100) lex
    UNION ALL
    SELECT id, RANK() OVER (ORDER BY dist) AS rank FROM (
        SELECT id, emb <-> [1.0,0.0,0.0]::FLOAT[3] AS dist
        FROM   items_idx
        ORDER  BY dist LIMIT 100) vec
)
SELECT id FROM fused GROUP BY id
ORDER  BY SUM(1.0 / (60 + rank)) DESC LIMIT 4;
Step 5: RRF — fuse the two rankings.
> use cases

Use Cases for Hybrid Search

E-commerce product search

A shopper types red running shoes. Keyword catches the exact attributes — colour, category, brand — and vector fills in the semantically similar products that use different words. One SQL statement, no external search engine, and the sales analytics that read the same table stay in the same database.

RAG pipelines for LLM apps

Documents are split into chunks, each holding its text and its embedding. Hybrid search surfaces the chunks that match on keywords and on meaning, which is what raises the quality of the context window. The langchain-serenedb package implements a VectorStore with hybrid search, and RAGFlow can use SereneDB as its doc store.

Log and observability search

Index the message text for full-text and an embedding for "find me errors that look like this one". Filtered ANN is the natural shape: an exact predicate on severity and service, then semantic ranking on the body. Works over a view, so indexing external data — Parquet in S3 — needs no ingest.

> faq

Frequently Asked Questions

Running keyword search (BM25) and vector search (ANN) over the same corpus and combining their results. Keyword brings precision on exact terms, vector brings recall on meaning, and together they cover queries that either one alone would miss.

One inverted index, built on IResearch, covers the text column and the vector column together. As a hybrid search database it combines both signals inside a single SQL query — see the hybrid search documentation for Filtered ANN and RRF.

A way to merge two ranked lists by summing 1/(k + rank) per document. It uses positions rather than scores, so BM25 relevance and vector distance never have to be calibrated onto a common scale.

No. SereneDB keeps the text and the embeddings in one index, so a hybrid search engine here needs no second datastore and no sync pipeline keeping two copies of the corpus consistent with each other.

Yes. ai_embed() calls OpenAI, Gemini, Ollama or any OpenAI-compatible endpoint straight from SQL, at write time or inline in ORDER BY. No external ETL step.

Yes. The langchain-serenedb package implements a VectorStore with hybrid search support, including HybridSearchConfig for choosing and tuning the fusion strategy.

Filtered ANN, RRF, normalized scores and weighted sum — all expressed in standard SQL rather than a config key. Which one fits depends on the workload; the hybrid search documentation writes each one out.

> get started

Get Started with SereneDB

One index, one query, five statements from empty table to fused results. The docs carry all four fusion strategies.