Neon is expanding into a backend: Object Storage, Functions, and AI Gateway now in beta
/Postgres/Extensions/lakebase_vector

The lakebase_vector extension

Fast approximate nearest-neighbor vector search for Neon Postgres

The lakebase_vector extension adds the lakebase_ann index type to Postgres for approximate nearest-neighbor (ANN) vector search. It is a drop-in companion to pgvector: the same vector types, distance operators, and query syntax work unchanged; only the index type changes.

See Lakebase Search for the architecture and the companion lakebase_text extension.

Why lakebase_vector?

lakebase_ann uses IVF (Inverted File) partitioning combined with RaBitQ quantization, an architecture built to scale beyond what HNSW can reach. HNSW indexes must fit entirely in memory and traverse the graph with random I/O at query time, which limits how far they can scale. IVF partitions the vector space into lists and searches only the most relevant ones at query time, enabling sequential I/O rather than random pointer-chasing. RaBitQ compresses vectors 4–8x, reducing the index size and enabling index builds 50–100x faster than HNSW. Together, this scales to over 1 billion vectors on a single index while keeping cold starts fast and query performance stable.

There is no migration involved. lakebase_vector inherits all pgvector data types and operators. You can create a lakebase_ann index on your existing pgvector columns without changing your schema or application code.

Enable the lakebase_vector extension

Lakebase Search must be enabled on your Neon project before you can install this extension. Once it's enabled, run the following statement in the Neon SQL Editor or from a client such as psql:

CREATE EXTENSION IF NOT EXISTS lakebase_vector CASCADE;

lakebase_vector requires Postgres 16 or later. The CASCADE option automatically installs pgvector if it is not already installed, since lakebase_vector depends on it.

Quick start

Create a table with a vector column and insert some data:

CREATE TABLE items (id bigserial PRIMARY KEY, embedding vector(3));

INSERT INTO items (embedding)
SELECT ARRAY[random(), random(), random()]::real[]
FROM generate_series(1, 1000);

Create a lakebase_ann index on the embedding column:

CREATE INDEX ON items USING lakebase_ann (embedding vector_l2_ops);

Query using the standard pgvector syntax:

SELECT * FROM items ORDER BY embedding <-> '[3,1,2]' LIMIT 5;

Index tuning

Set build_mode at index creation to control the accuracy/speed tradeoff:

  • standard (default): optimizes for recall. Use for most workloads.
  • fast: builds faster at lower recall. Use when build time matters more than search quality.
CREATE INDEX ON items USING lakebase_ann (embedding vector_l2_ops) WITH (build_mode = 'fast');

Before tuning search, call lakebase_ann_index_info(index_name) to get the index's lists, default_probes, and default_epsilon values.

note

The probes and epsilon GUCs apply only once the index has built IVF lists, which happens above a corpus-size threshold. On a small dataset, lakebase_ann uses exact (flat) search instead: lakebase_ann_index_info returns empty lists and default_probes, SET lakebase_ann.probes fails with usage: need 0 probes, but N provided, and epsilon has no effect. This is expected, since the index is already returning exact results, so there is nothing to tune. These GUCs become relevant as your data grows and the index switches to IVF partitioning.

Use the lakebase_ann.probes GUC to control how many IVF partitions are searched at query time. Higher values improve recall at the cost of speed.

SET lakebase_ann.probes TO '10';
SELECT * FROM items ORDER BY embedding <-> '[3,1,2]' LIMIT 10;

lakebase_ann.epsilon controls the re-ranking margin. The default value of 1.9 works well for most workloads.

SET lakebase_ann.epsilon TO '1.5';

When you set these GUCs from application code, the SET and the query must run on the same session. With a connection pool or the Neon serverless driver, where each statement can use a different connection, issue both in a single transaction so the SET applies to the query.

Concurrent index updates

For large, frequently changing datasets, use CREATE INDEX CONCURRENTLY to build or rebuild an index without blocking reads and writes:

