# Semantic search

Learn how to search by meaning rather than exact keywords.

Semantic search interprets the meaning behind user queries rather than exact [keywords](/docs/guides/ai/keyword-search). It uses machine learning to capture the intent and context behind the query, handling language nuances like synonyms, phrasing variations, and word relationships.

## When to use semantic search

Semantic search is useful in applications where the depth of understanding and context is important for delivering relevant results. A good example is in customer support or knowledge base search engines. Users often phrase their problems or questions in various ways, and a traditional keyword-based search might not always retrieve the most helpful documents. With semantic search, the system can understand the meaning behind the queries and match them with relevant solutions or articles, even if the exact wording differs.

For instance, a user searching for "increase text size on display" might miss articles titled "How to adjust font size in settings" in a keyword-based search system. However, a semantic search engine would understand the intent behind the query and correctly match it to relevant articles, regardless of the specific terminology used.

It's also possible to combine semantic search with keyword search to get the best of both worlds. See [Hybrid search](/docs/guides/ai/hybrid-search) for more details.

## How semantic search works

Semantic search uses an intermediate representation called an “embedding vector” to link database records with search queries. A vector, in the context of semantic search, is a list of numerical values. They represent various features of the text and allow for the semantic comparison between different pieces of text.

The best way to think of embeddings is by plotting them on a graph, where each embedding is a single point whose coordinates are the numerical values within its vector. Importantly, embeddings are plotted such that similar concepts are positioned close together while dissimilar concepts are far apart. For more details, see [What are embeddings?](/docs/guides/ai/concepts#what-are-embeddings)

Embeddings are generated using a language model, and embeddings are compared to each other using a similarity metric. The language model is trained to understand the semantics of language, including syntax, context, and the relationships between words. It generates embeddings for both the content in the database and the search queries. Then the similarity metric, often a function like cosine similarity or dot product, is used to compare the query embeddings with the document embeddings (in other words, to measure how close they are to each other on the graph). The documents with embeddings most similar to the query's are deemed the most relevant and are returned as search results.

## Embedding models

There are many embedding models available today. Supabase Edge Functions has [built in support](/docs/guides/functions/examples/semantic-search) for the `gte-small` model. Others can be accessed through third-party APIs like [OpenAI](https://platform.openai.com/docs/guides/embeddings), where you send your text in the request and receive an embedding vector in the response. Others can run locally on your own compute, such as through Transformers.js for JavaScript implementations. For more information on local implementation, see [Generate embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings).

It's crucial to remember that when using embedding models with semantic search, you must use the same model for all embedding comparisons. Comparing embeddings created by different models will yield meaningless results.

## Semantic search in Postgres

To implement semantic search in Postgres we use `pgvector` - an extension that allows for efficient storage and retrieval of high-dimensional vectors. These vectors are numerical representations of text (or other types of data) generated by embedding models.

1. Enable the `pgvector` extension by running:

```sql
create extension vector
with
  schema extensions;
```

1. Create a table to store the embeddings:

```sql
create table documents (
  id bigint primary key generated always as identity,
  content text,
  embedding extensions.vector(512)
);
```

Or if you have an existing table, you can add a vector column like so:

```sql
alter table documents
add column embedding extensions.vector(512);
```

In this example, we create a column named `embedding` which uses the newly enabled `vector` data type. The size of the vector (as indicated in parentheses) represents the number of dimensions in the embedding. Here we use 512, but adjust this to match the number of dimensions produced by your embedding model.

For more details on vector columns, including how to generate embeddings and store them, see [Vector columns](/docs/guides/ai/vector-columns).

### Similarity metric

`pgvector` support 3 operators for computing distance between embeddings:

| **Operator** | **Description**        |
| ------------ | ---------------------- |
| `<->`        | Euclidean distance     |
| `<#>`        | negative inner product |
| `<=>`        | cosine distance        |

These operators are used directly in your SQL query to retrieve records that are most similar to the user's search query. Choosing the right operator depends on your needs. Inner product (also known as dot product) tends to be the fastest if your vectors are normalized.

The easiest way to perform semantic search in Postgres is by creating a function:

```sql
-- Match documents using cosine distance (<=>)
create or replace function match_documents (
  query_embedding extensions.vector(512),
  match_threshold float,
  match_count int
)
returns setof documents
language sql
as $$
  select *
  from documents
  where documents.embedding <=> query_embedding < 1 - match_threshold
  order by documents.embedding <=> query_embedding asc
  limit least(match_count, 200);
$$;
```

Here we create a function `match_documents` that accepts three parameters:

1. `query_embedding`: a one-time embedding generated for the user's search query. Here we set the size to 512, but adjust this to match the number of dimensions produced by your embedding model.
1. `match_threshold`: the minimum similarity between embeddings. This is a value between 1 and -1, where 1 is most similar and -1 is most dissimilar.
1. `match_count`: the maximum number of results to return. Note the query may return less than this number if `match_threshold` resulted in a small shortlist. Limited to 200 records to avoid unintentionally overloading your database.

In this example, we return a `setof documents` and refer to `documents` throughout the query. Adjust this to use the relevant tables in your application.

You'll notice we are using the cosine distance (`<=>`) operator in our query. Cosine distance is a safe default when you don't know whether or not your embeddings are normalized. If you know for a fact that they are normalized (for example, your embedding is returned from OpenAI), you can use negative inner product (`<#>`) for better performance:

```sql
-- Match documents using negative inner product (<#>)
create or replace function match_documents (
  query_embedding extensions.vector(512),
  match_threshold float,
  match_count int
)
returns setof documents
language sql
as $$
  select *
  from documents
  where documents.embedding <#> query_embedding < -match_threshold
  order by documents.embedding <#> query_embedding asc
  limit least(match_count, 200);
$$;
```

Note that since `<#>` is negative, we negate `match_threshold` accordingly in the `where` clause. For more information on the different operators, see the [pgvector docs](https://github.com/pgvector/pgvector?tab=readme-ov-file#vector-operators).

### Calling from your application

Finally you can execute this function from your application. If you are using a Supabase client library such as [`supabase-js`](https://github.com/supabase/supabase-js), you can invoke it using the `rpc()` method:

```tsx
const { data: documents } = await supabase.rpc('match_documents', {
  query_embedding: embedding, // pass the query embedding
  match_threshold: 0.78, // choose an appropriate threshold for your data
  match_count: 10, // choose the number of matches
})
```

You can also call this method directly from SQL:

```sql
select *
from match_documents(
  '[...]'::extensions.vector(512), -- pass the query embedding
  0.78, -- chose an appropriate threshold for your data
  10 -- choose the number of matches
);
```

In this scenario, you'll likely use a Postgres client library to establish a direct connection from your application to the database. It's best practice to parameterize your arguments before executing the query.

### Filtering vector search by metadata

In real applications you usually want to combine the similarity search with a filter on another column, for example only matching documents in a given category, owned by a specific user, or carrying matching metadata in a `jsonb` column. The recommended pattern is to push the filter into the SQL function so the planner can combine it with the vector predicate. Chaining `.eq()` after `rpc()` is applied by PostgREST as an outer filter on the function's result, _after_ the function has already executed its similarity ranking and `limit` — so the vector planner can't use it, and selective filters can leave you with fewer than `match_count` rows.

Assuming you've added a `category text` column to `documents`, you can extend the cosine variant of `match_documents` with a typed filter parameter:

```sql
-- Match documents in a given category using cosine distance (<=>)
create or replace function match_documents (
  query_embedding extensions.vector(512),
  match_threshold float,
  match_count int,
  filter_category text
)
returns setof documents
language sql
as $$
  select *
  from documents
  where documents.category = filter_category
    and documents.embedding <=> query_embedding < 1 - match_threshold
  order by documents.embedding <=> query_embedding asc
  limit least(match_count, 200);
$$;
```

If you store side data in a `jsonb metadata` column instead of dedicated columns, the same pattern works with the `@>` containment operator:

```sql
where documents.metadata @> filter_metadata
  and documents.embedding <=> query_embedding < 1 - match_threshold
```

Call the filtered function from your application by passing the extra parameter:

```tsx
const { data: documents } = await supabase.rpc('match_documents', {
  query_embedding: embedding,
  match_threshold: 0.78,
  match_count: 10,
  filter_category: 'blog',
})
```

When the filter is selective enough that the post-filter result set falls below `match_count`, an HNSW index can return fewer rows than expected. From `pgvector` 0.8.0, the planner supports iterative index scans that re-enter the index to gather more candidates. See [Filtering with HNSW indexes](/docs/guides/ai/vector-indexes/hnsw-indexes#filtering-with-hnsw-indexes) for details.

## Next steps

As your database scales, you will need an index on your vector columns to maintain fast query speeds. See [Vector indexes](/docs/guides/ai/vector-indexes) for an in-depth guide on the different types of indexes and how they work.

For larger datasets, choosing and tuning the right index is critical for maintaining fast and accurate semantic search.

## pgvector index tuning

When working with embedding datasets at scale (100k+ rows), index selection and tuning can significantly impact query latency and accuracy.

Supabase uses Postgres with the `pgvector` extension, which supports two primary index types for vector similarity search: IVFFlat and HNSW.

### IVFFlat index

**Best for:**

- Large datasets (100k-10M rows)
- Fast approximate search
- Lower memory usage

```sql
create index on documents
using ivfflat (embedding vector_cosine_ops)
with (lists = 100);
```

### HNSW index

**Best for:**

- High accuracy requirements
- Read-heavy workloads
- Low-latency semantic search
- Scenarios where recall is more important than memory usage

```sql
create index on documents
using hnsw (embedding vector_cosine_ops);
```

## See also

- [Embedding concepts](/docs/guides/ai/concepts)
- [Vector columns](/docs/guides/ai/vector-columns)
- [Vector indexes](/docs/guides/ai/vector-indexes)
- [Hybrid search](/docs/guides/ai/hybrid-search)
- [Keyword search](/docs/guides/ai/keyword-search)