# Hybrid search

Compare lexical full-text search with vector similarity on the same dataset and fuse rankings with search::rrf and related helpers.

## Vector search vs full-text search

SurrealDB supports [full-text search](/docs/learn/data-models/full-text-search/overview.md) and Vector Search. Full-text search (FTS) involves indexing documents using an [FTS index](/docs/reference/query-language/statements/define/indexes.md#full-text-search-fulltext-index) that makes use of an [analyzer](/docs/reference/query-language/statements/define/analyzer.md) that breaks down text using [tokenizers](/docs/reference/query-language/statements/define/analyzer.md#tokenizers) and [filters](/docs/reference/query-language/statements/define/analyzer.md#filters).

<img src="~/assets/img/lead.png" alt="Google search for the word 'lead'" />

The image above is a Google search for the word “lead”, a word with more than one definition (and pronunciation!). Lead can mean 'taking initiative', as well as the chemical element with the symbol 'Pb'.

Let's consider this in the context of a database of liquid samples which note down harmful chemicals that are found in them.

In the example below, we have a table called `liquids` with a `sample` field and a `content` field.  Next, we can define a [full-text index](/docs/reference/query-language/statements/define/indexes.md#full-text-search-fulltext-index) on the `content` field by first defining an analyzer called `liquid_analyzer`. We can then [define an index](/docs/reference/query-language/statements/define/indexes.md) on the content field in the liquid table and set our [custom analyzer](/docs/reference/query-language/statements/define/analyzer.md) (`liquid_analyzer`)to search through the index.

Then, using the select statement to retrieve all the samples containing the chemical lead will also bring up samples that mention the word `lead`.

```surql
-- Insert a sample & content field into a liquids table

INSERT INTO liquids [
    {sample:'Sea water', content: 'The sea water contains some amount of lead'},
    {sample:'Tap water', content: 'The team lead by Dr. Rose found out that the tap water in was potable'},
    {sample:'Sewage water', content: 'High amounts of a were found in Sewage water'}
];

-- Define an analyzer for the liquid table and an index on the content field with the analyzer

DEFINE ANALYZER liquid_analyzer TOKENIZERS blank,class,camel,punct FILTERS snowball(english);
DEFINE INDEX liquid_content ON liquids FIELDS content SEARCH ANALYZER liquid_analyzer BM25 HIGHLIGHTS;

-- Retrieve all the samples containing the chemical lead will also bring up samples that simply mention the word lead

SELECT
  sample,
  content
FROM liquids
WHERE content @0@ 'lead';
```

[Run this example in SurrealDB Studio](https://app.surrealdb.com/mini?query=--+Insert+a+sample+%26+content+field+into+a+liquids+table%0A%0AINSERT+INTO+liquids+%5B%0A++++%7Bsample%3A%27Sea+water%27%2C+content%3A+%27The+sea+water+contains+some+amount+of+lead%27%7D%2C%0A++++%7Bsample%3A%27Tap+water%27%2C+content%3A+%27The+team+lead+by+Dr.+Rose+found+out+that+the+tap+water+in+was+potable%27%7D%2C%0A++++%7Bsample%3A%27Sewage+water%27%2C+content%3A+%27High+amounts+of+a+were+found+in+Sewage+water%27%7D%0A%5D%3B%0A%0A--+Define+an+analyzer+for+the+liquid+table+and+an+index+on+the+content+field+with+the+analyzer%0A%0ADEFINE+ANALYZER+liquid_analyzer+TOKENIZERS+blank%2Cclass%2Ccamel%2Cpunct+FILTERS+snowball%28english%29%3B%0ADEFINE+INDEX+liquid_content+ON+liquids+FIELDS+content+SEARCH+ANALYZER+liquid_analyzer+BM25+HIGHLIGHTS%3B%0A%0A--+Retrieve+all+the+samples+containing+the+chemical+lead+will+also+bring+up+samples+that+simply+mention+the+word+lead%0A%0ASELECT%0A++sample%2C%0A++content%0AFROM+liquids%0AWHERE+content+%400%40+%27lead%27%3B&orientation=horizontal)

If you read through the content of the tap water sample, you’ll notice that it does not contain any lead in it but it has the mention of the word `lead` under “The team lead by Dr. Rose…” which means that the team was guided by Dr. Rose.

The search pulled up both the records although the tap water sample had no lead in it. This example shows us that while full-text search does a great job at matching query terms with indexed documents, on its own it may not be the best solution for use cases where the query terms have deeper context and scope for ambiguity.

For vector-side retrieval on the same story, see [Similarity search](/docs/learn/data-models/vector-search/similarity-search.md).

## Hybrid search functions

As mentioned above, full-text search and vector search can both be used in SurrealDB. In addition, some functions exist inside the [`search::`](/docs/reference/query-language/functions/database-functions/search.md) namespace that take both full-text and vector arguments in order to produce a single unified output.

Here is an example of one of them called [`search::rrf()`](/docs/reference/query-language/functions/database-functions/search.md#searchrrf) which does this using an algorithm called reciprocal rank fusion.

```surql
-- Sample data --
CREATE test:1 SET text = "Graph databases are great.", embedding = [0.10, 0.20, 0.30];
CREATE test:2 SET text = "Relational databases store tables.", embedding = [0.05, 0.10, 0.00];
CREATE test:3 SET text = "This document mentions graphs.", embedding = [0.20, 0.10, 0.25];

-- Analyzer used by the full‑text index
DEFINE ANALYZER simple TOKENIZERS class, punct FILTERS lowercase, ascii;

-- Full‑text index
DEFINE INDEX idx_text
  ON TABLE test FIELDS text FULLTEXT ANALYZER simple BM25;
```

**HNSW (in-memory)**

```surql
DEFINE INDEX idx_embedding
    ON TABLE test 
    FIELDS embedding 
    HNSW DIMENSION 3 DIST COSINE;
```

**DISKANN (on-disk)**

_(since v3.1.0)_

```surql
DEFINE INDEX idx_embedding
    ON TABLE test 
    FIELDS embedding 
    DISKANN DIMENSION 3 DIST COSINE TYPE F32;
```

For very large embedding sets that do not fit comfortably in RAM, prefer DISKANN. It is **not available on WASM** builds.

```surql
-- Query vector (whatever your embedding model produced for "graph databases")
LET $qvec = [0.12, 0.18, 0.27];

-- Vector search: top 2 nearest neighbours
LET $vs = SELECT id FROM test  WHERE embedding <|2,100|> $qvec;

-- Full‑text search: top 2 lexical matches
LET $ft = SELECT id, search::score(1) as score FROM test
          WHERE text @1@ 'graph' ORDER BY score DESC LIMIT 2;

-- Fuse with Reciprocal Rank Fusion (k defaults to 60 if omitted)
search::rrf([$vs, $ft], 2, 60);
```