CREATE INDEX CONCURRENTLY items_embedding_ann ON items
  USING lakebase_ann (embedding vector_l2_ops);

REINDEX INDEX CONCURRENTLY items_embedding_ann;

Reference

Operator classes

lakebase_ann supports the following operator classes. Each class provides two operators:

  • A pgvector distance operator (<->, <#>, <=>) that returns a distance and is used in ORDER BY for nearest-neighbor search.
  • A lakebase_vector range operator (<<->>, <<#>>, <<=>>) that takes a sphere_* value on its right side and returns a boolean: true when the vector falls within the sphere's radius. Use it in a WHERE clause to filter by similarity. Build the sphere with the sphere(vector, radius) function.
Operator classDistance operator (ORDER BY)Range operator (WHERE)
vector_l2_ops<->(vector, vector)<<->>(vector, sphere_vector)
vector_ip_ops<#>(vector, vector)<<#>>(vector, sphere_vector)
vector_cosine_ops<=>(vector, vector)<<=>>(vector, sphere_vector)
halfvec_l2_ops<->(halfvec, halfvec)<<->>(halfvec, sphere_halfvec)
halfvec_ip_ops<#>(halfvec, halfvec)<<#>>(halfvec, sphere_halfvec)
halfvec_cosine_ops<=>(halfvec, halfvec)<<=>>(halfvec, sphere_halfvec)
rabitq8_l2_ops<->(rabitq8, rabitq8)<<->>(rabitq8, sphere_rabitq8)
rabitq8_ip_ops<#>(rabitq8, rabitq8)<<#>>(rabitq8, sphere_rabitq8)
rabitq8_cosine_ops<=>(rabitq8, rabitq8)<<=>>(rabitq8, sphere_rabitq8)
rabitq4_l2_ops<->(rabitq4, rabitq4)<<->>(rabitq4, sphere_rabitq4)
rabitq4_ip_ops<#>(rabitq4, rabitq4)<<#>>(rabitq4, sphere_rabitq4)
rabitq4_cosine_ops<=>(rabitq4, rabitq4)<<=>>(rabitq4, sphere_rabitq4)

To filter by similarity, wrap the query vector in sphere(vector, radius) and use the range operator in a WHERE clause. Rank the matches with the corresponding distance operator:

-- Rows within cosine radius 0.5 of the query vector, closest first
SELECT * FROM items
WHERE embedding <<=>> sphere('[3,1,2]'::vector, 0.5)
ORDER BY embedding <=> '[3,1,2]'
LIMIT 5;

The range operator returns a boolean, so it belongs in WHERE, not ORDER BY. Use the distance operator (<=> here) to order results.

The rabitq8 and rabitq4 types are quantization types defined by lakebase_vector. They offer reduced memory footprint at the cost of some precision.

Pick the operator class that matches how your embeddings were trained, and use the same metric for the index and your queries:

  • Cosine (vector_cosine_ops, <=>) suits most text embeddings and is the common default.
  • L2 / Euclidean (vector_l2_ops, <->) fits cases where absolute distance matters and vectors aren't normalized.
  • Inner product (vector_ip_ops, <#>) is for vectors pre-normalized to unit length; for unit vectors it matches cosine and is typically faster.

The halfvec, rabitq8, and rabitq4 families provide the same three metrics with smaller, quantized storage.

Index options

OptionTypeDefaultDescription
build_modestring'standard'Controls the accuracy/speed tradeoff at index build time. 'standard' optimizes for recall; 'fast' builds faster with lower recall.

Search parameters

GUCTypeDefaultDescription
lakebase_ann.probesintegernot setNumber of IVF partitions to scan at query time. Higher values improve recall at the cost of query speed.
lakebase_ann.epsilonfloat1.9Re-ranking margin. Valid range: 0.0 to 4.0.

Need help?

Join our Discord Server to ask questions or see what others are doing with Neon. For paid plan support options, see Support.

Was this page helpful?
Edit on GitHub