Skip to content

This is the multi-page printable view of this section. .

Return to the regular view of this page.

pgvecto.rs Documentation

Complete documentation archive for pgvecto.rs (vectors extension), predecessor to vchord.
Warning

pgvecto.rs has been succeeded by vchord. For new deployments, use vchord; for existing pgvecto.rs databases, follow the Migration Guide.

1 - Getting Started

Introduction and installation guide for pgvecto.rs.

1.1 - Overview

An introduction to the pgvecto.rs.
Warning

We strongly suggest you explore our new VectorChord implementation, offering enhanced stability and performance. Users are advised to transition to VectorChord. Refer to the migration guide for assistance.

An introduction to the pgvecto.rs.

What is pgvecto.rs

pgvecto.rs is a Postgres extension that provides vector similarity search functions. It is written in Rust and based on pgrx. It is currently in the beta status, we invite you to try it out in production and provide us with feedback. Read more at 📝our launch blog.

Comparison with pgvector

Checkout pgvecto.rs vs pgvector for more details.

Feature pgvecto.rs pgvector
Filtering Introduces VBASE method for vector search and relational query (e.g. Single-Vector TopK + Filter + Join). When filters are applied, the results may be incomplete. For example, if you originally intended to limit the results to 10, you might end up with only 5 results with filters.
Vector Dimensions Supports up to 65535 dimensions. Supports up to 2000 dimensions.
SIMD SIMD instructions are dynamically dispatched at runtime to maximize performance based on the capabilities of the specific machine. Added CPU dispatching for distance functions on Linux x86-64" in 0.7.0.
Data Types Introduces additional data types: binary vectors, FP16 (16-bit floating point), and INT8 (8-bit integer). -
Indexing Handles the storage and memory of indexes separately from PostgreSQL Relies on the native storage engine of PostgreSQL
WAL Support Provides Write-Ahead Logging (WAL) support for data, index support is working in progress. Provides Write-Ahead Logging (WAL) support for index and data.

Quick start

For new users, we recommend using the Docker image to get started quickly.

docker run \
  --name pgvecto-rs-demo \
  -e POSTGRES_PASSWORD=mysecretpassword \
  -p 5432:5432 \
  -d tensorchord/pgvecto-rs:pg16-v0.2.0

Then you can connect to the database using the psql command line tool. The default username is postgres, and the default password is mysecretpassword.

psql postgresql://postgres:mysecretpassword@localhost:5432/postgres

Run the following SQL to ensure the extension is enabled.

DROP EXTENSION IF EXISTS vectors;
CREATE EXTENSION vectors;

pgvecto.rs introduces a new data type vector(n) denoting an n-dimensional vector. The n within the brackets signifies the dimensions of the vector.

You could create a table with the following SQL.

-- create table with a vector column

CREATE TABLE items (
  id bigserial PRIMARY KEY,
  embedding vector(3) NOT NULL -- 3 dimensions
);
Details

vector(n) is a valid data type only if 1≤n≤655351 \leq n \leq 65535. Due to limits of PostgreSQL, it’s possible to create a value of type vector(3) of 55 dimensions and vector is also a valid data type. However, you cannot still put 00 scalar or more than 6553565535 scalars to a vector. If you use vector for a column or there is some values mismatched with dimension denoted by the column, you won’t able to create an index on it.

You can then populate the table with vector data as follows.

-- insert values

INSERT INTO items (embedding)
VALUES ('[1,2,3]'), ('[4,5,6]');

-- or insert values using a casting from array to vector

INSERT INTO items (embedding)
VALUES (ARRAY[1, 2, 3]::real[]), (ARRAY[4, 5, 6]::real[]);

We support three operators to calculate the distance between two vectors.

  • <->: squared Euclidean distance, defined as Σ(xi−yi)2\Sigma (x_i - y_i) ^ 2.
  • <#>: negative dot product, defined as −Σxiyi- \Sigma x_iy_i.
  • <=>: cosine distance, defined as 1−ΣxiyiΣxi2Σyi21 - \frac{\Sigma x_iy_i}{\sqrt{\Sigma x_i^2 \Sigma y_i^2}}.
-- call the distance function through operators

-- squared Euclidean distance
SELECT '[1, 2, 3]'::vector <-> '[3, 2, 1]'::vector;
-- negative dot product
SELECT '[1, 2, 3]'::vector <#> '[3, 2, 1]'::vector;
-- cosine distance
SELECT '[1, 2, 3]'::vector <=> '[3, 2, 1]'::vector;

You can search for a vector simply like this.

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

A simple Question-Answering application

Please check out the Question-Answering application tutorial.

Half-precision floating-point

vecf16 type is the same with vector in anything but the scalar type. It stores 16-bit floating point numbers. If you want to reduce the memory usage to get better performance, you can try to replace vector type with vecf16 type.

For more usage of vecf16, please refer to vector types.

Sparse vector

svector type is a sparse vector type. It stores a vector in a sparse format. It is suitable for vectors with many zeros.

For more usage of svector, please refer to vector types.

Binary vector

bvector type is a binary vector type. It is a fixed-length bit string. Except for above 3 distances, we also support jaccard distance <~>, which defined as 1−∣X∩Y∣∣X∪Y∣1 - \frac{|X\cap Y|}{|X\cup Y|}. And hamming distance is the same with squared Euclidean distance, you can use <-> operator to calculate it. We also provide binarize function to construct a bvector from a vector, which set the positive elements to 1, otherwise 0.

For more usage of bvector, please refer to vector types.

Roadmap 🗂️

Please check out ROADMAP.

Contribute 😊

We welcome all kinds of contributions from the open-source community, individuals, and partners.

Talk with us

💬 Interested in talking with us about your experience building or managing AI/ML applications?

Set up a time to chat!

1.2 - Installation

There are four ways to install pgvecto.rs.
Warning

We strongly encourage you to explore our new VectorChord implementation, which offers improved stability and performance. Users are encouraged to migrate to VectorChord. See the migration guide for assistance.

There are four ways to install pgvecto.rs.

Docker

The easiest way to try pgvecto.rs is to run it from a ready-to use Docker image.

docker run \
  --name pgvecto-rs-demo \
  -e POSTGRES_PASSWORD=mysecretpassword \
  -p 5432:5432 \
  -d tensorchord/pgvecto-rs:pg16-v0.2.0

Then you can connect to the database using the psql command line tool. The default username is postgres, and the default password is mysecretpassword.

psql postgresql://postgres:mysecretpassword@localhost:5432/postgres

Run the following SQL to ensure the extension is enabled.

DROP EXTENSION IF EXISTS vectors;
CREATE EXTENSION vectors;

To achieve full performance, please mount the volume to pg data directory by adding the option like -v $PWD/pgdata:/var/lib/postgresql/data

You can configure PostgreSQL by the reference of the parent image in https://hub.docker.com/_/postgres/.

From Debian package

Tip

Installation from the Debian package requires a dependency on GLIBC >= 2.31, e.g:

  • Ubuntu 20.04 or later
  • Debian Bullseye or later

Debian packages(.deb) are used in distributions based on Debian, such as Ubuntu and many others. They can be easily installed by dpkg or apt-get.

  1. Download the deb package in the release page, and type sudo apt install vectors-pg15-*.deb to install the deb package.

  2. Configure your PostgreSQL by modifying the shared_preload_libraries and search_path to include the extension.

psql -U postgres -c 'ALTER SYSTEM SET shared_preload_libraries = "vectors.so"'
psql -U postgres -c 'ALTER SYSTEM SET search_path TO "$user", public, vectors'
# You need restart the PostgreSQL cluster to take effects.
sudo systemctl restart postgresql.service   # for pgvecto.rs running with systemd
  1. Connect to the database and enable the extension.
DROP EXTENSION IF EXISTS vectors;
CREATE EXTENSION vectors;

From ZIP package

since v0.2.1
Tip

Installation from the ZIP package requires a dependency on GLIBC >= 2.31, e.g:

  • RHEL 9 or later

For systems that are not Debian based and cannot run a Docker container, please follow these steps to install:

  1. Before install, make sure that you have the necessary packages installed, including PostgreSQL, pg_config, unzip, wget.
# Example for RHEL 9 dnf
# Please check your package manager
sudo dnf install -y unzip wget libpq-devel
sudo dnf module install -y postgresql:15/server
sudo postgresql-setup --initdb
sudo systemctl start postgresql.service
sudo systemctl enable postgresql.service
  1. Verify whether $pkglibdir and $shardir have been set by PostgreSQL.
pg_config --pkglibdir
# Print something similar to:
# /usr/lib/postgresql/15/lib or
# /usr/lib64/pgsql

pg_config --sharedir
# Print something similar to:
# /usr/share/postgresql/15 or
# /usr/share/pgsql
  1. Download the zip package in the release page and extract it to a temporary directory.
wget https://github.com/tensorchord/pgvecto.rs/releases/download/v0.2.1/vectors-pg15_x86_64-unknown-linux-gnu_0.2.1.zip -O vectors.zip
unzip vectors.zip -d vectors
  1. Copy the extension files to the PostgreSQL directory.
# Copy library to `$pkglibdir`
sudo cp vectors/vectors.so $(pg_config --pkglibdir)/
# Copy schema to `$shardir`
sudo cp vectors/vectors--* $(pg_config --sharedir)/extension/
sudo cp vectors/vectors.control $(pg_config --sharedir)/extension/
  1. Configure your PostgreSQL by modifying the shared_preload_libraries and search_path to include the extension.
psql -U postgres -c 'ALTER SYSTEM SET shared_preload_libraries = "vectors.so"'
psql -U postgres -c 'ALTER SYSTEM SET search_path TO "$user", public, vectors'
# You need restart the PostgreSQL cluster to take effects.
sudo systemctl restart postgresql.service   # for pgvecto.rs running with systemd
  1. Connect to the database and enable the extension.
DROP EXTENSION IF EXISTS vectors;
CREATE EXTENSION vectors;

From source

Before building from source, you could refer to the development guide to set up the development environment.

  1. Then you could build and install the extension.
sed -e "s/@CARGO_VERSION@/0.0.0/g" ./vectors.control  # 0.0.0 here is the version number that you need
sudo cp -a ./sql/upgrade/. $(pg_config --sharedir)/extension
cargo pgrx install --sudo --release
  1. Configure your PostgreSQL by modifying the shared_preload_libraries and search_path to include the extension.
psql -U postgres -c 'ALTER SYSTEM SET shared_preload_libraries = "vectors.so"'
psql -U postgres -c 'ALTER SYSTEM SET search_path TO "$user", public, vectors'
# You need restart the PostgreSQL cluster to take effects.
sudo systemctl restart postgresql.service   # for pgvecto.rs running with systemd
service postgresql restart  # for pgvecto.rs running in envd
  1. Connect to the database and enable the extension.
DROP EXTENSION IF EXISTS vectors;
CREATE EXTENSION vectors;

2 - Usage

Creating indexes, running similarity searches, monitoring, quantization, and compatibility in pgvecto.rs.

2.1 - Indexing

Indexing is the process of building a data structure that allows for efficient search. pgvecto.rs supports three indexing algorithms: brute force (flat), IVF, a

Indexing is the process of building a data structure that allows for efficient search. pgvecto.rs supports three indexing algorithms: brute force (flat), IVF, and HNSW. The default algorithm is HNSW.

To construct an index for vectors, first create a table named items with a column named embedding of type vector(n). Then, populate the table with generated data.

CREATE TABLE items (embedding vector(3));
INSERT INTO items (embedding) SELECT ARRAY[random(), random(), random()]::real[] FROM generate_series(1, 1000);
Tip

vector is the suitable data type for most situations. pgvecto.rs also supports other specific vector types, including:

You can create a vector index for squared Euclidean distance with the following SQL.

CREATE INDEX ON items USING vectors (embedding vector_l2_ops);

The vector_l2_ops operator class calculates squared Euclidean distance. Refer to the documentation for a list of operator classes.

To switch the indexing algorithm from the default HNSW to IVF, execute the SQL command below. For additional examples of usage, refer to the documentation.

CREATE INDEX ON items USING vectors (embedding vector_l2_ops)
WITH (options = $$
[indexing.ivf]
$$);

You can now perform a KNN search with the following SQL, leveraging the vector index for your query.

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

pgvecto.rs constructs the index asynchronously. When you insert new rows into the table, they will first be placed in an append-only file. The background thread will periodically merge the newly inserted row to the existing index. When a user performs any search prior to the merge process, it scans the append-only file to ensure accuracy and consistency.

Examples

Here are some examples of how to create indexes using various algorithms and settings. For a complete list of options, please consult the reference guide.

-- HNSW algorithm, default settings.

CREATE INDEX ON items USING vectors (embedding vector_l2_ops);

-- Use FP16 vector type for optimizing data storage.

CREATE TABLE items (embedding vecf16(3));
CREATE INDEX ON items USING vectors (embedding vecf16_l2_ops)

--- Or using brute force with product quantization (PQ).

CREATE INDEX ON items USING vectors (embedding vector_l2_ops)
WITH (options = $$
[indexing.flat]
quantization.product.ratio = "x16"
$$);

-- Or using brute force with scalar quantization (SQ).
CREATE INDEX ON items USING vectors (embedding vector_l2_ops)
WITH (options = "[indexing.hnsw.quantization.scalar]");

--- Or using IVFPQ algorithm.

CREATE INDEX ON items USING vectors (embedding vector_l2_ops)
WITH (options = $$
[indexing.ivf]
quantization.product.ratio = "x16"
$$);

-- Use more threads for background building the index.

CREATE INDEX ON items USING vectors (embedding vector_l2_ops)
WITH (options = $$
optimizing.optimizing_threads = 16
$$);

-- Prefer smaller HNSW graph.

CREATE INDEX ON items USING vectors (embedding vector_l2_ops)
WITH (options = $$
segment.max_growing_segment_size = 200000
$$);

Suggestions

Selecting the appropriate indexing algorithm and settings is crucial for vector search performance. Below are recommendations tailored to various use cases.

Brute force (flat)

If you’re conducting a limited number of searches (between 1,000 and 10,000) or require precise results, the brute force algorithm is a viable option.

This algorithm sets the standard for other indexing methods. It retains vectors in their original form without compression and avoids additional overhead. While straightforward and user-friendly, it’s not ideal for processing large datasets.

In addition, you can use quantization to reduce memory usage. For a complete list of all options, please refer to the documentation. Detailed information about quantization can also be found there.

Inverted file index (IVF)

IVF mainly uses the idea of inverted indexing to store the vectors (id, vector) under each cluster center. When querying a vector, it finds the nearest several centers and searches for the vectors under these centers respectively.

Before you begin your search, it’s necessary to train nlist cluster centers. You also have the option to set nsample for K-Means clustering; a higher value yields more precise clusters but increases training time. For additional settings, please consult the reference guide.

Before searching, train nlist cluster centers using K-Means clustering with nsample points. A higher value yields more precise clustering but requires longer training time. For additional settings, please consult the reference guide.

Hierarchical navigable small world graph (HNSW)

HNSW merges the concepts of skip list with navigable small world (NSW) graphs, employing a hierarchical structure that features longer edges on higher layers for rapid searching and shorter edges on lower layers to enhance search precision.

Navigable small world

Navigable Small World operate on the premise that each node is accessible from any other through a minimal number of “hops.” Once an entry point is chosen—be it random, heuristic-based, or algorithmically determined—the surrounding nodes are examined to identify if they’re nearer than the current one. The search progresses to the nearest neighbor and continues until no closer nodes can be found.

When constructing the graph, the m parameter determines how many connections a new node will establish with its nearest neighbors. A higher m value results in a denser and more interconnected graph but also increases memory usage and slows down insertion times. During node insertion, the algorithm identifies the closest m nodes to create two-way links with them.

The search process navigates through an NSW graph, beginning at a pre-defined entry point. The algorithm proceeds by greedily moving to adjacent vertices closer to the query vector. The process stops once it finds no closer neighbors in the current vertex’s adjacency list.

A limitation of the NSW search is that it opts for the seemingly shortest path to the nearest node, disregarding the graph’s overall structure. This “greedy search” approach can result in getting stuck at a local optimum, an issue often referred to as “early stopping.”

Skip list

Skip lists are constructed using a probabilistic approach, where nodes become less likely to appear as you move up the layers, resulting in fewer nodes at higher levels. To search for a node, the algorithm starts at the top layer’s “head” and moves to the next node that is greater than or equal to the target. If the node isn’t found, it descends to a lower (more fine-grained) layer and continues this pattern until it locates either the desired node or its nearest neighbors.

HNSW

Constructing this multi-layered graph begins with a foundational layer that encapsulates the entire dataset. Moving up the hierarchy, each successive layer provides a streamlined summary of its predecessor, featuring fewer nodes and functioning as fast tracks for making broader leaps within the graph.

Smaller m) values are better for lower-dimensional data or when you require lower recall. Larger m values are useful for higher-dimensional data or when high recall is important.

The ef_construction parameter determines the dynamic candidate list size when adding new nodes; increasing this value may enhance recall but could extend index construction time.

The HNSW index is resource-intensive, requiring additional RAM and an adjustment to the maintenance_work_mem setting for larger datasets. If you’re seeking performance that’s markedly faster than IVF, with a high recall rate and scalability that matches dataset size, HNSW is an excellent option.

2.2 - Search

Get the nearest 5 neighbors to a vector

Get the nearest 5 neighbors to a vector

SET vectors.hnsw_ef_search = 64;
SELECT * FROM items ORDER BY embedding <-> '[3,2,1]' LIMIT 5;

Operators

These operators are used for distance metrics:

Name Description
<-> squared Euclidean distance
<#> negative dot product
<=> cosine distance

For their definitions, see overview.

Filter

For a given category, get the nearest 10 neighbors to a vector

SELECT 1 FROM items WHERE category_id = 1 ORDER BY embedding <#> '[0.5,0.5,0.5]' limit 10

Query options

Set ivf scan lists to 1 in session:

SET vectors.ivf_nprobe=1;

Set hnsw search scope to 40 in transaction:

SET LOCAL vectors.hnsw_ef_search=40;

For all options, refer to search options.

Search modes

There are two search modes: vbase and basic.

vbase

As the default search mode, vbase is suitable for most scenarios.

For how it works, refer to the thesis VBASE: Unifying Online Vector Similarity Search and Relational Queries via Relaxed Monotonicity.

basic

basic is behaviorally consistent with vector algorithm libraries such as faiss. It will be useful if you want to align them.

Enabling basic, you must respect these restrictions:

  • Execute VACUUM after updates before searches.
  • Search without filter

2.3 - Monitoring

We provide a view pgvectorindexstat to monitor the progress of indexing.

We provide a view pg_vector_index_stat to monitor the progress of indexing.

Column Type Description
tablerelid oid The oid of the table.
indexrelid oid The oid of the index.
tablename name The name of the table.
indexname name The name of the index.
idx_status text Its value is NORMAL or UPGRADE. Whether this index is normal or needs upgrade.
idx_indexing bool Not null if idx_status is NORMAL. Whether the background thread is indexing.
idx_tuples int8 Not null if idx_status is NORMAL. The number of tuples.
idx_sealed int8[] Not null if idx_status is NORMAL. The number of tuples in each sealed segment.
idx_growing int8[] Not null if idx_status is NORMAL. The number of tuples in each growing segment.
idx_write int8 Not null if idx_status is NORMAL. The number of tuples in write buffer.
idx_size int8 Not null if idx_status is NORMAL. The byte size for all the segments.
idx_options text Not null if idx_status is NORMAL. The configuration of the index.
idx_options Details

The idx_options is a JSON string that includes vector, segment, optimization, and indexing options. Examples below:

{
    "vector": {
        "dimensions": 256,
        "distance": "L2",
        "kind": "F32"
    },
    "segment": {
        "max_growing_segment_size": 20000,
        "max_sealed_segment_size": 1000000
    },
    "optimizing": {
        "sealing_secs": 60,
        "sealing_size": 1,
        "delete_threshold": 0.2,
        "optimizing_threads": 2
    },
    "indexing": {
        "hnsw": {
            "m": 12,
            "ef_construction": 300,
            "quantization": {
                "trivial": {}
            }
        }
    }
}

If there are no more insertions and you wait for all indexing to be finished, you can check the field idx_indexing every 60 seconds util it’s true.

2.4 - Quantization

Quantization is a technique to reduce the memory consumption and computation complexity with the affordable decrease in precision. In the context of vector sear

Quantization is a technique to reduce the memory consumption and computation complexity with the affordable decrease in precision. In the context of vector search, there are mainly two types of quantization: production quantization and scalar quantization.

Production Quantization

Production quantization is a method that trains several clustering models on the dataset and maps each vector to a list of cluster center indexes. It is commonly used address the problem of excessive memory usage with high-dimensional data.

In pgvecto.rs, you can enable the product quantization with:

CREATE INDEX ON items USING vectors (embedding vector_l2_ops)
WITH (options = $$
[indexing.ivf]
quantization.product.ratio = "x16"
$$);

More configurations can be found in indexing.

Scalar Quantization

Scalar quantization maps each value to low-bit format like from 32-bit float to 8-bit integer. Usually, this won’t affect the precision a lot but can reduce the memory usage and improve the query performance. pgvecto.rs supports uint8 scalar quantization with:

CREATE INDEX ON items USING vectors (embedding vector_l2_ops)
WITH (options = "[indexing.hnsw.quantization.scalar]");

Suggestions

When quantization is required, it’s always recommended to try the scalar quantization first. It’s a good balance between precision and memory usage.

If the memory usage is still too high, you can try the product quantization. This method can reduce the memory usage significantly but may affect the precision. Since it’s highly related to the training dataset. You need to try different ratio and sample values to find the best one for your dataset.

Otherwise, you may also use some dimension reduction methods like PCA to reduce the vector dimension.

2.5 - `pgvector` compatibility

pgvecto.rs is natively compatible with pgvector at:

pgvecto.rs is natively compatible with pgvector at:

  • CREATE TABLE commands, e.g. CREATE TABLE t (val vector(3))
  • INSERT INTO commands, e.g. INSERT INTO t (val) VALUES ('[0.6,0.6,0.6]')

pgvecto.rs can be configured to be compatible with pgvector at:

  • Index options, which allows you to create index by USING hnsw (val vector_ip_ops)
  • Query options, which can be set by SET ivfflat.probes = 10

This feature is named pgvector compatibility. It can be enabled by SET vectors.pgvector_compatibility = on.

Index Options & Query Options

For index ivfflat and hnsw only the following options are available.

  • Default value for index options is different from pgvecto.rs original, which keeps the same as in pgvector.
  • Default value for query options is the same as in pgvecto.rs.

Index options for ivfflat:

Key Type Range Default Description
nlist integer [1, 1_000_000] 100 Number of cluster units.

Query options for ivfflat:

Option Type Range nn Default Description
ivfflat.probes integer [1, 1_000_000] 10 Number of lists to scan.
Warning

Default value of ivfflat.probes is 10 instead of 1 from pgvector.

Index options for hnsw:

Key Type Range Default Description
m integer [4, 128] 16 Maximum degree of the node.
ef_construction integer [10, 2000] 64 Search scope in building.

Query options for hnsw:

Option Type Range Default Description
hnsw.ef_search integer [1, 65535] 100 Search scope of HNSW.
Warning

Default value for hnsw.ef_search is 100 instead of 40 from pgvector.

Examples

It’s easy to enable pgvector compatibility and start a vector query.

DROP TABLE IF EXISTS t;
SET vectors.pgvector_compatibility=on;
SET hnsw.ef_search=40;
CREATE TABLE t (val vector(3));
INSERT INTO t (val) SELECT ARRAY[random(), random(), random()]::real[] FROM generate_series(1, 1000);
CREATE INDEX hnsw_cos_index ON t USING hnsw (val vector_cosine_ops);
SELECT COUNT(1) FROM (SELECT 1 FROM t ORDER BY val <-> '[0.5,0.5,0.5]' limit 100) t2;
DROP INDEX hnsw_cos_index;

Multiply types of indexes are accepted:

SET vectors.pgvector_compatibility=on;
SET hnsw.ef_search=40;
-- [hnsw + vector_l2_ops] index with default options
CREATE INDEX hnsw_l2_index ON t USING hnsw (val vector_l2_ops);
-- [hnsw + vector_cosine_ops] index with single ef_construction option
CREATE INDEX hnsw_cosine_index ON t USING hnsw (val vector_cosine_ops) WITH (ef_construction = 80);
-- anonymous [hnsw + vector_ip_ops] with all options
CREATE INDEX ON t USING hnsw (val vector_ip_ops) WITH (ef_construction = 80, m = 12);
SET ivfflat.probes=1;
-- [ivfflat + vector_l2_ops] index with default options
CREATE INDEX ivfflat_l2_index ON t USING ivfflat (val vector_l2_ops);
-- [ivfflat + vector_ip_ops] index with all options
CREATE INDEX ivfflat_ip_index ON t USING ivfflat (val vector_cosine_ops) WITH (nlist = 80);
-- anonymous [ivf + vector_ip_ops] with all options
CREATE INDEX ON t USING ivfflat (val vector_ip_ops) WITH (lists = 80)

3 - Use Cases

Tutorials for hybrid search, adaptive retrieval, image search, multi-tenancy, sparse vectors, and RAG QA.

3.1 - Hybrid Search

We will show you how to use pgvecto.rs to implement hybrid search in this section.

We will show you how to use pgvecto.rs to implement hybrid search in this section.

Postgres provides full text search, based on the match operator @@. It returns true if a tsvector (document) matches a tsquery (query). It doesn’t matter which data type is written first:

SELECT 'a fat cat sat on a mat and ate a fat rat'::tsvector @@ 'cat & rat'::tsquery;
 ?column?
----------
 t

SELECT 'cat & rat'::tsquery @@ 'a fat cat sat on a mat and ate a fat rat'::tsvector;
 ?column?
----------
 t
Tip

As the above example suggests, a tsquery is not just raw text, any more than a tsvector is. A tsquery contains search terms, which must be already-normalized lexemes, and may combine multiple terms using AND, OR, NOT, and FOLLOWED BY operators. In this example, the tsquery 'cat & rat' matches the tsvector 'a fat cat sat on a mat and ate a fat rat' because it contains both the terms 'cat' and 'rat'.

You could use full text search and vector search together in a single query, with the help of pgvecto.rs. In this example, we create a table named items with two columns: content and embedding. The column content is of type text, and the column embedding is of type vector(3). We insert four items into the table:

CREATE TABLE items (
  id bigserial PRIMARY KEY,
  content text NOT NULL,
  embedding vector(3) NOT NULL -- 3 dimensions
);

INSERT INTO items (content, embedding) VALUES
  ('a fat cat sat on a mat and ate a fat rat', '[1, 2, 3]'),
  ('a fat dog sat on a mat and ate a fat rat', '[4, 5, 6]'),
  ('a thin cat sat on a mat and ate a thin rat', '[7, 8, 9]'),
  ('a thin dog sat on a mat and ate a thin rat', '[10, 11, 12]');

You should get the following table:

postgres=# select * from items;
 id |                  content                   |  embedding   
----+--------------------------------------------+--------------
  2 | a fat cat sat on a mat and ate a fat rat   | [1, 2, 3]
  3 | a fat dog sat on a mat and ate a fat rat   | [4, 5, 6]
  4 | a thin cat sat on a mat and ate a thin rat | [7, 8, 9]
  5 | a thin dog sat on a mat and ate a thin rat | [10, 11, 12]
(4 rows)

Let’s search for the items with the word cat and rat in their content first. We will use full text search to do that. The function to_tsvector converts a text to a tsvector. The operator @@ checks if a tsvector matches a tsquery.

SELECT content, embedding
FROM items
WHERE to_tsvector('english', content) @@ 'cat & rat'::tsquery;
Tip

to_tsvector parses a textual document into tokens, reduces the tokens to lexemes, and returns a tsvector which lists the lexemes together with their positions in the document.

SELECT to_tsvector('english', 'a fat  cat sat on a mat - it ate a fat rats');
                  to_tsvector
-----------------------------------------------------
 'ate':9 'cat':3 'fat':2,11 'mat':7 'rat':12 'sat':4

In the example above we see that the resulting tsvector does not contain the words a, on, or it, the word rats became rat, and the punctuation sign - was ignored, because of the text search configuration is set to english.

We should get the following result:

                  content                   | embedding 
--------------------------------------------+-----------
 a fat cat sat on a mat and ate a fat rat   | [1, 2, 3]
 a thin cat sat on a mat and ate a thin rat | [7, 8, 9]
(2 rows)

Then we will sort the result by the distance between the embedding of each item and the embedding of the query. We will use vector search to do that:

SELECT content, embedding
FROM items
WHERE to_tsvector('english', content) @@ 'cat & rat'::tsquery
ORDER BY embedding <-> '[7, 8, 9]'::vector;

We will get the item with the embedding [7, 8, 9] first, and the item with the embedding [1, 2, 3] second:

                  content                   | embedding 
--------------------------------------------+-----------
 a thin cat sat on a mat and ate a thin rat | [7, 8, 9]
 a fat cat sat on a mat and ate a fat rat   | [1, 2, 3]
(2 rows)

You could normalize the rank of the full text search result and the vector search result, and then merge them together:

-- The query starts with a Common Table Expression (CTE), named "semantic_search".
WITH semantic_search AS (
    SELECT id, content, embedding, 
    RANK () OVER (ORDER BY embedding <-> '[7, 8, 9]') AS rank
    FROM items
    ORDER BY embedding <-> '[7, 8, 9]'::vector
    ), 
-- Another CTE, named "full_text_search", is defined.
    full_text_search AS (
    SELECT id, content, embedding, 
    RANK () OVER (ORDER BY 
        ts_rank(to_tsvector('english', content), 
            'cat & rat'::tsquery) DESC) AS rank
    FROM items
    WHERE to_tsvector('english', content) @@ 'cat & rat'::tsquery
    ORDER BY ts_rank(
        to_tsvector('english', content), 'cat & rat'::tsquery) DESC
    )
-- The main query selects columns from both CTEs.
    SELECT 
        COALESCE(semantic_search.id, full_text_search.id) AS id,
        COALESCE(semantic_search.content, full_text_search.content) AS content,
        COALESCE(semantic_search.embedding, full_text_search.embedding) AS embedding,
        COALESCE(1.0 / (1 + semantic_search.rank), 0.0) + 
        COALESCE(1.0 / (1 + full_text_search.rank), 0.0) AS rank
    FROM semantic_search FULL OUTER JOIN full_text_search USING (id)
    ORDER BY rank DESC;
Tip

The SQL query performs a combined semantic search and full-text search on “items” and retrieves the results based on their relevance scores. Here’s a breakdown of the query:

  • The query starts with a Common Table Expression (CTE) named “semantic_search.” It selects columns “id,” “content,” “embedding,” and calculates the rank using the “<->” operator, which measures the distance between the “embedding” column and the vector ‘[7, 8, 9]’. The results are ordered by the rank.
  • Another CTE named “full_text_search” is defined. It selects the same columns as the previous CTE but calculates the rank using the “ts_rank” function with a full-text search query. The ’to_tsvector’ function converts the “content” column into a tsvector, and the ’tsquery’ ‘cat & rat’ matches rows containing both “cat” and “rat” in the “content” column. The results are ordered by the rank.
  • The main query selects columns from both CTEs using the COALESCE function to handle cases where one CTE may have null values. It also calculates the overall rank by summing the reciprocal of the ranks from each CTE.
  • The main query performs a FULL OUTER JOIN between the two CTEs on the “id” column.
  • The final result is ordered by the overall rank in descending order.

In this query, we first calculate the rank of the full text search result and the vector search result separately. Then we merge the two results together. We will get the following result:

 id |                  content                   |  embedding   |          rank          
----+--------------------------------------------+--------------+------------------------
  4 | a thin cat sat on a mat and ate a thin rat | [7, 8, 9]    | 1.00000000000000000000
  2 | a fat cat sat on a mat and ate a fat rat   | [1, 2, 3]    | 0.70000000000000000000
  3 | a fat dog sat on a mat and ate a fat rat   | [4, 5, 6]    | 0.33333333333333333333
  5 | a thin dog sat on a mat and ate a thin rat | [10, 11, 12] | 0.33333333333333333333
(4 rows)

3.2 - Adaptive retrieval and shortening embeddings

OpenAI latest embedding models support shortening embeddings, where developers could simply “remove some numbers from the end of a sequence” and still maintain

OpenAI latest embedding models support shortening embeddings, where developers could simply “remove some numbers from the end of a sequence” and still maintain a valid representation for text.

This enables very flexible usage. For example, when using a vector data store that only supports embeddings up to 1024 dimensions long, developers can now still use our best embedding model text-embedding-3-large and specify a value of 1024 for the dimensions API parameter, which will shorten the embedding down from 3072 dimensions, trading off some accuracy in exchange for the smaller vector size.

Shortening embeddings

OpenAI’s new embedding model text-embedding-3-large produces embeddings with 3072 dimensions by default.

But you could safely remove some numbers from the end of the sequence and still maintain a valid representation for text. For example, you could shorten the embeddings to 1024 dimensions.

Details

You may need normalization to ensure that the shortened embeddings compatible with some distance calculation e.g. dot product. OpenAI’s API will help you on this if you are calling text-embedding-3-large to generate a lower dimension embedding directly, instead of truncating the original embeddings on your own.

embedding_3072_dim_from_openai = [-0.00611, -0.00269, ...]
embedding_1024_dim_from_openai = [-0.01520, -0.03839, ...]

embedding_1024_dim_truncated_from_3072_dim = [-0.00611, -0.00269, ...]
embedding_1024_dim_normalized = [-0.01520, -0.03839, ...]

There are lots of benefits to using shortening embeddings:

  • Reduced memory usage: Shortening embeddings can reduce the memory usage to store the embeddings.
  • Lower dimensions: Shortening embeddings can reduce the dimensions of the embeddings. Some vector data stores only support embeddings up to a certain number of dimensions (e.g. 2000).
  • Faster indexing and search: Smaller embeddings can be indexed and searched faster than larger embeddings.
  • Trade-off performance and cost: Shortening embeddings can trade off some accuracy in exchange for the smaller vector size.

However, the accuracy of the shortened embeddings is not as good as the full embeddings.

Trade-off performance and cost

We have performed an experiment to evaluate the effectiveness of reduced embeddings compared to the complete embeddings. The benchmark was conducted using the dbpedia-entities-openai3-text-embedding-3-large-3072-1M dataset. The experiment was carried out on a Google Cloud VM with specifications of n2-standard-8, which includes 8 vCPUs and 32 GB of memory.

We have established a table and populated it with 1 million embeddings, each consisting of 3072 dimensions.

CREATE TABLE openai3072 (
  id bigserial PRIMARY KEY,
  text_embedding_3_large_3072_embedding vector(3072),
  text_embedding_3_large_3072_embedding bvector(3072)
);
Details

The code for inserting 1M embeddings into the table:

from pgvecto_rs.sdk import PGVectoRs, Record
from pgvecto_rs.psycopg import register_vector
import psycopg

conn = psycopg.connect(conninfo='postgresql://postgres:mysecretpassword@localhost:5433/postgres', autocommit=True)

import json
import os

import numpy as np
import random
from datasets import load_dataset
from datasets.exceptions import DatasetNotFoundError
from tqdm import tqdm

MODEL_NAME, DIMENSIONS = "text-embedding-3-large", 3072
DATASET_NAME = f"Qdrant/dbpedia-entities-openai3-{MODEL_NAME}-{DIMENSIONS}-1M"
collection_name = f"dbpedia-{MODEL_NAME}-{DIMENSIONS}"
embedding_column_name = f"{MODEL_NAME}-{DIMENSIONS}-embedding"

dataset = load_dataset(
        DATASET_NAME,
        streaming=False,
        split="train",
    )

bs = 1000
for i, record in tqdm(enumerate(dataset)):
    if i % bs == 0:
        points = []
    points.append({
        "embedding": record[embedding_column_name]
    })
    if i % bs == bs - 1:
        batch_points = f", ".join([f"('{p['embedding']}')" for p in points])
        conn.execute(f"INSERT INTO openai3072 (text_embedding_3_large_3072_embedding) VALUES %s" % (batch_points))
    print(f"Inserted {i} records")

Furthermore, we have created indexes for embeddings with dimensions of 256, 1024 and 3072, and performed an ANN (Approximate Nearest Neighbors) query for each dimension. There is no need to create the 256-dimensional column, as we can use the first 256 dimensions of the 3072-dimensional embeddings.

-- Create 256-dim index
CREATE INDEX openai_vector_index_256 ON public.openai3072 USING vectors (((text_embedding_3_large_3072_embedding[0:256])::vector(256)) vector_l2_ops);

-- Create 1024-dim index
CREATE INDEX openai_vector_index_1024 on openai3072 using vectors((text_embedding_3_large_3072_embedding[0:1024]::vector(1024)) vector_l2_ops);

-- Create 3072-dim index
CREATE INDEX openai_vector_index on openai3072 using vectors(text_embedding_3_large_3072_embedding vector_l2_ops);

Additionally, we have constructed a binary vector index for the 3072-dimensional embeddings and conducted an ANN query using this index as well.

CREATE INDEX openai_vector_index_bvector ON public.openai3072 USING vectors (text_embedding_3_large_3072_bvector bvector_l2_ops);

We run the KNN query with different limits (5, 10, 50, 100) for the number of neighbors to retrieve.

Details

The QPS (Queries Per Second) and the average query time were measured for each query with the help of pgbench.

export PGHOST="localhost"
export PGUSER="postgres"
export PGPASSWORD="mysecretpassword"
export PGDATABASE="postgres"
export PGPORT=5433
export VECTOR_DIM=3072
TEST_TIME=300
for clients in 2 4 8 16 32; do
    pgbench -n -T "${TEST_TIME}" \
      --file=query.sql \
      -c "${clients}" -j "${clients}"
done

The query.sql file contains the following SQL queries:

\set random_id random(1, 1000000)

select id from openai3072 order by text_embedding_3_large_3072_embedding <-> (select text_embedding_3_large_3072_embedding from openai3072 where id = :random_id) limit 100;

The results of the experiment are shown here:

As anticipated, the 3072-dimensional embeddings exhibit the highest accuracy, while the 1024-dimensional embeddings demonstrate a slightly lower accuracy at around 85%. The 256-dimensional embeddings yield the lowest accuracy, approximately 65%. On the other hand, the binary vector index achieves an accuracy of about 80%.

Regarding the Requests Per Second (RPS) metric, the binary vector index showcases the most efficient performance, followed by the 256-dimensional embeddings, the 1024-dimensional embeddings, and finally, the 3072-dimensional embeddings.

Details
table

The memory usage of the indexes is an important aspect to take into account. Indexing 1 million 3072-dimensional binary vectors requires only around 600MB of memory. In contrast, the 3072-dimensional vector index consumes approximately 18GB of memory. This represents a significant difference in memory usage, as the 3072-dimensional vector index utilizes approximately 30x more memory than the binary vector index.

Improve the accuracy via adaptive retrieval

Lower dimension and binary vector indexes trade off accuracy for advantages such as higher RPS and lower memory usage compared to full dimension indexes. Adaptive retrieval techniques can be used to combine the strengths of both approaches, achieving an optimized solution that balances accuracy, memory usage, and query speed.

The logic behind adaptive retrieval is really simple. Let’s take get top 100 candidates as an example. We can perform the following steps:

  1. Query the lower dimensional or binary vector index first to retrieve 200 candidates from the 1 million embeddings. This is a fast operation.
  2. Rerank the candidates using a KNN query to retrieve the top 100 candidates. K-NN is well-suited for situations where smaller sets and precise similarity search are necessary, making it an excellent choice for reranking in this context.

The reranking step is a bit slower than the initial query, but it is still much faster than querying the full dimension index. It could be done in the pgvecto.rs:

CREATE OR REPLACE FUNCTION match_documents_adaptive(
  query_embedding vector(3072),
  match_count int
)
RETURNS SETOF openai3072
LANGUAGE SQL
AS $$
WITH shortlist AS (
  SELECT *
  FROM openai3072
  ORDER BY (text_embedding_3_large_3072_embedding[0:256])::vector(256) <-> (query_embedding[0:256])::vector(256)
  LIMIT match_count * 2
)
SELECT *
FROM shortlist
ORDER BY text_embedding_3_large_3072_embedding <-> query_embedding
LIMIT match_count;
$$;

The match_documents_adaptive function accepts a query embedding and a match count as input parameters. It first retrieves match_count * 2 candidates from the 1 million embeddings using the 256-dimensional index. Then, it reranks the candidates using a KNN query to retrieve the top match_count candidates.

The function for binary vector and 1024-dimensional embeddings can be implemented in a similar manner.

We conduct the benchmark again with the adaptive retrieval technique. We tag the adaptive retrieval technique as Adaptive Retrieval in the following figure.

adaptive retrieval benchmark

Adding a reranking step significantly improves the accuracy of the lower dimensional and binary vector indexes.

The binary vector index with adaptive retrieval achieves an accuracy of around 95%, with only a slight decrease in RPS compared to the binary vector index without adaptive retrieval. Remarkably, the memory usage of the binary vector index with adaptive retrieval is only around 600MB, which is the same as the binary vector index without adaptive retrieval.

Details
table

Conclusion

Shortening embeddings can reduce the memory usage to store the embeddings, and speed up indexing and search. However, the accuracy of the shortened embeddings is not as good as the full embeddings. That’s why we need to use adaptive retrieval techniques to improve the accuracy of the lower dimensional indexes.

It seems that binary vector indexes with adaptive retrieval are the best choice for the text-embedding-3-large model. You could also consider 1024-dimensional indexes with adaptive retrieval if you need a higher accuracy (e.g. 99%).

PS: All the code and data used in this experiment are available in the pgvecto.rs-matryoshka-embeddings-benchmark.

3.3 - Image Search

Image intelligent search, also known as content-based image retrieval, goes beyond traditional keyword-based search and enables users to search for images based

Image intelligent search, also known as content-based image retrieval, goes beyond traditional keyword-based search and enables users to search for images based on their visual content. For example, you could search images that contain a red mushroom (image from immich):

image-search

The feature looks like a magic because it allows you to search for images that you don’t have keywords for. The search engine understands the content of the images and videos and can find the ones that are similar to the query.

How it works

Image intelligent search leverages advanced computer vision techniques and machine learning algorithms to understand the visual characteristics and features within images, allowing users to perform freeform searches without relying on specific keywords or metadata.

The first step is to extract meaningful features from images and generate a vector representation of each image. This is typically done using deep learning models such as CLIP. The vector representation is called an embedding. It is a list of numbers that represents the image.

Then you could use vector stores like pgvecto.rs to store the embeddings and perform efficient vector search.

Example

We will leverage CLIP, a neural network that connects text and images, to generate embeddings for our images. These embeddings capture the visual concepts present in the images. Subsequently, we will utilize pgvecto.rs, a scalable vector search tool built on Postgres, to store these embeddings and conduct efficient vector searches. By combining the power of CLIP and pgvecto.rs, we can retrieve similar images based on their visual content.

Generate embeddings

You could use CLIP to generate embeddings for images:

from transformers import CLIPProcessor, CLIPModel

processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")

def compute_image_embeddings(list_of_images):
    return model.get_image_features(**processor(images=list_of_images, return_tensors="pt", padding=True))

Store embeddings

Let’s say you have a table named images with three columns: id, author and embedding. The column embedding is of type vector(512). You could insert an image into the table like this:

-- Create the table
CREATE TABLE images (
    id SERIAL PRIMARY KEY,
    author TEXT NOT NULL,
    vector vector(512) NOT NULL -- 3 dimensions
);

INSERT INTO images (author, embedding) VALUES
  ('Allen', '[...]');

Search for similar images

You could search for similar images like this:

SELECT * FROM images ORDER BY embedding <-> '[...]' LIMIT 5;

Assuming you want to search for the 64 nearest rows and the results don’t contain the images from the author Allen.

You may want the following SQL to work:

SET vectors.hnsw_ef_search = 64;
SELECT * FROM images ORDER BY embedding <-> '[...]' WHERE author != 'Allen' LIMIT 64;

In base mode, you may only get 32 rows since the HNSW algorithm bypasses filter conditions during the search and relies on PostgreSQL to apply filters post-search.

You could use the vbase search mode to achieve this. In vbase mode, searching results become an iterator that continuously returns results. The database can continuously retrieve results from it until the user’s query conditions are met. It’s quite different from other vector search library, such as faiss. The latter can only return a fixed-size result set, and this size needs to be determined before the search. For queries with filtering conditions, it is not possible to determine the size of the result set in advance. As a result, either insufficient results can be obtained or excessive searching will affect latency. The original idea comes from VBASE: Unifying Online Vector Similarity Search and Relational Queries via Relaxed Monotonicity.

You can enable vbase by a SQL statement SET vectors.search_mode = vbase;. Then you can get the correct behavior. This mode is used in immich’s image search feature. vbase is the default search mode in 0.2.

Real-world applications

Image intelligent search is used in immich to enable users to search for images based on their visual content. The code snippets provided here are inspired by the implementation of the image search feature in immich.

SELECT a.*, s.*, e.*
FROM asset_entity AS a
INNER JOIN smart_search AS s ON s.assetId = a.id
LEFT JOIN exif_info AS e ON e.assetId = a.id
WHERE a.ownerId IN (:...userIds)
  AND a.isArchived = false
  AND a.isVisible = true
  AND a.fileCreatedAt < NOW()
ORDER BY s.embedding <=> :embedding
LIMIT :numResults;

The query retrieves data from the asset_entity, smart_search, and exif_info tables based on specified conditions and ordering. Let’s break down the query:

SELECT a.*, s.*, e.*

This part of the query specifies the columns to be selected from the tables asset_entity, smart_search, and exif_info. The aliases a, s, and e are used to refer to each table respectively.

FROM asset_entity AS a
INNER JOIN smart_search AS s ON s.assetId = a.id
LEFT JOIN exif_info AS e ON e.assetId = a.id

Here, the asset_entity table is aliased as a, the smart_search table as s, and the exif_info table as e. The INNER JOIN is used to combine asset_entity and smart_search based on the condition s.assetId = a.id. The LEFT JOIN is used to include matching records from the exif_info table based on the condition e.assetId = a.id.

Details

The table asset_entity has the following schema.

CREATE TABLE asset_entity (
  id UUID PRIMARY KEY,
  deviceAssetId VARCHAR NOT NULL,
  ownerId VARCHAR NOT NULL,
  libraryId VARCHAR NOT NULL,
  deviceId VARCHAR NOT NULL,
  type VARCHAR NOT NULL,
  originalPath VARCHAR NOT NULL,
  resizePath VARCHAR,
  webpPath VARCHAR DEFAULT '',
  thumbhash BYTEA,
  encodedVideoPath VARCHAR,
  createdAt TIMESTAMPTZ NOT NULL,
  updatedAt TIMESTAMPTZ NOT NULL,
  deletedAt TIMESTAMPTZ,
  fileCreatedAt TIMESTAMPTZ NOT NULL,
  localDateTime TIMESTAMPTZ NOT NULL,
  fileModifiedAt TIMESTAMPTZ NOT NULL,
  isFavorite BOOLEAN DEFAULT false,
  isArchived BOOLEAN DEFAULT false,
  isExternal BOOLEAN DEFAULT false,
  isReadOnly BOOLEAN DEFAULT false,
  isOffline BOOLEAN DEFAULT false,
  checksum BYTEA NOT NULL,
  duration VARCHAR,
  isVisible BOOLEAN DEFAULT true,
  livePhotoVideoId UUID,
  originalFileName VARCHAR NOT NULL,
  sidecarPath VARCHAR,
  stackId UUID,
  CONSTRAINT fk_asset_owner FOREIGN KEY (ownerId) REFERENCES user_entity (id) ON DELETE CASCADE ON UPDATE CASCADE,
  CONSTRAINT fk_asset_library FOREIGN KEY (libraryId) REFERENCES library_entity (id) ON DELETE CASCADE ON UPDATE CASCADE,
  CONSTRAINT fk_asset_live_photo_video FOREIGN KEY (livePhotoVideoId) REFERENCES asset_entity (id) ON DELETE SET NULL ON UPDATE CASCADE,
  CONSTRAINT fk_asset_stack FOREIGN KEY (stackId) REFERENCES asset_stack_entity (id) ON DELETE SET NULL ON UPDATE CASCADE
);
WHERE a.ownerId IN (:...userIds)
  AND a.isArchived = false
  AND a.isVisible = true
  AND a.fileCreatedAt < NOW()

In this part, the query applies filters to the asset_entity table. It selects rows where the ownerId column is among the specified userIds, isArchived is false, isVisible is true, and fileCreatedAt is earlier than the current time (NOW()).

ORDER BY s.embedding <=> :embedding

The query orders the results based on the embedding column of the smart_search table, comparing it to the provided :embedding value. The <=> operator is used for the comparison.

LIMIT :numResults;

Finally, limit the number of rows returned by the query to the value specified by the :numResults parameter.

This is an real-world use case to show how image intelligent search is implemented in a production system.

3.4 - Multi Tenancy

Multi-tenancy is essential for SaaS applications, allowing a single instance to serve multiple users or groups while ensuring data privacy and security. This bl

Multi-tenancy is essential for SaaS applications, allowing a single instance to serve multiple users or groups while ensuring data privacy and security. This blog outlines the design and implementation a simple multi-tenant vector search system with pgvecto.rs.

Designing the Database Schema

The success of user-based isolation in multi-tenant systems hinges on a well-structured database schema:

Users and Documents

Each user is identified by a unique user_id, which directly links them to their documents for private access. The documents table contains each user’s documents, each with a distinct id, title, and content. Additional metadata can be included in the documents table to improve search functionality. The user_id is linked to the documents instead of chunk embeddings to replicate real-world scenarios more accurately.

CREATE TABLE documents (
    id SERIAL PRIMARY KEY,
    user_id INTEGER NOT NULL,
    title TEXT,
    content TEXT,
    -- Additional metadata
);

Chunks

Embeddings store vectorized representations of chunks of documents, and use document_id to reference its document.

CREATE TABLE chunks (
    id SERIAL PRIMARY KEY,
    document_id INTEGER REFERENCES documents(id),
    embedding vector(512) NOT NULL
);

Entity-Relation Diagram

Implementing Multi-Tenancy

Create Index

Create vector index on chunks.embedding to accelerate query process. Here we use dot product as the similarity measure.

CREATE INDEX idx_chunks_embedding ON chunks USING vectors (embedding vector_dot_ops);

Run the query with user_id

SELECT chunks.id AS chunk_id, documents.id AS document_id, documents.title
FROM chunks
INNER JOIN documents ON chunks.document_id = documents.id
WHERE documents.user_id = 'alice' /* Replace with user_id */
ORDER BY chunks.embedding <-> '[3,2,1]' /* Replace with query embedding */ LIMIT 5;

In this query:

  • The SELECT statement now includes chunks.id AS chunk_id, documents.id AS document_id, and documents.title to return the chunk ID, the document ID, and the document title, respectively.
  • The INNER JOIN ensures that only chunks associated with documents owned by the specified user (‘alice’) are considered.
  • The ORDER BY clause calculates the distance between the chunk’s embedding and the provided vector [3,2,1], sorting the results by similarity. The closest or most similar embeddings are returned first.
  • Replace [3,2,1] with the query embedding and ‘specific_user_id’ with the actual ID of the user for whom you are performing the search.

Performance Tip

The vector search will scan through additional points and evaluate the conditions individually. Consequently, if there is a large number of user IDs or the filter criteria are challenging to meet, the query speed may be impacted. To enhance performance, you may want to explore utilizing PostgreSQL’s partition table function.

3.5 - Sparse Vector Search

In the past, hybrid search combined two search methods: traditional keyword-based search and vector-based similarity search. However, sparse vector can act as a

In the past, hybrid search combined two search methods: traditional keyword-based search and vector-based similarity search. However, sparse vector can act as a substitute for keyword search, unlocking the full potential of the data pipeline with pure vector search.

Tip

For more information about keyword-based hybrid search, please refer to Hybrid Search

This post explores how to use how BGE-M3 and pgvecto.rs to exploit the potential of hybrid search using both sparse and dense vectors.

Let’s dive in.

An Overview of Sparse and Dense Vector Embedding

Sparse vectors are high-dimensional vectors that contain few non-zero values. They are suitable for traditional information retrieval use cases. For example, a vector with 32,000 dimensions but only 2 non-zero elements is a typical sparse vector.

S[1×32000]=[000.0150000⋯0.5430] S[1\times 32000]=\left[ \begin{matrix} 0 & 0 & 0.015 & 0 & 0 & 0 & 0 & \cdots & 0.543 & 0 \end{matrix} \right]

Dense vectors are embeddings from neural networks. They are generated by text embedding models and have most or all elements non-zero. They have fewer dimensions, such as 256 or 1536, much less than sparse vectors.

D[1×256]=[0.3421.7740.0870.3210.6640.8700.001⋯0.5430.999] D[1\times 256]=\left[ \begin{matrix} 0.342 & 1.774 & 0.087 & 0.321 & 0.664 & 0.870 & 0.001 & \cdots & 0.543 & 0.999 \end{matrix} \right]

Dense vectors and sparse vectors are both used to represent features of text chunks, but they have significant differences. A single dimension in a dense vector embedding does not mean anything, as it is too abstract to determine its meaning. However, when we take all the dimensions together, they provide the semantic meaning of the input text.

Sparse vectors have dimensions equal to the vocabulary size, with each element representing the weight of a word in the text.

Generally, words with higher weights indicate greater importance in the text. Sparse vectors simplify the analysis and visualization of the relationship between text and vocabulary. To visualize dense vectors, additional dimension reduction algorithms are necessary, such as:

Dense Vector Sparse Vector
Dimension Hundreds to thousands Tens of thousands
Element Meaning Nothing Word Weight
Interpretability Hard Easy
Usage Vector Distance Vector Distance / Visualize
Semantic sensitivity High Low

Sparse vectors are generally less effective at capturing the relationships between words, particularly for synonyms and related terms, compared to dense vectors.

For instance, both rocket and SPACE X are related to space exploration, but Dense vectors are more likely to reveal similarities and assign a closer vector distance to corresponding texts.

How to create a vector embedding?

A vector embedding is the internal representation of input data in deep learning models, also known as embedding models. Most embedding models, such as text-embedding-3-small, only output dense embeddings.

SPLADE and BGE-M3 can generate sparse embeddings, sometimes called lexical weights. In this section, we will introduce the BGE-M3 model for generating dense and sparse vectors.

BGE-M3 is a multi-functionality model, which can simultaneously perform the three common retrieval functionalities of the embedding model: dense retrieval, multi-vector retrieval, and sparse retrieval. We will learn how to generate dense and sparse embeddings from it.

# pip install -U FlagEmbedding

from FlagEmbedding import BGEM3FlagModel

model = BGEM3FlagModel("BAAI/bge-m3", use_fp16=True)
sentences = ["What is BM25?", "Definition of BM25"]

output = model.encode(sentences, return_dense=True, return_sparse=True)

# The dense embedding of the two texts above:
dense_embedding = output['dense_vecs']
# [[-0.02501617 -0.04525582 -0.01972856 ... -0.0099566   0.01677509
#   0.01292699]
# [-0.0106499  -0.04731942 -0.01477693 ... -0.0149255   0.01040097
#   0.00965083]

sparse_weight_id = output['lexical_weights']

# You can see the weight for each token:
# print(model.convert_id_to_token(sparse_weight_id))
# 
# [{'What': 0.10430284, 'is': 0.10090457, 'BM': 0.2635918, '25': 0.3382988, '?': 0.052101523}, 
# {'Definition': 0.14298248, 'of': 0.076763615, 'BM': 0.2577639, '25': 0.33806682}]

For all sentences and words, a text-word matrix was created:

TEXT What is BM 25 ? Definition of …
What is BM25? 0.08 0.08 0.13 0.25 0.04 0 0 …
Definition of BM25 0 0 0.25 0.33 0 0.14 0.06 …

The matrix is highly sparse, especially when all vocabularies are included. Most words only appear in a few sentences, resulting in non-zero weights.

By feeding text into sparse embedding models like BGE-M3, we can easily obtain sparse embeddings from it.

# pip install -U numpy

import numpy as np

# Sparse vector embedding of text
sparse_indices = [[int(token_id) for token_id in text] for text in sparse_weight_id]
# [[4865, 83, 90017, 2588, 32], [155455, 111, 90017, 2588]]
sparse_values = [[float(text[token_id]) for token_id in text] for text in sparse_weight_id]
# [[0.10430284, 0.10090457, 0.2635918, 0.3382988, 0.052101523], [0.14298248, 0.076763615, 0.2577639, 0.33806682]]

# Sparse indexes must be ascending for each pgvecto.rs
for i in range(len(sparse_weight_id)):
    sparse_values[i] = [x for _, x in sorted(zip(sparse_indices[i], sparse_values[i]))]
    sparse_indices[i] = sorted(sparse_indices[i])

Now we have both the sparse and dense embeddings. It’s time to insert them into the vector database.

Storing vectors in pgvecto.rs

First, a deployed pgvecto.rs instance is required for vector storage and retrieval. This can be achieved by the official pgvecto-rs Docker image:

docker run \
  --name pgvecto-rs-demo \
  -e POSTGRES_PASSWORD=mysecretpassword \
  -p 5432:5432 \
  -d tensorchord/pgvecto-rs:pg16-v0.3.0

Now the only necessary dependency is installed. Let’s create a table to store the vectors.

pgvecto.rs supports dense or sparse vectors with different data types:

  • vector for dense vector
  • svector for sparse vector

The BGE-M3 model has a dense embedding dimension of 1024. The sparse embedding dimension is 250002, which matches the size of tokenizer vocabulary.

print(len(model.tokenizer))
# 250002

In this example, we create a table named documents with four columns: an ID column(id), a text column(text) and two vector columns(sparse for sparse vector and dense for dense vector).

# pip install -U psycopg

import psycopg
URL = "postgresql://postgres:mysecretpassword@localhost:5432/postgres"

with psycopg.connect(URL) as conn:
    conn.execute("DROP TABLE IF EXISTS documents;")
    conn.execute(f"""
        CREATE TABLE documents (
            id SERIAL PRIMARY KEY, 
            text TEXT NOT NULL,
            dense vector(1024) NOT NULL,
            sparse svector({len(model.tokenizer)}) NOT NULL);
    """
    )

With the table created, we can now insert embeddings into the table and create indexes for vector columns.

# pip install -U pgvecto_rs

import psycopg
from pgvecto_rs.psycopg import register_vector
URL = "postgresql://postgres:mysecretpassword@localhost:5432/postgres"

with psycopg.connect(URL) as conn:
    register_vector(conn)
    for text, dense, sparse_ind, sparse_val in zip(sentences, dense_embedding, sparse_indices, sparse_values):
        conn.execute(
            "INSERT INTO documents (text, dense, sparse) VALUES (%s, %s, to_svector(%s, %s::real[]::int[], %s::real[]));",
            (text, dense, len(model.tokenizer), sparse_ind, sparse_val),
        )
    conn.execute("""
        CREATE INDEX ON documents 
        USING vectors (sparse svector_dot_ops) 
        WITH (options = \"[indexing.hnsw]\");
    """)
    conn.execute("""
        CREATE INDEX ON documents 
        USING vectors (dense vector_l2_ops) 
        WITH (options = \"[indexing.hnsw]\");
    """)

For dense vectors, vector_l2_ops calculates the Squared Euclidean distance, which is the most commonly used distance metric.

DL2=Σ(xi−yi)2 D_{L2} = \Sigma (x_i - y_i) ^ 2

For sparse vectors, vector_dot_ops calculates the dot product, which is a more efficient method.

Ddot=−Σxiyi D_{dot} = - \Sigma x_i y_i

For more information about index types in pgvecto.rs, please check out our documentation.

Retrieving and Mixing Results

With the vectors stored in the database, we can efficiently query them using the pgvecto.rs extension. To find the nearest vectors to a given input text, we need to encode the text with the same model.

This process is identical to the one described in the previous chapter:

def embedding(sentence: str) -> tuple[np.ndarray, list[list[int]], list[list[float]]]:
    """
    Create dense and sparse embeddings from text
    """
    output = model.encode(sentence, return_dense=True, return_sparse=True, return_colbert_vecs=False)
    dense_embedding = output['dense_vecs']
    sparse_weight_id = output['lexical_weights']
    sparse_indices = [int(token_id) for token_id in sparse_weight_id]
    sparse_values = [float(sparse_weight_id[token_id]) for token_id in sparse_weight_id]

    sparse_values = [x for _, x in sorted(zip(sparse_indices, sparse_values))]
    sparse_indices = sorted(sparse_indices)
    return dense_embedding, sparse_indices, sparse_values

The pgvecto.rs extension provides a simple SELECT statement to get sparse and dense vectors like this:

dense_embedding, sparse_indices, sparse_values = embedding("The text you want to search...")

with psycopg.connect(URL) as conn:
    register_vector(conn)
    cur = conn.execute(
        "SELECT text FROM documents ORDER BY sparse <#> to_svector(%s, %s::real[]::int[], %s::real[]) LIMIT 50;",
        (len(model.tokenizer), sparse_indices, sparse_values),
    )
    sparse_result = cur.fetchall()
      
    cur = conn.execute(
              "SELECT text FROM documents ORDER BY dense <-> %s LIMIT 50;",
              (dense_embedding,),
          )
    dense_result = cur.fetchall()
    
# Merge sparse and dense vector search results
mix_text = set([r[0] for r in dense_result]).union([r[0] for r in sparse_result])

To rerank the merged result, you can use popular fusion methods such as Reciprocal Ranked Fusion (RRF). However, to make better use of BGE-M3, let’s introduce the Reranker model of BGE-M3. Unlike the embedding model, Reranker uses text input and directly outputs their similarity or score.

We can initialize a Reranker model and feed it with mixed candidate texts. With this method, reranking can be as simple as sorting by the Reranker model’s estimated score.

from FlagEmbedding import FlagReranker

reranker = FlagReranker('BAAI/bge-reranker-large', use_fp16=True)

# Obtain relevance scores (Higher scores indicate greater relevance)
scores = reranker.compute_score([["The text you want to search...", candidate] for candidate in mix_text])
# Rerank text using combined dense and sparse embeddings
reranked_text = [t for _, t in sorted(zip(scores, mix_text))]

Congratulations! We have now completed our first step into the world of sparse vector search.

3.6 - Generative Question-Answering

Vector search can also be used to provide context to generative models such as OpenAI’s GPT series, improving the quality of the outputs.

Vector search can also be used to provide context to generative models such as OpenAI’s GPT series, improving the quality of the outputs.

This post will explore the use of pgvecto.rs in building and retrieving from a vector knowledge base to bolster the performance of generative models in question-answering tasks. A knowledge base serves as “long-term memory” for generative models, providing them persistent, curated, and accurate information as context to draw from when generating an answer to a question. This results in answers that are more accurate, reduces the possibility of the model producing “hallucinations” (statements with no basis in reality), and increases user trust in the outputs. This technique is known as Retrieval-Augmented Generation, or RAG: see this post or the related paper for more information.

Overview of the Question-Answering task

The generative model question-answering (QA) task is a method of Information Retrieval. The model is presented with a question, and generates an answer based on some context: typically, documents or other information sources the model can look at to retrieve the information necessary to answer the question.

This use case will involve giving the OpenAI gpt-3.5-turbo-instruct model context to answer questions about movies from the vishnupriyavr/wiki-movie-plots-with-summaries-faiss-embeddings Huggingface dataset.

The overall workflow will look something like this:

Question-Answering without Context

First, let’s try having the model answer some questions without providing it context.

We begin with defining a function to streamline getting a response for a given prompt:

# pip install openai

import openai

openai.api_key = "YOUR_API_KEY_HERE"
def complete(prompt):
    result = openai.Completion.create(
        engine='gpt-3.5-turbo-instruct',
        prompt=prompt,
        temperature=0,
        max_tokens=400,
        top_p=1,
        frequency_penalty=0,
        presence_penalty=0,
        stop=None
    )
    return result['choices'][0]['text'].strip()
Note

The api_key requires an OpenAI account. Once logged in, sign up for an API key from the top-right drop-down menu, and paste it in place of YOUR_API_KEY_HERE.

Let’s start with asking about a relatively popular movie:

base_prompt = "What is the release year and overall plot of Skyfall?"
print(complete(base_prompt))
Output:

The release year of Skyfall is 2012. The overall plot follows James Bond as he investigates an attack on MI6 and uncovers a sinister plot by a former agent seeking revenge on M and the entire agency. Bond must confront his past and protect MI6 from destruction.

Accurate, if somewhat vague in the plot summary. However, what about a movie that’s much less known?

base_prompt_2 = "What is the release year and overall plot of Cat Napping?"
print(complete(base_prompt_2))
Output:

The release year of Cat Napping is unknown. The overall plot of Cat Napping is about a group of cats who go on a mission to rescue their friend who has been kidnapped by a group of evil dogs. Along the way, they encounter various challenges and obstacles, but with their cleverness and teamwork, they are able to save their friend and defeat the dogs.

Correct answer:

Cat Napping is actually a Tom and Jerry movie from 1951, where the two fight over a hammock.

We have found the limits of what the model “knows”: it is unable to produce the release year, and invents a potential plot based on the title. We will remedy this by creating a knowledge base for the model to draw on.

There are two options for allowing the model to better answer our question:

  1. Fine-tune the model on text data in the domain of interest (movies in this case).
  2. Employ retrieval augmented generation, which adds an information retrieval component to the process. This will allow us to retrieve and feed relevant information to the model as a secondary source of information.

We will be taking the second option in this post.

Building the Knowledge Base

Let’s build a knowledge base of movies to retrieve relevant information from, using the vishnupriyavr/wiki-movie-plots-with-summaries-faiss-embeddings Huggingface dataset. This dataset contains titles, release years, casts, Wikipedia pages, plot summaries/lengths, and vector embeddings of this information for 33,155 movies. Each movie also has text data, which encompasses title, release year, cast, and plot summary in one. We will be using the text and embeddings data for this application.

Load the Data

Start by loading the data:

# pip install -U datasets
from datasets import load_dataset
dataset = load_dataset("vishnupriyavr/wiki-movie-plots-with-summaries-faiss-embeddings", split='train')

texts = dataset['train']['text']
# texts[0]
# 'Alice in Wonderland \n  starring May Clark \n  released in the year1903 \n  with the following plot  \n Alice follows a large white rabbit down a "Rabbit-hole". She finds a tiny door. When she finds a bottle labeled "Drink me", she does, and shrinks, but not enough to pass through the door. She then eats something labeled "Eat me" and grows larger. She finds a fan when enables her to shrink enough to get into the "Garden" and try to get a "Dog" to play with her. She enters the "White Rabbit\'s tiny House," but suddenly resumes her normal size. In order to get out, she has to use the "magic fan."\r\nShe enters a kitchen, in which there is a cook and a woman holding a baby. She persuades the woman to give her the child and takes the infant outside after the cook starts throwing things around. The baby then turns into a pig and squirms out of her grip. "The Duchess\'s Cheshire Cat" appears and disappears a couple of times to Alice and directs her to the Mad Hatter\'s "Mad Tea-Party." After a while, she leaves.\r\nThe Queen invites Alice to join the "ROYAL PROCESSION": a parade of marching playing cards and others headed by the White Rabbit. When Alice "unintentionally offends the Queen", the latter summons the "Executioner". Alice "boxes the ears", then flees when all the playing cards come for her. Then she wakes up and realizes it was all a dream.'

Generate Embeddings

This particular dataset comes with embeddings, so you can skip this step and use the embeddings directly.

embeddings = dataset['train']['embeddings']

If you want to generate embeddings from the text data, you can use the OpenAI, Cohere or other embeddings APIs. We provide an example using the Cohere API, you can get Cohere API key from here.

# pip install cohere
import getpass
import os
import cohere

if not os.getenv("COHERE_API_KEY"):
    os.environ["COHERE_API_KEY"] = getpass.getpass("Enter your Cohere API key: ")

co = cohere.Client()
# embedding model can refer to this documentation: https://docs.cohere.com/docs/cohere-embed
embed = co.embed(
    texts=texts,
    input_type="search_query",
    model="embed-english-v3.0"
)
embeddings = embed.embeddings
Note

The embeddings in this user case are dense vectors. For creating sparse vectors, you can see this page.

Insert Data into the Database

A deployed pgvecto.rs instance is required for vector storage and retrieval. We can use the official pgvecto-rs Docker image:

docker run \
  --name pgvecto-rs-demo \
  -e POSTGRES_PASSWORD=mysecretpassword \
  -p 5432:5432 \
  -d tensorchord/pgvecto-rs:pg16-v0.3.0

Install the required psycopg library to interact with Postgres, and ensure the extension is running:

#pip install "psycopg[binary,pool]"
import psycopg

URL = "postgresql://postgres:mysecretpassword@localhost:5432/postgres"
#Create the table
with psycopg.connect(URL) as conn:
    conn.execute("DROP EXTENSION IF EXISTS vectors;")
    conn.execute("CREATE EXTENSION vectors;")

Now, we create a table movies with a few columns: an ID column(id), a text column(texts), and a vector column(embeddings).

import psycopg
URL = "postgresql://postgres:mysecretpassword@localhost:5432/postgres"

#Create the table
with psycopg.connect(URL) as conn:
    conn.execute("DROP TABLE IF EXISTS movies;")
    conn.execute(f"""
        CREATE TABLE movies (
            id SERIAL PRIMARY KEY, 
            text TEXT NOT NULL,
            embedding vector(768) NOT NULL);
    """
    )
Note

We use type vector(n), as we are working with dense vector embeddings, where n is the dimension of each embedding (768 in this case).

With the table created, we can now insert the embeddings from the dataset and create indexes for vector columns:

# Optional install, but recommended:
# pip install tqdm
import psycopg
from tqdm import tqdm
URL = "postgresql://postgres:mysecretpassword@localhost:5432/postgres"

#Insert into table
with psycopg.connect(URL) as conn:
    with conn.cursor() as cursor:    
        for text, embedding in tqdm(zip(texts, embeddings), total=len(texts)):
            cursor.execute(
                """INSERT INTO movies (text, embedding) 
                    VALUES (%s, %s::real[]);""",
                (text, embedding),
            )
        conn.commit()
Warning

This may take a while, depending on how much data is to be inserted.

Create Index

import psycopg
URL = "postgresql://postgres:mysecretpassword@localhost:5432/postgres"

#Creating indexes for embedding column
with psycopg.connect(URL) as conn:
    cursor = conn.cursor()
    cursor.execute("""
        CREATE INDEX ON movies 
        USING vectors (embedding vector_l2_ops) 
        WITH (options = \"[indexing.hnsw]\");
    """)
    conn.commit()
Note

Recall that for dense vectors, vector_l2_ops calculates the Squared Euclidean distance, which is the most commonly used distance metric.

$$

D_{L2} = \Sigma (x_i - y_i) ^ 2

$$

Question-Answering with Context

To make use of our knowledge base, queries must first be encoded to determine what texts to retrieve:

# pip install sentence-transformers

from sentence_transformers import SentenceTransformer

#some options: 'paraphrase-mpnet-base-v2', 'all-mpnet-base-v2'
encoder = SentenceTransformer('all-mpnet-base-v2')

def encode_query(query): #Query is a string
    #Preprocess the query
    query = query.lower()
    query = query.split()
    query = [word for word in query]
    query = ' '.join(query)
    
    #then encode the query
    vector = encoder.encode(query)
    return vector

Now, we define a function to streamline adding the retrieved context to the initial prompt:

import psycopg
URL = "postgresql://postgres:mysecretpassword@localhost:5432/postgres"

def add_context(prompt):
    search_vector = encode_query(prompt).tolist()
    # Construct prompt with the retrieved contexts included
    prompt_start = (
        "Answer the question based on the following context:\n\n"
    )
    prompt_end = (
        f"\n\nQuestion: {prompt}\nAnswer:"
    )
    with psycopg.connect(URL) as conn:
        cursor = conn.cursor()
        #Tune limit in accordance with how movie texts you want to give to the model
        cursor.execute("SELECT text FROM movies ORDER BY embedding <-> %s::real[] LIMIT 3;", 
                  (search_vector,),)
        result = cursor.fetchall()
    context = ""
    for r in result:
        context += r[0] + "\n\n"
    return prompt_start + context + prompt_end
Note

Recall the options for distance operators:

  • <-> for squared Euclidean distance
  • <#> for negative dot product
  • <=> for cosine distance

For definitions, please see the overview.

Now, let’s try the prompt again, with added context:

base_prompt_2 = "What is the release year and overall plot of Cat Napping?"

context_prompt_2 = add_context(base_prompt_2)
context_response_2 = complete(context_prompt_2)
print(context_response_2)
Output:

The release year of Cat Napping is 1951 and the overall plot involves Tom and Jerry fighting over a hammock, with Jerry ultimately getting revenge on Tom.

Now the model has given us a more accurate answer! We can even ask further questions:

base_prompt_3 = "In Cat Napping, what does Tom do to Jerry?"
print(complete(add_context(base_prompt_3)))
Output:

Tom tries to send Jerry sliding into a nearby pond by unhooking the hammock he is sleeping in. He also tries to whack the hammock to send Jerry flying into the air, but Jerry lands in a bird’s nest. Tom then picks Jerry up on a spatula and places him onto a walking army of ants, causing Jerry to wake up as he bumps his head on a sprinkler. Tom also tries to chase Jerry with a lawn mower and a baseball bat.

As we saw earlier in this post, LLMs do work very well by themselves, but can struggle with niche or specific questions, creating “hallucinations” that may not be noticed. The use of the knowledge base improves factuality and subsequently user trust in the generated outputs.

Further Applications

This method is not only for asking about movies, of course. Other applications could include extracting summaries of scientific papers (try this dataset!), or answering questions about company policies provided a database of company literature.

4 - Integration

Integrate pgvecto.rs with LangChain, LlamaIndex, and Citus distributed PostgreSQL.

4.1 - LangChain

This document describes the LangChain integration.

This document describes the LangChain integration.

LangChain is a framework for developing applications powered by language models. It is a set of tools that allows you to fine-tune, deploy, and use language models in production.

pgvecto.rs provides a LangChain integration that allows you to retrieve the most similar vectors in LangChain.

Install dependencies

Some dependencies are required to use the LangChain integration:

pip install langchain langchain-openai pgvecto-rs

You could start the postgres instance with pgvecto.rs extension in a docker container:

docker run \
  --name pgvecto-rs-demo \
  -e POSTGRES_PASSWORD=mysecretpassword \
  -p 5432:5432 \
  -d tensorchord/pgvecto-rs:pg16-v0.2.0

Then you can connect to the database using the psql command line tool. The default username is postgres, and the default password is mysecretpassword.

psql postgresql://postgres:mysecretpassword@localhost:5432/postgres

Run the following SQL to ensure the extension is enabled.

DROP EXTENSION IF EXISTS vectors;
CREATE EXTENSION vectors;

Create the database and load documents

We will show how to use pgvecto.rs in LangChain to retrieve the most similar vectors.

First, you need to create the text loader and the text splitter to split the text into chunks. We use the markdown file getting-started/overview.md as an example.

## Loading Environment Variables
from dotenv import load_dotenv

load_dotenv()

from langchain.docstore.document import Document
from langchain.text_splitter import CharacterTextSplitter
from langchain_community.document_loaders import TextLoader
from langchain_community.vectorstores.pgvecto_rs import PGVecto_rs
from langchain_openai import OpenAIEmbeddings


loader = TextLoader("./src/getting-started/overview.md")
documents = loader.load()
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
docs = text_splitter.split_documents(documents)

embeddings = OpenAIEmbeddings()

Then, we will create the PGVecto_rs instance and load the documents into the database.

## PGVecto.rs needs the connection string to the database.
## We will load it from the environment variables.
import os

PORT = os.getenv("DB_PORT", 5432)
HOST = os.getenv("DB_HOST", "localhost")
USER = os.getenv("DB_USER", "postgres")
PASS = os.getenv("DB_PASS", "mysecretpassword")
DB_NAME = os.getenv("DB_NAME", "postgres")

# Run tests with shell:
URL = "postgresql+psycopg://{username}:{password}@{host}:{port}/{db_name}".format(
    port=PORT,
    host=HOST,
    username=USER,
    password=PASS,
    db_name=DB_NAME,
)

# The pgvectors Module will try to create a table with the name of the collection.
# So, make sure that the collection name is unique and the user has the permission to create a table.

COLLECTION_NAME = "state_of_the_union_test"

db = PGVecto_rs.from_documents(
    embedding=embeddings,
    documents=docs,
    collection_name=COLLECTION_NAME,
    db_url=URL,
)

Query index

Finally, we can retrieve the most similar chunks in LangChain.

query = "What is pgvecto.rs"
docs_with_score = db.similarity_search_with_score(query)

for doc, score in docs_with_score:
    print("-" * 80)
    print("Score: ", score)
    print(doc.page_content)
    print("-" * 80)

The output is:

Created a chunk of size 1181, which is longer than the specified 1000
--------------------------------------------------------------------------------
Score:  0.25059962
# Overview

An introduction to the pgvecto.rs.

## What is pgvecto.rs

pgvecto.rs is a Postgres extension that provides vector similarity search functions. It is written in Rust and based on [pgrx](https://github.com/tcdi/pgrx). It is currently in the beta status, we invite you to try it out in production and provide us with feedback. Read more at [📝our launch blog](https://modelz.ai/blog/pgvecto-rs).

## Why use pgvecto.rs
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Score:  0.29536954
- 💃 **Easy to use**: pgvecto.rs is a Postgres extension, which means that you can use it directly within your existing database. This makes it easy to integrate into your existing workflows and applications.
- 🔗 **Async indexing**: pgvecto.rs's index is asynchronously constructed by the background threads and does not block insertions and always ready for new queries.
- 🥅 **Filtering**: pgvecto.rs supports filtering. You can set conditions when searching or retrieving points. This is the missing feature of other postgres extensions.
- 🧮 **Quantization**: pgvecto.rs supports scalar quantization and product quantization up to 64x.
- 🦀 **Rewrite in Rust**: Rust's strict compile-time checks ensure memory safety, reducing the risk of bugs and security issues commonly associated with C extensions.

## Comparison with pgvector
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Score:  0.35845917
More details at [📝`pgvecto.rs` vs. pgvector](../faqs/comparison-pgvector.md).

## Quick start

For new users, we recommend using the [Docker image](https://hub.docker.com/r/tensorchord/pgvecto-rs) to get started quickly.
...

Initialize from an existing database

Above, we created a vector store from scratch. However, often times we want to work with an existing vector store. In order to do that, we can initialize it directly.

db = PGVecto_rs(
    embedding=embeddings,
    collection_name=COLLECTION_NAME,
    # OpenAI embedding has 1536 dimensions.
    dimension=1536,
    db_url=URL,
)

Then, we can add vectors to the store and query:

db.add_documents([Document(page_content="foo")])
docs_with_score = db.similarity_search_with_score("foo")
print(docs_with_score[0])

The output is:

(Document(page_content='foo'), 0.0)

4.2 - LlamaIndex

LlamaIndex is a simple, flexible data framework for connecting custom data sources to large language models (LLMs).

LlamaIndex is a simple, flexible data framework for connecting custom data sources to large language models (LLMs).

pgvecto.rs provides a LlamaIndex integration that allows you to retrieve the most similar vectors.

Install dependencies

Some dependencies are required to use the LlamaIndex integration:

pip install llama-index "pgvecto_rs[sdk]"

You could start the postgres instance with pgvecto.rs extension in a docker container:

docker run \
  --name pgvecto-rs-demo \
  -e POSTGRES_PASSWORD=mysecretpassword \
  -p 5432:5432 \
  -d tensorchord/pgvecto-rs:pg16-v0.2.0

Then you can connect to the database using the psql command line tool. The default username is postgres, and the default password is mysecretpassword.

psql postgresql://postgres:mysecretpassword@localhost:5432/postgres

Run the following SQL to ensure the extension is enabled.

DROP EXTENSION IF EXISTS vectors;
CREATE EXTENSION vectors;

Create the database and load documents

First, create the text loader and the text splitter to split the text into chunks. We use the markdown file getting-started/overview.md as an example.

import logging
import os
import sys

logging.basicConfig(stream=sys.stdout, level=logging.INFO)
logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))

from pgvecto_rs.sdk import PGVectoRs

URL = "postgresql+psycopg://{username}:{password}@{host}:{port}/{db_name}".format(
    port=os.getenv("DB_PORT", "5432"),
    host=os.getenv("DB_HOST", "localhost"),
    username=os.getenv("DB_USER", "postgres"),
    password=os.getenv("DB_PASS", "mysecretpassword"),
    db_name=os.getenv("DB_NAME", "postgres"),
)

client = PGVectoRs(
    db_url=URL,
    collection_name="example",
    dimension=1536,  # Using OpenAI’s text-embedding-ada-002
)

Then setup the OpenAI API key and load the documents into the database.

import os

os.environ["OPENAI_API_KEY"] = "sk-..."

from llama_index import SimpleDirectoryReader, VectorStoreIndex
from llama_index.vector_stores import PGVectoRsStore

# load documents
documents = SimpleDirectoryReader("./src/getting-started/overview.md").load_data()

# initialize without metadata filter
from llama_index.storage.storage_context import StorageContext

vector_store = PGVectoRsStore(client=client)
storage_context = StorageContext.from_defaults(vector_store=vector_store)
index = VectorStoreIndex.from_documents(
    documents, storage_context=storage_context
)

Query index

Finally, we can retrieve the most similar chunks.

# set Logging to DEBUG for more detailed outputs
query_engine = index.as_query_engine()
response = query_engine.query("What did the author do growing up?")

4.3 - Citus

citus is a PostgreSQL extension that turns Postgres into a distributed database. pgvecto.rs works well with citus Here are the steps to enable vector search in

citus is a PostgreSQL extension that turns Postgres into a distributed database. pgvecto.rs works well with citus Here are the steps to enable vector search in a distributed PostgreSQL instance.

citus can works on Single-Node and Multi-Node, both of which are compatible with pgvecto.rs natively.

Single-Node Citus

First, prepare the dockerfile with pgvecto.rs and citus installed and attach them to PostgreSQL.

Details

citus must be placed first in shared_preload_libraries due to its restrictions.

FROM tensorchord/pgvecto-rs-binary:pg16-v0.3.0-amd64 as binary

FROM postgres:16
COPY --from=binary /pgvecto-rs-binary-release.deb /tmp/vectors.deb
RUN apt-get update && apt-get install -y curl
RUN apt-get install -y /tmp/vectors.deb && rm -f /tmp/vectors.deb
RUN curl https://install.citusdata.com/community/deb.sh -o deb.sh
RUN bash deb.sh 
RUN apt-get install -y postgresql-16-citus-12.1

CMD ["postgres", "-c" ,"shared_preload_libraries=citus,vectors.so", "-c", "search_path=\"$user\", public, vectors"]

Then we will build a Docker image called citus-vector:16 locally.

Warning

Don’t use POSTGRES_HOST_AUTH_METHOD: "trust" in production, see citus notes about Increasing Worker Security for detailed authentication configure.

docker build -t citus-vector:16 .
docker run --name citus-single -e POSTGRES_HOST_AUTH_METHOD=trust -p 5432:5432 -d citus-vector:16

To enable the essential extensions, connect our container with psql and execute SQL command.

-- psql -h 127.0.0.1 -d postgres -U postgres -w
CREATE EXTENSION IF NOT EXISTS citus;
CREATE EXTENSION IF NOT EXISTS vectors;

Once the extensions are created, you can create a simple table, then transform it into a distributed table.

CREATE TABLE items (id bigserial, embedding vector(3), category_id bigint, PRIMARY KEY (id, category_id));
SET citus.shard_count = 4;
SELECT create_distributed_table('items', 'category_id');

The index build and vector search is as same as before. Tables and indexes are distributed to all shards/workers automatically.

INSERT INTO items (embedding, category_id)
SELECT '[1, 1, 1]'::vector, FLOOR(random() * 100) FROM generate_series(1, 1000)
UNION ALL
SELECT '[1, 2, 3]'::vector, FLOOR(random() * 100) FROM generate_series(1, 1000)
UNION ALL
SELECT '[3, 2, 1]'::vector, FLOOR(random() * 100) FROM generate_series(1, 1000);
CREATE INDEX ON items USING vectors (embedding vector_cos_ops) WITH (options = "[indexing.hnsw]");
SELECT id FROM items ORDER BY embedding <=> '[1, 1, 1]' LIMIT 100;

Multi-Node Citus

You can use the same Dockerfile and image for Multi-Node citus as for Single-Node Citus. In this example, we will start a cluster of 1 coordinator node and 2 worker nodes with docker compose.

Warning

Don’t use POSTGRES_HOST_AUTH_METHOD: "trust" in production, see citus notes about Increasing Worker Security for detailed authentication configure.

services:
  main:
    container_name: main
    image: 'citus-vector:16'
    ports:
      - "5432:5432"
    environment:
      POSTGRES_HOST_AUTH_METHOD: "trust"
  worker1:
    container_name: worker1
    image: 'citus-vector:16'
    ports:
      - "5431:5432"
    environment:
      POSTGRES_HOST_AUTH_METHOD: "trust"
  worker2:
    container_name: worker2
    image: 'citus-vector:16'
    ports:
      - "5430:5432"
    environment:
      POSTGRES_HOST_AUTH_METHOD: "trust"

Now start all containers with docker-compose. Each container is a citus node, with 1 coordinator(main) and 2 workers.

docker compose up -d

As before, you’ll want to enable the extensions, but make sure you run the command on all nodes.

psql -h 127.0.0.1 -d postgres -U postgres -w -c 'CREATE EXTENSION IF NOT EXISTS citus;CREATE EXTENSION IF NOT EXISTS vectors;'
psql -h 127.0.0.1 -d postgres -U postgres -w -p 5431 -c 'CREATE EXTENSION IF NOT EXISTS citus;CREATE EXTENSION IF NOT EXISTS vectors;'
psql -h 127.0.0.1 -d postgres -U postgres -w -p 5430 -c 'CREATE EXTENSION IF NOT EXISTS citus;CREATE EXTENSION IF NOT EXISTS vectors;'

To inform the coordinator about its workers, connect to the coordinator node with psql and register the two workers.

-- psql -h 127.0.0.1 -d postgres -U postgres -w
SELECT citus_set_coordinator_host('main', 5432);
SELECT * from citus_add_node('worker1', 5432);
SELECT * from citus_add_node('worker2', 5432);

Finally, you can perform vector queries in the same way as in Single-Node Citus.

CREATE TABLE items (id bigserial, embedding vector(3), category_id bigint, PRIMARY KEY (id, category_id));
SET citus.shard_count = 4;
SELECT create_distributed_table('items', 'category_id');
INSERT INTO items (embedding, category_id) 
SELECT '[1, 1, 1]'::vector, FLOOR(random() * 100) FROM generate_series(1, 1000)
UNION ALL
SELECT '[1, 2, 3]'::vector, FLOOR(random() * 100) FROM generate_series(1, 1000)
UNION ALL
SELECT '[3, 2, 1]'::vector, FLOOR(random() * 100) FROM generate_series(1, 1000);
CREATE INDEX ON items USING vectors (embedding vector_cos_ops) WITH (options = "[indexing.hnsw]");
SELECT id FROM items ORDER BY embedding <-> '[1, 1, 1]' LIMIT 100;

Monitor

If you’re using a citus distributed table, the pg_vector_index_stat view on the coordinator will be empty. Since all the indexes are created at the workers actually, we can only inspect the vector status by distributing this view to the workers.

SELECT * from run_command_on_workers($$ SELECT (to_json(array_agg(pg_vector_index_stat .*))) FROM pg_vector_index_stat $$);

5 - Administration

Configuration, upgrading, logical replication, FDW, Kubernetes, and migration for pgvecto.rs.

5.1 - Configuration

By default, you cannot capture all pgvecto.rs logs. pgvecto.rs starts a background worker process for indexing, and it prints logs to standard error. To capture

Logging

By default, you cannot capture all pgvecto.rs logs. pgvecto.rs starts a background worker process for indexing, and it prints logs to standard error. To capture them, you need to set logging_collector to on. You can get more information from PostgreSQL document about logging collector.

You can set logging_collector to on with the following command:

psql -U postgres -c 'ALTER SYSTEM SET logging_collector = on;'
# You need restart the PostgreSQL cluster to take effects.
sudo systemctl restart postgresql.service   # for pgvecto.rs running with systemd
docker restart pgvecto-rs-demo  # for pgvecto.rs running in docker

Index configuration

since v0.3.0

For each index, there are a number of configuration items that are variable. Unlike options, they support real-time modification after the index has been created. These items can be configured by function alter_vector_index.

-- alter_vector_index(dim: OID, key: TEXT, value: TEXT)
SELECT alter_vector_index('index_name'::regclass::oid, 'configuration_name', 'value');

Here are all the available configurations.

Threads for backend optimizing

By default, each vector index is optimized by only one thread at the backend. However, if you have multiple idle CPUs and want to take advantage of them, you can increase the number of threads.

Set the key argument to optimizing.threads and the value argument to the desired number of threads.

-- alter_vector_index(dim: OID, key = 'optimizing.threads', value: TEXT as INT[1-65535])
SELECT alter_vector_index('table_col_idx'::regclass::oid, 'optimizing.threads', '4');

5.2 - Upgrading

If you update the extension to a new version, let’s say it’s a.b.c. This command helps you to update the schema of extension:

General Steps

If you update the extension to a new version, let’s say it’s a.b.c. This command helps you to update the schema of extension:

ALTER EXTENSION vectors UPDATE;
-- or
ALTER EXTENSION vectors UPDATE TO 'a.b.c';

If you’re upgrading from 0.1.x, please read Upgrading from 0.1.x.

If you’re upgrading to 0.4.0, please read Upgrading to 0.4.0.

Execute the following SQL. It helps you do some maintenance work.

SELECT pgvectors_upgrade();

You need to restart PostgreSQL to take effects.

You could check the status of all vector indexes in this command:

SELECT
        I.relname AS indexname
    FROM pg_index X JOIN
         pg_class I ON I.oid = X.indexrelid JOIN
         pg_am A ON A.oid = I.relam
    WHERE A.amname = 'vectors';

Let’s assume the output is:

 indexname
-------------
 t_val_idx_1
 t_val_idx_2

Then you could reindex all vector indexes.

REINDEX INDEX t_val_idx_1;
REINDEX INDEX t_val_idx_2;

Upgrade to 0.4.0

You may get this error if some indexes were created before upgrading pgvecto.rs to 0.4.0:

ERROR:  index public.this_is_index depends on operator class vector_cos_ops for access method vectors
index public.this_is_index depends on operator class vector_cos_ops for access method vectors cannot drop operator class vector_cos_ops for access method vectors because other objects depend on it 

ERROR:  cannot drop operator class vector_cos_ops for access method vectors because other objects depend on it
SQL state: 2BP01
Detail: index public.that_is_also_index depends on operator class vector_cos_ops for access method vectors
index public.that_is_also_index depends on operator class vector_cos_ops for access method vectors
Hint: Use DROP ... CASCADE to drop the dependent objects too.

You can simply drop these indexes, and then upgrade the extension:

DROP INDEX this_is_index;
DROP INDEX that_is_also_index;

ALTER EXTENSION vectors UPDATE;

-- CREATE INDEX this_is_index ...
-- CREATE INDEX that_is_also_index ...

Upgrade from 0.1.x

You need to follow these steps to make ALTER EXTENSION vectors UPDATE work.

Let’s assume your pgvecto.rs version is 0.1.x (replace x with a number).

CREATE SCHEMA IF NOT EXISTS vectors;
UPDATE pg_catalog.pg_extension SET extversion = '0.1.x' where extname = 'vectors';
UPDATE pg_catalog.pg_extension SET extrelocatable = true where extname = 'vectors';
ALTER EXTENSION vectors SET SCHEMA vectors;
UPDATE pg_catalog.pg_extension SET extrelocatable = false where extname = 'vectors';
ALTER EXTENSION vectors UPDATE;
Tip

pgvecto.rs is installed in schema vectors from 0.2.0, you may need to set search_path for the database.

5.3 - Logical replication

Logical replication is a feature in PostgreSQL that enables the replication of individual database objects or a subset of data from one PostgreSQL database to a

Logical replication is a feature in PostgreSQL that enables the replication of individual database objects or a subset of data from one PostgreSQL database to another.

With logical replication, you can selectively replicate specific tables, databases, or even specific rows based on predefined replication rules. This provides more flexibility compared to physical replication, where the entire database cluster is replicated. It allows you to design custom replication topologies and replicate only the data that is necessary for your use case.

We will show you how to use logical replication to replicate data from one PostgreSQL database to another.

Deploy source and target PostgreSQL clusters

$ echo 'version: "3.7"
services:
  source_db:
    image: tensorchord/pgvecto-rs:pg15-v0.2.0
    ports:
      - "5432:5432"
    environment:
      POSTGRES_PASSWORD: password
    command: >
      -c wal_level=logical
      -c shared_preload_libraries=vectors.so
    networks:
      localnet:

  target_db:
    image: tensorchord/pgvecto-rs:pg15-v0.2.0
    ports:
      - "5433:5432"
    environment:
      POSTGRES_PASSWORD: password
    command: >
      -c wal_level=logical
      -c shared_preload_libraries=vectors.so
    networks:
      localnet:

networks:
  localnet:
' > docker-compose.yml

$ docker compose up -d

Create extension vectors in source database and target database.

# create extension vectors
$ psql -h <source_db_ip> -U postgres -p 5432 
DROP EXTENSION IF EXISTS "vectors";
CREATE EXTENSION "vectors";

$ psql -h <target_db_ip> -U postgres -p 5432
DROP EXTENSION IF EXISTS "vectors";
CREATE EXTENSION "vectors";

Configure logical replication

Now, we can set logical replication between source database and target database.

Prepare test data

We need to create a table named test with a column named embedding of type vector(10) in source database and target database. Then we create an index on the embedding column of the test table in source database and target database. Finally, we insert data into the source database.

Create the table

DROP TABLE IF EXISTS test;
CREATE TABLE test (id integer PRIMARY KEY, embedding vector(10) NOT NULL);

Create the index

We create an index on the embedding column of the test table in source database and target database. The index type is flat, it is a brute force algorithm. We choose vector_l2_ops squared Euclidean distance to measure the distance between vectors. Another index type and distance function can be found in here.

CREATE INDEX ON test USING vectors (embedding vector_l2_ops) WITH (options = "[indexing.flat]");

Insert data

We only need to insert data into the source database. The data will be synchronized to the target database.

INSERT INTO test
SELECT i, ARRAY[random(), random(),random(),random(),random(),random(),random(),random(),random()]::real[] 
FROM generate_series(1, 100) i;

In source database, now you can search for the nearest neighbor of a vector in the embedding column of the test table.

postgres=# SELECT id FROM test ORDER BY embedding <-> '[0.40671515, 0.24202824, 0.37059402, 0.50316447, 0.10779921, 0.80774295, 0.8879849, 0.31292745, 0.05584943, 0.8738258]' LIMIT 10;
 id 
----
 82
 69
 80
 22
 31
 20
 85
 97
 72
(10 rows)

If you query the target database, you will get the noting:

SELECT id FROM test ORDER BY embedding <-> '[0.40671515, 0.24202824, 0.37059402, 0.50316447, 0.10779921, 0.80774295, 0.8879849, 0.31292745, 0.05584943, 0.8738258]' LIMIT 10;
 id 
----
(0 rows)

Set up logical replication

To simplify the process of setting up logical replication between two PostgreSQL databases, we will use pg-easy-replicate.

We use pg-easy-replicate to set up logical replication between source database and target database.

# get the network name
$ docker network ls | grep  pg
8631dd77129b   pg_regress_localnet   bridge    local

# get source and target container ip
$ docker inspect <container_id> | grep IP

$ docker run -e SOURCE_DB_URL="postgres://postgres:password@<source_db_ip>:5432/postgres"    -e TARGET_DB_URL="postgres://postgres:password@<target_db_ip>:5432/postgres"   -it --rm --network=<network> shayonj/pg_easy_replicate:latest   pg_easy_replicate config_check
/usr/bin/pg_dump
✅ Config is looking good.

Bootstrap

Every sync will need to be bootstrapped before you can set up the sync between two databases. Bootstrap creates a new super user to perform the orchestration required during the rest of the process. It also creates some internal metadata tables for record keeping.

$ docker run -e SOURCE_DB_URL="postgres://postgres:password@<source_db_ip>:5432/postgres"    -e TARGET_DB_URL="postgres://postgres:password@<target_db_ip>:5432/postgres"   -it --rm --network=pg_regress_localnet shayonj/pg_easy_replicate:latest pg_easy_replicate bootstrap --group-name database-cluster-1 --copy-schema

Start sync

Once the bootstrap is complete, you can start the sync. Starting the sync sets up the publication, subscription and performs other minor housekeeping things.

$ docker run -e SOURCE_DB_URL="postgres://postgres:[email protected]:5432/postgres"    -e TARGET_DB_URL="postgres://postgres:[email protected]:5432/postgres"   -it --rm --network=pg_regress_localnet shayonj/pg_easy_replicate:latest pg_easy_replicate start_sync --group-name database-cluster-1 

Test logical replication

Query

Now we can query the target database to get the nearest neighbor of a vector in the embedding column of the test table. The result is the same as the source database.

postgres=# SELECT id FROM test ORDER BY embedding <-> '[0.40671515, 0.24202824, 0.37059402, 0.50316447, 0.10779921, 0.80774295, 0.8879849, 0.31292745, 0.05584943, 0.8738258]' LIMIT 10;
 id 
----
 82
 69
 80
 22
 31
 20
 85
 97
 72
(10 rows)

Update index

If you insert data into the source database, the data will be synchronized to the target database. Insert data into the source database:

INSERT INTO test
SELECT i, ARRAY[random(), random(),random(),random(),random(),random(),random(),random(),random()]::real[] 
FROM generate_series(101, 200) i;

Query in the source database:

postgres=# SELECT id FROM test ORDER BY embedding <-> '[0.40671515, 0.24202824, 0.37059402, 0.50316447, 0.10779921, 0.80774295, 0.8879849, 0.31292745, 0.05584943, 0.8738258]' LIMIT 10;
id  
-----
  82
 130
 145
  69
  80
 200
  22
  31
 182
 (10 rows)

Query in the target database, it will return the same result:

postgres=# SELECT id FROM test ORDER BY embedding <-> '[0.40671515, 0.24202824, 0.37059402, 0.50316447, 0.10779921, 0.80774295, 0.8879849, 0.31292745, 0.05584943, 0.8738258]' LIMIT 10;
id  
-----
  82
 130
 145
  69
  80
 200
  22
  31
 182
 (10 rows)

5.4 - Foreign data wrapper (FDW)

Foreign data wrapper(FDW) is an extension in PostgreSQL that allows you to access and query data stored in remote databases as if they were local tables. FDW pr

Foreign data wrapper(FDW) is an extension in PostgreSQL that allows you to access and query data stored in remote databases as if they were local tables. FDW provides a way to integrate and interact with different data sources.

With the FDW extension, you can define foreign tables in your local PostgreSQL database that mirror the structure of tables in remote databases. These foreign tables act as proxies or wrappers for the remote data, enabling you to perform SELECT, INSERT, UPDATE, and DELETE operations on them, just like regular tables in PostgreSQL.

In this tutorial, we will use the postgres_fdw module, which includes the foreign-data wrapper. This wrapper can be used to access data stored in external PostgreSQL servers. This article will explain how to use postgres_fdw to access index data in a foreign PostgreSQL cluster that already has the pgvecto.rs extension installed.

Deploy local and foreign PostgreSQL clusters

In this tutorial, we will use docker compose to deploy two PostgreSQL clusters.

$ echo 'version: "3.7"
services:
  local_db:
    image: tensorchord/pgvecto-rs:pg15-v0.2.0
    ports:
      - "5432:5432"
    environment:
      POSTGRES_PASSWORD: password
    command: >
      -c shared_preload_libraries=vectors.so
    networks:
      localnet:

  foreign_db:
    image: tensorchord/pgvecto-rs:pg15-v0.2.0
    ports:
      - "5433:5432"
    environment:
      POSTGRES_PASSWORD: password
    command: >
      -c shared_preload_libraries=vectors.so
    networks:
      localnet:

networks:
  localnet:
' > docker-compose.yml

$ docker compose up -d
# create extension postgres_fdw in local db
$ psql -h <local_db_ip> -U postgres -p 5432 
DROP EXTENSION IF EXISTS postgres_fdw;
CREATE EXTENSION postgres_fdw;

# create extension vectors in foreign db
$ psql -h <foreign_db_ip> -U postgres -p 5432
DROP EXTENSION IF EXISTS "vectors";
CREATE EXTENSION "vectors";

Foreign database operations

First, we need create a table test and build an index on it in the foreign db. The test table has two columns: id and embedding. The embedding column is a vector column, and its type is vector(10). The id column is the primary key of the test table.

Create a table

DROP TABLE IF EXISTS test;
CREATE TABLE test (id integer PRIMARY KEY, embedding vector(10) NOT NULL);
INSERT INTO test SELECT i, ARRAY[random(),random(),random(),random(),random(),random(),random(),random(),random(),random()]::real[] FROM generate_series(1, 100) i;

Create a user

Create a user named fdw_user in foreign db, and grant SELECT, INSERT, UPDATE, DELETE privileges on table test to fdw_user.

CREATE USER fdw_user WITH ENCRYPTED PASSWORD 'secret';
GRANT SELECT,INSERT,UPDATE,DELETE ON TABLE test TO fdw_user;

Create the index

We create an index on the embedding column of the test table. The index type is flat, it is a brute force algorithm. We choose vector_l2_ops squared Euclidean distance to measure the distance between vectors. Another index type and distance function can be found in here.

CREATE INDEX ON test USING vectors (embedding vector_l2_ops) WITH (options = "[indexing.flat]");

Local database operations

In local database, we need to create a table local and a foreign server foreign_server. The local table has two columns: id and name. The id column is the primary key of the local table. The foreign_server is a foreign server, which is used to access the foreign db.

Create local table

DROP TABLE IF EXISTS local;
CREATE TABLE local (id integer PRIMARY KEY, name VARCHAR(50) NOT NULL);
INSERT INTO local (id, name) VALUES (1, 'terry'), (2, 'jason'), (3, 'curry');

create local user

Using superuser, execute the following statement in the local PostgreSQL database to create a regular user named local_user.

CREATE USER local_user WITH ENCRYPTED PASSWORD 'secret';

Create the foreign server

To create an external server using the CREATE SERVER statement, you need to specify the host, port, and database name of the remote database.

CREATE SERVER foreign_server
    FOREIGN DATA WRAPPER postgres_fdw
    OPTIONS (host '<foreign_db_ip>', port '5432', dbname 'postgres');

Create the user mapping

Use the CREATE USER MAPPING statement to create a mapping between remote users and local users, requiring the username and password of the remote user.

CREATE USER MAPPING FOR local_user
    SERVER foreign_server
    OPTIONS (user 'fdw_user', password 'secret');

Creat the foreign table

Use the CREATE FOREIGN TABLE statement to create a remote table. It is important to note that the types of each column should match those of the actual remote table, and it’s best to keep the column names consistent. Otherwise, you will need to use the column_name parameter to specify the column name in the remote table for each individual column.

CREATE FOREIGN TABLE foreign_test (id integer, embedding vector(10) NOT NULL)
    SERVER foreign_server
    OPTIONS (schema_name 'public', table_name 'test');

Authorization

Authorize the local_user:

  • Grant local_user the permission to use postgres_fdw.
  • Grant local_user the permission to use foreign server foreign_server.
  • Grant local_user the permission to access all tables in schema public.
GRANT USAGE ON FOREIGN DATA WRAPPER postgres_fdw TO local_user;
GRANT USAGE ON FOREIGN SERVER foreign_server TO local_user;
GRANT SELECT,INSERT,UPDATE,DELETE ON ALL TABLES IN SCHEMA public TO local_user;

Query

Now we can use join query to access the foreign table in local db.

$ psql -h <local_db_ip> -U local_user -p 5432 -d postgres
SELECT l.id, l.name FROM local l LEFT JOIN foreign_test f on l.id = f.id ORDER BY f.embedding <-> '[0.40671515, 0.24202824, 0.37059402, 0.50316447, 0.10779921, 0.80774295, 0.8879849, 0.31292745, 0.05584943, 0.8738258]' LIMIT 10;
 id | name  
----+-------
  2 | jason
  3 | curry
  1 | terry


EXPLAIN SELECT l.id, l.name FROM local l LEFT JOIN foreign_test f on l.id = f.id ORDER BY f.embedding <-> '[0.40671515, 0.24202824, 0.37059402, 0.50316447, 0.10779921, 0.80774295, 0.8879849, 0.31292745, 0.05584943, 0.8738258]' LIMIT 10;
 Limit  (cost=209.63..209.65 rows=10 width=126)
   ->  Sort  (cost=209.63..213.04 rows=1365 width=126)
         Sort Key: ((f.embedding <-> '[0.40671515, 0.24202824, 0.37059402, 0.50316447, 0.10779921, 0.80774295, 0.8879849, 0.31292745, 0.05584943, 0.8738258]'::vector))
         ->  Hash Right Join  (cost=122.15..180.13 rows=1365 width=126)
               Hash Cond: (f.id = l.id)
               ->  Foreign Scan on foreign_test f  (cost=100.00..150.95 rows=1365 width=36)
               ->  Hash  (cost=15.40..15.40 rows=540 width=122)
                     ->  Seq Scan on local l  (cost=0.00..15.40 rows=540 width=122)

5.5 - Kubernetes

If you want to use pgvecto.rs in PostgreSQL which is running in Kubernetes, we provide a tutorial to help you. In this tutorial, we will use CloudNative-PG to d

If you want to use pgvecto.rs in PostgreSQL which is running in Kubernetes, we provide a tutorial to help you. In this tutorial, we will use CloudNative-PG to deploy PostgreSQL cluster in Kubernetes.

Requirements

Name Version
helm >= 2.4.1
kubectl >= 1.14
Kubernetes >= 1.24.0

Install CloudNative-PG

We use CloudNative-PG (cnpg) to deploy PostgreSQL cluster in Kubernetes. You can install it by helm.

$ sudo helm repo add cnpg https://cloudnative-pg.github.io/charts
$ sudo helm upgrade --install cnpg   --namespace cnpg-system   --create-namespace   cnpg/cloudnative-pg
$ helm list --all-namespaces
NAME                    NAMESPACE       REVISION        UPDATED                                 STATUS          CHART                         APP VERSION
cnpg                    cnpg-system     1               2024-01-02 08:17:11.661297545 +0000 UTC deployed        cloudnative-pg-0.20.0         1.22.0     
$ kubectl get pod -n cnpg-system   
NAME                                   READY   STATUS    RESTARTS   AGE
cnpg-cloudnative-pg-67ff4f5f66-94m8d   1/1     Running   0          1m

Build PostgreSQL image with pgvecto.rs

We use the official CloudNative-PG docker image as the base image, and install pgvecto.rs in it. And you can also find pgvecto.rs version from releases.

# syntax=docker/dockerfile-upstream:master
ARG CNPG_TAG

FROM ghcr.io/cloudnative-pg/postgresql:$CNPG_TAG

ARG CNPG_TAG
ARG PGVECTORS_TAG
ARG TARGETARCH

# drop to root to install packages
USER root

# install pgvecto.rs
ADD https://github.com/tensorchord/pgvecto.rs/releases/download/$PGVECTORS_TAG/vectors-pg${CNPG_TAG%.*}_${PGVECTORS_TAG#"v"}_$TARGETARCH.deb ./pgvectors.deb
RUN apt install ./pgvectors.deb

USER postgres
$ docker build -t <your-postgresql-image-repo>:15 --build-arg CNPG_TAG=15 --build-arg PGVECTORS_TAG=v0.1.13   --build-arg TARGETARCH=amd64 .

Notice: PostgreSQL docker image tag must start with CNPG_TAG for cloudnative-pg to recognize the postgres version.

Create PostgreSQL cluster

We provide a sample yaml file to create a PostgreSQL cluster in Kubernetes. You can modify it according to your needs. You need pay attention to the following points:

  • Set shared_preload_libraries to load pgvecto.rs shared library. You can also set postInitApplicationSQL ALTER SYSTEM SET shared_preload_libraries = "vectors.so"; to load pgvecto.rs shared library.
  • Execute CREATE EXTENSION IF NOT EXISTS "vectors"; to create vectors extension.
apiVersion: v1
kind: Secret
metadata:
  name: tensorchord
type: kubernetes.io/basic-auth
data:
  password: dGVuc29yY2hvcmQ= # tensorchord 
  username: dGVuc29yY2hvcmQ= # tensorchord
---
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: pgvectors 
spec:
  instances: 1 
  bootstrap:
    initdb:
      database: tensorchord
      postInitApplicationSQL:
        - CREATE EXTENSION IF NOT EXISTS "vectors";
      owner: tensorchord
      secret:
        name: tensorchord
      dataChecksums: true
      encoding: 'UTF8'
  storage:
    size: 1Gi
  imageName: <your-postgresql-image-repo>:<pg-tag>
  postgresql:
    shared_preload_libraries: # load pgvecto.rs shared library
    - "vectors.so"

You can install cnpg kubectl plugin to manage your PostgreSQL cluster. Now we can check the status of the cluster.

$ sudo kubectl get pod
pgvectors-1                                                1/1     Running   0             3m54s

$ kubectl cnpg status pgvectors
Cluster Summary
Name:                pgvectors
Namespace:           default
System ID:           7323107530735296538
PostgreSQL Image:    docker.io/xieydd/pgvectors:15
Primary instance:    pgvectors-1
Primary start time:  2024-01-12 07:17:34 +0000 UTC (uptime 17m33s)
Status:              Cluster in healthy state 
Instances:           1
Ready instances:     1
Current Write LSN:   0/4000000 (Timeline: 1 - WAL File: 000000010000000000000003)

Certificates Status
Certificate Name       Expiration Date                Days Left Until Expiration
----------------       ---------------                --------------------------
pgvectors-ca           2024-04-11 07:12:00 +0000 UTC  89.98
pgvectors-replication  2024-04-11 07:12:00 +0000 UTC  89.98
pgvectors-server       2024-04-11 07:12:00 +0000 UTC  89.98

Continuous Backup status
Not configured

Physical backups
No running physical backups found

Streaming Replication status
Not configured

Unmanaged Replication Slot Status
No unmanaged replication slots found

Managed roles status
No roles managed

Tablespaces status
No managed tablespaces

Instances status
Name         Database Size  Current LSN  Replication role  Status  QoS         Manager Version  Node
----         -------------  -----------  ----------------  ------  ---         ---------------  ----
pgvectors-1  29 MB          0/4000000    Primary           OK      BestEffort  1.22.0           kind-control-plane

Notice: This article just provides a minimal configuration, not include logical replication, streaming logical replication, backup and restore, etc. We will provide those in another article. You can also refer to CloudNative-PG for more Cluster configuration.

Connect to PostgreSQL cluster

You can use kubectl port-forward to connect to PostgreSQL cluster.

$ kubectl port-forward services/pgvectors-rw 5432:5432
$ psql -h 127.0.0.1 -d tensorchord -U tensorchord
tensorchord=> \dx
Name   | Version |   Schema   |                                         Description                                          
---------+---------+------------+----------------------------------------------------------------------------------------------
 plpgsql | 1.0     | pg_catalog | PL/pgSQL procedural language
 vectors | 0.1.13  | public     | vectors: Vector database plugin for Postgres, written in Rust, specifically designed for LLM

The vectors extension is installed successfully. You can try quick-start without installation.

5.6 - Migration

When migrating from pgvector to PGVecto.rs, it is possible that you may have a table with a column of vector type. An example of this would be:

outline: [2,3]

PGVecto.rs Migration Overview

From pgvector

Prerequisites

When migrating from pgvector to PGVecto.rs, it is possible that you may have a table with a column of vector type. An example of this would be:

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

In addition, some rows will be inserted into the table, as follows:

INSERT INTO items (embedding) VALUES ('[1,2,3]'), ('[4,5,6]');

In most cases, there will also be one or more vector indexes:

CREATE INDEX ON items USING hnsw (embedding vector_l2_ops);

This is a typical scenario when pgvector is used. If the following requirements are met, the migration can be initiated.

  • Please note that your pgvector extension is not installed in the vectors schema. This means that you can install pgvector and PGVecto.rs at the same time.

Steps to Migration

1. Install PGVecto.rs

Please follow the instructions provided to install PGVecto.rs and ensure that the extension is loaded by PostgreSQL.

CREATE EXTENSION vectors;

The two extensions, pgvector and PGVecto.rs, are now installed at the PostgreSQL.

To validate the install, please run \dx in psql. You will see that there is an extension called vector for pgvector and another called vectors for PGVecto.rs.

postgres=# \dx
                                                 List of installed extensions
  Name   | Version |   Schema   |                                         Description                                          
---------+---------+------------+----------------------------------------------------------------------------------------------
 plpgsql | 1.0     | pg_catalog | PL/pgSQL procedural language
 vector  | 0.7.2   | public     | vector data type and ivfflat and hnsw access methods
 vectors | 0.3.0   | vectors    | vectors: Vector database plugin for Postgres, written in Rust, specifically designed for LLM

Please note that there are some symbols with the same name in both extensions. This may cause confusion when doing the migration. To differentiate between the two, please verify that the search_path does not include the PGVecto.rs schema vectors.

SHOW search_path;
-- if it shows: `"$user", public` -> This is okay
-- if it shows: `"$user", public, vectors` -> Remove `vectors` by:
SET search_path = "$user, public";

2. Check existing indexes

For indexes created at vector columns, please retrieve the definition and record it in a convenient location. This information is necessary for the recovery of the indexes at a later stage.

postgres=# SELECT indexdef FROM pg_indexes WHERE tablename = 'items';
                                       indexdef                                        
---------------------------------------------------------------------------------------
 CREATE UNIQUE INDEX items_pkey ON public.items USING btree (id)
 CREATE INDEX items_embedding_idx ON public.items USING hnsw (embedding vector_l2_ops)
(2 rows)

3. Migrate vector columns

To migrate from a pgvector vector column, the first step is to create a PGVecto.rs vector column of the corresponding type. The subsequent step is to replicate the vector data from the original column to the new one.

Here provides the conversion path for each vector type of pgvector:

  • vector(pgvector) -> real[] -> vector(PGVecto.rs)
  • halfvec -> vector(pgvector) -> real[] -> vector(PGVecto.rs) -> vecf16
  • bit -> text[] -> real[] -> vector(PGVecto.rs) -> bvector

This vector type is not supported for migration yet:

  • sparsevec

Method 1: Conversion after validation

With this method, you can check the correctness of the vector data during the migration process, and you do not need to worry about the indexes that have been created on the vector columns, as they will be automatically deleted. However, it requires additional memory to store the vectors before and after the migration.

Warning

This step will cause some downtime, due to:

  • a short ACCESS EXCLUSIVE lock by ALTER TABLE ADD COLUMN
  • a much longer ROW EXCLUSIVE lock by UPDATE
  • a short ACCESS EXCLUSIVE lock by ALTER TABLE DROP COLUMN
-- From vector type
ALTER TABLE items ADD COLUMN migrate_embedding vectors.vector(3);
UPDATE items SET migrate_embedding = embedding::real[]::vectors.vector;

-- From halfvec type
ALTER TABLE items ADD COLUMN migrate_embedding vectors.vecf16(3);
UPDATE items SET migrate_embedding = embedding::vector::real[]::vectors.vector::vectors.vecf16;

-- For bit type
ALTER TABLE items ADD COLUMN migrate_embedding vectors.bvector(3);
UPDATE items SET migrate_embedding = string_to_array(embedding::text, NULL)::real[]::vectors.vector::vectors.bvector;

Once the data has been copied, you can check the data by SELECT and replace the existing column with the new one.

ALTER TABLE items DROP COLUMN embedding;
ALTER TABLE items RENAME COLUMN migrate_embedding TO embedding;

Method 2: Conversion without additional memory

With this method, the migration will be completed in one command and no additional memory is required. However, you need to delete the index on the vector column in advance.

Warning

This step will cause some downtime, due to:

  • a long ACCESS EXCLUSIVE lock by ALTER TABLE ALTER COLUMN
-- The name of index is from step 2
DROP INDEX IF EXISTS items_embedding_idx;

-- From vector type
ALTER TABLE items ALTER COLUMN embedding TYPE vectors.vector(3) USING embedding::real[]::vectors.vector;

-- From halfvec type
ALTER TABLE items ALTER COLUMN embedding TYPE vectors.vecf16(3) USING embedding::vector::real[]::vectors.vector::vectors.vecf16;

-- For bit type
ALTER TABLE items ALTER COLUMN embedding TYPE vectors.bvector(3) USING string_to_array(embedding::text, NULL)::real[]::vectors.vector::vectors.bvector;

4. Recreate index

To recreate index on vector columns for PGVecto.rs , please enable the compatibility mode and create it like before.

SET vectors.pgvector_compatibility=on;
-- Execute the command you get in step 2
CREATE INDEX items_embedding_idx ON public.items USING hnsw (embedding vector_l2_ops);

For other vector types, such as halfvec, sparsevec, and bit, please refer to the indexing. Currently, pgvector_compatibility mode is not supported for these types.

5. Clean the environment

You may now safely remove the pgvector extension if it is no longer required.

DROP extension vector;

In the event that the pgvector extension is removed, it is advisable to reset the search_path in order to ensure that symbols in PGVecto.rs are accessible globally.

SET search_path = "$user, public, vectors";
-- or globally
ALTER SYSTEM SET search_path = "$user, public, vectors";

Verify the result

Once the migration task is executed, you can verify the new columns by SELECT SQL.

SELECT embedding, pg_typeof("embedding") from items limit 3;

-- embedding |   pg_typeof    
-------------+----------------
-- [1, 2, 3] | vectors.vector
-- [4, 5, 6] | vectors.vector

6 - FAQs

Frequently asked questions, comparisons with pgvector and specialized vector databases, and benchmarks.

6.1 - General FAQ

In most cases, this is due to a missing schema search path or a lack of permissions.
Warning

We strongly suggest you explore our new VectorChord implementation, offering enhanced stability and performance. Users are advised to transition to VectorChord. Refer to the migration guide for assistance.

The vector type or operator class is not found

In most cases, this is due to a missing schema search path or a lack of permissions.

Please go through this checklist:

  1. Run \dx with psql or SELECT * FROM pg_extension; and check if the extension exists.
postgres=# \dx
                                                    List of installed extensions
  Name   |    Version    |   Schema   |                                         Description                                          
---------+---------------+------------+----------------------------------------------------------------------------------------------
 vectors | 0.3.0         | vectors    | vectors: Vector database plugin for Postgres, written in Rust, specifically designed for LLM

If not, install it with CREATE EXTENSION vectors;

  1. Run SHOW search_path; and check if it contains vectors.
postgres=# SHOW search_path;
       search_path        
--------------------------
 "$user", public, vectors

If not, set it with SET search_path="$user", public, vectors;

  1. If pgvecto.rs is installed by another superuser

Run \dn with psql or SELECT nspname FROM pg_catalog.pg_namespace; and check if the schema vectors exists.

postgres=# SELECT current_user;
 current_user 
--------------
 tensorchord

postgres=# \dn
       List of schemas
  Name   |       Owner       
---------+-------------------
 public  | pg_database_owner
 vectors | postgres

If not, log in as the superuser and grant schema permissions to current_user:

GRANT ALL ON SCHEMA vectors to tensorchord;

6.2 - Comparison with pgvector

Original post: pgvector vs. pgvecto.rs in 2024: A Comprehensive Comparison for Vector Search in PostgreSQL

Original post: pgvector vs. pgvecto.rs in 2024: A Comprehensive Comparison for Vector Search in PostgreSQL

TL; DR: Please navigate directly to the final section that contains a table showcasing the main distinctions.

Search is essential for a database. It plays a pivotal role in facilitating efficient data retrieval, enabling users to quickly find and access the information they need. We will demonstrate their performance and highlight the various features pgvecto.rs and pgvector offer.

Filtering

While both pgvector and pgvecto.rs provide support for vector similarity search in PostgreSQL, pgvecto.rs goes a step further by introducing VBASE method from OSDI 2023.

Real-world workloads often involve a combination of vector search and relational queries. While vector search is powerful for similarity matching and retrieval of similar vectors, relational queries allow you to perform complex joins, filters, and aggregations on structured data.

pgvecto.rs is designed to perform efficiently in scenarios that involve Single-Vector TopK + Filter + Join.

We utilize the laion-768-5m-ip-probability dataset for benchmarking purposes due to the absence of a comprehensive relational benchmark. The dataset is derived from LAION 2B images. It contains 5,000,000 vectors, 10,000 queries.

The dataset includes a probability column that stores random floating-point values generated from a uniform distribution between 0 and 1. The ratio of 0.01 means that each query covers 1% (or 0.01 times) of the dataset, allowing for focused analysis.

We present the recall, latency (in milliseconds), and RPS (requests per second) for various probability ranges while keeping the ef_search constant. The ef_search parameter represents the size of the list utilized during k-NN (k-Nearest Neighbors) searches, determining the trade-off between search accuracy and query processing time.

pgvecto.rs, when used with VBASE, consistently yields improved recall, particularly when working with low probability values.

Sparse vectors are high-dimensional vectors that contain few non-zero values. They are suitable for traditional information retrieval use cases. For example, a vector with 32,000 dimensions but only 2 non-zero elements is a typical sparse vector.

S[1×32000]=[000.0150000⋯0.5430] S[1\times 32000]=\left[ \begin{matrix} 0 & 0 & 0.015 & 0 & 0 & 0 & 0 & \cdots & 0.543 & 0 \end{matrix} \right]

Dense vectors are embeddings from neural networks. They are generated by text embedding models and have most or all elements non-zero. They have fewer dimensions, such as 256 or 1536, much less than sparse vectors.

D[1×256]=[0.3421.7740.0870.8700.001⋯0.5430.999] D[1\times 256]=\left[ \begin{matrix} 0.342 & 1.774 & 0.087 & 0.870 & 0.001 & \cdots & 0.543 & 0.999 \end{matrix} \right]

pgvecto.rs supports both dense vector search and sparse vector search. It provides the ability to use the svector data type to build sparse vector indexes and perform searches on them.

CREATE TABLE items (
  id bigserial PRIMARY KEY,
  embedding svector(10) NOT NULL
);

INSERT INTO items (embedding) VALUES ('[0.1,0,0,0,0,0,0,0,0,0]');

CREATE INDEX your_index_name ON items USING vectors (embedding svector_l2_ops);

SELECT * FROM items ORDER BY embedding <-> '[0.3,0,0,0,0,0,0,0,0,0]' LIMIT 1;

Vector dimensions

The pgvecto.rs extension provides support for up to 65535 dimensions, whereas the pgvector extension supports a maximum of 2000 dimensions. This difference in dimensionality support allows pgvecto.rs to handle vectors from OpenAI’s latest embedding model directly.

Single Instruction/Multiple Data (SIMD)

pgvecto.rs utilizes the SIMD (Single Instruction, Multiple Data) instructions available on the user’s machine, pgvector relies on the compiler to generate SIMD code at compile time and cannot choose a faster instruction set at runtime. It makes search in pgvecto.rs faster due to the faster calculation of vector distances.

Data Types

Binary Vector

The bvector type is a binary vector type in pgvecto.rs. It represents a binary vector, which is a vector where each component can take on two possible values, typically 0 and 1.

CREATE TABLE items (
  id bigserial PRIMARY KEY,
  embedding bvector(3) NOT NULL
);

INSERT INTO items (embedding) VALUES ('[1,0,1]'), ('[0,1,0]');

CREATE INDEX your_index_name ON items USING vectors (embedding bvector_l2_ops);

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

Binary vectors offer significant advantages in specific embedding models like the OpenAI embedding model by effectively reducing memory usage while preserving a satisfactory level of accuracy.

Here are some performance benchmarks for the bvector type. We use the dbpedia-entities-openai3-text-embedding-3-large-3072-1M dataset for the benchmark. The VM is n2-standard-8 (8 vCPUs, 32 GB memory) on Google Cloud.

We upsert 1M binary vectors into the table and then run a KNN query for each embedding. It only takes about 600MB memory to index 1M binary vectors, while the vector type takes about 18GB memory to index the same number of vectors. The bvector’s accuracy exceeds 95% if we adopt adaptive retrieval.

bvector

FP16/INT8

Besides binary vectors, pgvecto.rs also provides support for FP16 (16-bit floating point) and INT8 (8-bit integer) data types.

Indexing

pgvecto.rs takes a different approach compared to pgvector. It handles the storage and memory of indexes separately from PostgreSQL, instead of relying on the native storage engine of PostgreSQL like pgvector does.

It’s a design tradeoff. During the initial stages of our development, we conducted experiments with PostgreSQL’s page storage for the Hierarchical Navigable Small World (HNSW) index. similar to pgvector. However, we encountered various limitations that hindered its effectiveness and functionality:

  • Parallelization Challenges: The process model of PostgreSQL, where each statement corresponds to a single process and the lack of thread-safe APIs, presents challenges for parallelization. In the case of building vector indexes, which involve computationally intensive tasks, parallelization can significantly enhance performance. However, our efforts to parallelize this process were impeded by frequent occurrences of ‘Too many shared buffer locked’ errors.
  • Write-Ahead Logging (WAL) Amplification: The issue of Write-Ahead Logging (WAL) amplification arises when inserting a 2KB vector results in generating over 20KB of WAL. This problem is inherent in the HNSW algorithm used, as it involves modifying multiple edges for a single point insertion. PostgreSQL records each change individually in the WAL, leading to substantial amplification of the WAL size.
  • Lock Contention: Lock contention arises due to the need to lock every edge list during reads and writes when traversing the HNSW graph. The hierarchical structure of HNSW, where higher levels contain fewer points, often results in lock contention becoming a common bottleneck during index usage. This contention occurs when multiple operations attempt to access or modify the same edge list simultaneously, leading to delays and reduced concurrency.

pgvecto.rs adopted a design akin to FreshDiskANN, resembling the Log-Structured Merge (LSM) tree concept. This architecture comprises three components: the writing segment, the growing segment, and the sealed segment. New vectors are initially written to the writing segment. A background process then asynchronously transforms them into the immutable growing segment. Subsequently, the growing segment undergoes a merge with the sealed segment, akin to the compaction process in an LSM tree. This design offers several benefits:

  • Non-blocking Insertions: Index modification operations do not impede insertion processes.
  • Batched Modifications: Grouping modifications to the HNSW graph improves throughput.
  • Elimination of Read-Write Lock Contention: Since sealed segments are immutable, issues related to read-write lock contention are mitigated.

However, there are drawbacks to this approach. One notable limitation is the absence of out-of-the-box Write-Ahead Logging (WAL) support for the index. This means that features like Point-in-Time Recovery and Physical Replication, which rely on WAL, are not readily available for the index. Nevertheless, the PostgreSQL ecosystem is robust and allows for extensions to define their own custom WAL through a resource manager. While implementing this solution requires additional effort, it is feasible to overcome the limitation and enable WAL support for the index.

Summary

Here we will show the main distinctions between pgvecto.rs and pgvector.

Feature pgvecto.rs pgvector
Filtering Introduces VBASE method for vector search and relational query (e.g. Single-Vector TopK + Filter + Join). When filters are applied, the results may be incomplete. For example, if you originally intended to limit the results to 10, you might end up with only 5 results with filters.
Vector Dimensions Supports up to 65535 dimensions. Supports up to 2000 dimensions.
SIMD SIMD instructions are dynamically dispatched at runtime to maximize performance based on the capabilities of the specific machine. Relies on compiler-generated SIMD code at compile time.
Data Types Introduces additional data types: binary vectors, FP16 (16-bit floating point), and INT8 (8-bit integer). -
Indexing Handles the storage and memory of indexes separately from PostgreSQL Relies on the native storage engine of PostgreSQL
WAL Support Provides Write-Ahead Logging (WAL) support for data, index support is working in progress. Provides Write-Ahead Logging (WAL) support for index and data.

6.3 - Why not a specialty vector database?

Read our complete blog at Do we really need a specialized vector database? (2023) | vchord.pgsty.com.

Read our complete blog at Do we really need a specialized vector database? (2023) | vchord.pgsty.com.

Imagine this, your existing data is stored in a Postgres database, and you want to use a vector database to do some vector similarity search. You have to move your data from Postgres to the vector database, and you have to maintain two databases at the same time. This is not a good idea.

Why not just use Postgres to do the vector similarity search? This is the reason why we build pgvecto.rs. The user journey is like this:

-- Update the embedding column for the documents table
UPDATE documents SET embedding = ai_embedding_vector(content) WHERE length(embedding) = 0;

-- Create an index on the embedding column
CREATE INDEX ON documents USING vectors (embedding vector_l2_ops);

-- Query the similar embeddings
SELECT * FROM documents ORDER BY embedding <-> ai_embedding_vector('hello world') LIMIT 5;

From SingleStore DB Blog:

Vectors and vector search are a data type and query processing approach, not a foundation for a new way of processing data. Using a specialty vector database (SVDB) will lead to the usual problems we see (and solve) again and again with our customers who use multiple specialty systems: redundant data, excessive data movement, lack of agreement on data values among distributed components, extra labor expense for specialized skills, extra licensing costs, limited query language power, programmability and extensibility, limited tool integration, and poor data integrity and availability compared with a true DBMS.

6.4 - Benchmarks

pgvecto.rs has been focused on performance from the beginning. We have continuously monitored the performance of pgvecto.rs and compared it with other vector se

pgvecto.rs has been focused on performance from the beginning. We have continuously monitored the performance of pgvecto.rs and compared it with other vector search libraries. The following benchmark results are from January 2024.

The test is done on Google Cloud n2-standard-8 (8 vCPUs, 32 GB RAM) with the laion-768-5m-ip dataset.

pgvecto.rs v.s. pgvector

With the HNSW index, pgvecto.rs can achieve 2.5x responses per second as pgvector can do with a slightly better precision of around 97%. This advantage increases when higher precision is required.

pgvecto.rs_vs_pgvector

When the vbase mode is enabled, pgvecto.rs can achieve over 2x more responses per second compared to pgvector on various filter probabilities.

pgvecto.rs_vs_pgvector_filtervbase

Different quantization methods

pgvecto.rs supports several quantization methods and different low precision indexing. These methods can help to reduce memory usage at different scales. The following figure shows the performance of pgvecto.rs with different quantization methods.

pgvecto.rs_quantization

Shortening embeddings (a.k.a Matryoshka Embedding)

OpenAI latest embedding models support shortening embeddings, where developers could simply “remove some numbers from the end of a sequence” and still maintain a valid representation for text.

This enables very flexible usage. For example, when using a vector data store that only supports embeddings up to 1024 dimensions long, developers can now still use our best embedding model text-embedding-3-large and specify a value of 1024 for the dimensions API parameter, which will shorten the embedding down from 3072 dimensions, trading off some accuracy in exchange for the smaller vector size.

These are the performance benchmarks for the shortened embeddings. The dbpedia-entities-openai3-text-embedding-3-large-3072-1M dataset is used for the benchmark. The original embeddings have 3072 dimensions, while the shortened embeddings have dimensions of 256 and 1024. The benchmark was conducted using a VM with specifications of n2-standard-8 (8 vCPUs, 32 GB memory) on Google Cloud.

First Pass in the benchmark shows that the accuracy of the shortened embeddings is not as good as the full embeddings. 256D HNSW index only achieves ~65% accuracy, while 1024D HNSW index achieves ~85% accuracy.

However, the accuracy of the shortened embeddings surpasses 95% when adaptive retrieval is employed. Additionally, adopting this approach leads to significant improvements in terms of RPS (Requests Per Second) and latency.

Binary vectors

pgvecto.rs supports binary vectors. The bvector type is optimized for storage and performance. It uses a bit-packed representation to store the binary vector. The distance calculation is also optimized for binary vectors.

Here are some performance benchmarks for the bvector type. We use the dbpedia-entities-openai3-text-embedding-3-large-3072-1M dataset for the benchmark. The VM is n2-standard-8 (8 vCPUs, 32 GB memory) on Google Cloud.

We upsert 1M binary vectors into the table and then run a KNN query for each embedding. It only takes about 600MB memory to index 1M binary vectors, while the vector type takes about 18GB memory to index the same number of vectors.

bvector

We can see that the bvector’s accuracy is not as good as the vector type, but it exceeds 95% if we adopt adaptive retrieval.

7 - Reference

SQL schema, indexing options, search options, and vector types in pgvecto.rs.

7.1 - Schema

Here is the schema provided by pgvecto.rs.

Here is the schema provided by pgvecto.rs.

List of data types

Name Description
vector vector, scalar type of which is binary32 defined in IEEE 754-2008
vecf16 vector, scalar type of which is binary16 defined in IEEE 754-2008
veci8 vector, scalar type of which is 8-bit Integer
svector sparse vector, scalar type of which is binary32 defined in IEEE 754-2008
bvector binary vector, a fixed-length bit string
vector_index_stat a composite type representing vector index statistics

List of operators

Name Left arg type Right arg type Result type Description
+ vector vector vector element-wise arithmetic
+ vecf16 vecf16 vecf16 element-wise arithmetic
+ veci8 veci8 veci8 element-wise arithmetic
+ svector svector svector element-wise arithmetic
- vector vector vector element-wise arithmetic
- vecf16 vecf16 vecf16 element-wise arithmetic
- veci8 veci8 veci8 element-wise arithmetic
- svector svector svector element-wise arithmetic
& bvector bvector bvector element-wise logical AND
| bvector bvector bvector element-wise logical OR
^ bvector bvector bvector element-wise logical XOR
= vector vector boolean dictionary order comparison
= vecf16 vecf16 boolean dictionary order comparison
= veci8 veci8 boolean dictionary order comparison
= svector svector boolean dictionary order comparison
= bvector bvector boolean dictionary order comparison
<> vector vector boolean dictionary order comparison
<> vecf16 vecf16 boolean dictionary order comparison
<> veci8 veci8 boolean dictionary order comparison
<> svector svector boolean dictionary order comparison
<> bvector bvector boolean dictionary order comparison
< vector vector boolean dictionary order comparison
< vecf16 vecf16 boolean dictionary order comparison
< veci8 veci8 boolean dictionary order comparison
< svector svector boolean dictionary order comparison
< bvector bvector boolean dictionary order comparison
> vector vector boolean dictionary order comparison
> vecf16 vecf16 boolean dictionary order comparison
> veci8 veci8 boolean dictionary order comparison
> svector svector boolean dictionary order comparison
> bvector bvector boolean dictionary order comparison
<= vector vector boolean dictionary order comparison
<= vecf16 vecf16 boolean dictionary order comparison
<= veci8 veci8 boolean dictionary order comparison
<= svector svector boolean dictionary order comparison
<= bvector bvector boolean dictionary order comparison
>= vector vector boolean dictionary order comparison
>= vecf16 vecf16 boolean dictionary order comparison
>= veci8 veci8 boolean dictionary order comparison
>= svector svector boolean dictionary order comparison
>= bvector bvector boolean dictionary order comparison
<-> vector vector real squared Euclidean distance
<-> vecf16 vecf16 real squared Euclidean distance
<-> veci8 veci8 real squared Euclidean distance
<-> svector svector real squared Euclidean distance
<-> bvector bvector real squared Euclidean distance
<#> vector vector real negative dot product
<#> vecf16 vecf16 real negative dot product
<#> veci8 veci8 real negative dot product
<#> svector svector real negative dot product
<#> bvector bvector real negative dot product
<=> vector vector real cosine distance
<=> vecf16 vecf16 real cosine distance
<=> veci8 veci8 real cosine distance
<=> svector svector real cosine distance
<=> bvector bvector real cosine distance
<~> bvector bvector real jaccard distance

List of functions

Name Argument type Result type Description
pgvectors_upgrade void Administration function for upgrading pgvecto.rs.
to_svector dims integer, index integer[], value real[] svector Construct a sparse vector from two arrays of indexes and values
binarize vector vector bvector Binarize a vector. All positive elements are set to 1, otherwise 0.
text2vec_openai input text, model text vector Embedding function for OpenAI embeddings API.
text2vec_openai_v3 input text vector Embedding function for OpenAI embeddings API. The model is set to text-embedding-3-small.

List of casts

Source type Target type Implicit?
real[] vector yes
vector real[] yes
vector vecf16 no
vecf16 vector no
vector veci8 no
veci8 vector no
vector svector no
svector vector no
vector bvector no
bvector vector no

List of access methods

Name Type Description
vectors Index pgvecto.rs vector index

List of operator families

AM Operator family Applicable types
vectors vector_cos_ops vector
vectors vector_dot_ops vector
vectors vector_l2_ops vector
vectors vecf16_cos_ops vecf16
vectors vecf16_dot_ops vecf16
vectors vecf16_l2_ops vecf16
vectors veci8_cos_ops veci8
vectors veci8_dot_ops veci8
vectors veci8_l2_ops veci8
vectors svector_cos_ops svector
vectors svector_dot_ops svector
vectors svector_l2_ops svector
vectors bvector_cos_ops bvector
vectors bvector_dot_ops bvector
vectors bvector_l2_ops bvector
vectors bvector_jaccard_ops bvector

List of operator classes

AM Input type Operator class Default? Description
vectors vector vector_l2_ops no squared Euclidean distance
vectors vector vector_dot_ops no negative dot product
vectors vector vector_cos_ops no cosine distance
vectors vecf16 vecf16_l2_ops no squared Euclidean distance
vectors vecf16 vecf16_dot_ops no negative dot product
vectors vecf16 vecf16_cos_ops no cosine distance
vectors veci8 veci8_l2_ops no squared Euclidean distance
vectors veci8 veci8_dot_ops no negative dot product
vectors veci8 veci8_cos_ops no cosine distance
vectors svector svector_l2_ops no squared Euclidean distance
vectors svector svector_dot_ops no negative dot product
vectors svector svector_cos_ops no cosine distance
vectors bvector bvector_l2_ops no squared Euclidean distance
vectors bvector bvector_dot_ops no negative dot product
vectors bvector bvector_cos_ops no cosine distance
vectors bvector bvector_jaccard_ops no jaccard distance

List of views

Name Description
pg_vector_index_stat A view provided for vector index statistics

7.2 - Indexing Options

We utilize TOML syntax to express the index’s configuration. Here’s what each key in the configuration signifies:

We utilize TOML syntax to express the index’s configuration. Here’s what each key in the configuration signifies:

Key Type Description
segment table Options for segments.
optimizing table Options for background optimizing.
indexing table The algorithm to be used for indexing.

Options for table segment

Key Type Range Default Description
max_growing_segment_size integer [1, 4_000_000_000] 20_000 Maximum size of unindexed vectors.
max_sealed_segment_size integer [1, 4_000_000_000] 1_000_000 Maximum size of vectors for indexing.

Options for table optimizing

Key Type Range Default Description
optimizing_threads integer [1, 65535] sqrt of the number of cores Maximum threads for indexing.
sealing_secs integer [1, 60] 60 See below.
sealing_size integer [1, 4_000_000_000] 1 See below.

If a write segment larger than sealing_size do not accept new data for sealing_secs seconds, the write segment will be turned to a sealed segment.

Options for table indexing

Key Type Description
flat table If this table is set, brute force algorithm will be used for the index.
ivf table If this table is set, IVF will be used for the index.
hnsw table If this table is set, HNSW will be used for the index.

You can choose only one algorithm in above indexing algorithms. Default value is hnsw.

Options for table flat

Key Type Description
quantization table The algorithm to be used for quantization.

Options for table ivf

Key Type Range Default Description
nlist integer [1, 1_000_000] 1000 Number of cluster units.
nsample integer [1, 1_000_000] 65536 Number of samples for K-Means clustering.
least_iterations integer [1, 1_000_000] 16 Least iterations for K-Means clustering.
iterations integer [1, 1_000_000] 500 Max iterations for K-Means clustering.
quantization table The quantization algorithm to be used.

Options for table hnsw

Key Type Range Default Description
m integer [4, 128] 12 Maximum degree of the node.
ef_construction integer [10, 2000] 300 Search scope in building.
quantization table The quantization algorithm to be used.

Options for table quantization

Key Type Description
trivial table If this table is set, no quantization is used.
scalar table If this table is set, scalar quantization is used.
product table If this table is set, product quantization is used.

You can choose only one algorithm in above indexing algorithms. Default value is trivial.

Options for table product

Key Type Range Default Description
sample integer [1, 1_000_000] 65535 Samples to be used for quantization.
ratio enum "x4", "x8", "x16", "x32", "x64" "x4" Compression ratio for quantization.

Compression ratio is how many memory you are saved for saving vectors. For example, "x4" is 14\frac{1}{4} compared to before. If you are creating indexes on vecf16, "x4" is 12\frac{1}{2} compared to before.

7.3 - Search Options

Search options are specified by PostgreSQL GUC.

Search options are specified by PostgreSQL GUC.

Options for ivf

These options only work for IVF algorithm. Refer to Indexing.

Option Type Range Default Description
vectors.ivf_nprobe integer [1, 1_000_000] 10 Number of lists to scan.

Options for hnsw

These options only work for HNSW algorithm. Refer to Indexing.

Option Type Range Default Description
vectors.hnsw_ef_search integer [1, 65535] 100 Search scope of HNSW.

Other Options

Query options for search mode: .

Option Type Range Default Description
vectors.search_mode enum "basic", "vbase" "vbase" Search mode, Refer to Search
vectors.pgvector_compatibility boolean off Enable or disable pgvector compatibility, Refer to pgvector compatibility
vectors.enable_index boolean on Enable or disable the query planner’s use of vector types.

7.4 - Vector Types

In the previous sections Indexing and Search, we encountered the vector type. This section will explore additional vector types and their applications in the re

In the previous sections Indexing and Search, we encountered the vector type. This section will explore additional vector types and their applications in the realm of vector search.

Low-precision Indexing

since v0.3.0

To improve the speed of queries on the vector column, consider creating a low-precision index using vecf16, veci8, or bvector types instead of a vector type.

CREATE TABLE t (val vector(3));
INSERT INTO t (val) SELECT ARRAY[random(), random(), random()]::real[] FROM generate_series(1, 1000);

-- Create bvector index at vector column
-- Warning: Float32 vectors are truncated to 
--          integers of either 0 or 1 during index construction
CREATE INDEX lp_bin ON t USING vectors ((binarize(val)::bvector(3)) bvector_l2_ops);

-- Create vecf16 index at vector column
CREATE INDEX lp_f16 ON t USING vectors ((val::vecf16(3)) vecf16_l2_ops);

-- Create veci8 index at vector column
-- Warning: Float32 vectors are truncated to 
--          integers ranging from -128 to 127 during index construction
CREATE INDEX lp_i8 ON t USING vectors ((val::veci8(3)) veci8_l2_ops);

Shortening Embedding

since v0.3.0

Certain embedding models like OpenAI text-embedding-3-small support shortening embeddings, which involves removing some numbers from the end of the sequence without losing the embedding’s concept-representing properties.

In some cases, it may be necessary to change the embedding dimension after generation. The pgvecto.rs offers an approach to query by shortened embeddings.

CREATE TABLE t (val vector(3));
INSERT INTO t (val) SELECT ARRAY[random(), random(), random()]::real[] FROM generate_series(1, 1000);
CREATE INDEX ON t USING vectors (val vector_l2_ops);

-- Measure distance using only the first two dimensions
SELECT * from t ORDER BY (val[:2])::vector(2) <-> '[3, 2]'::vector;

Shortened embeddings are supported by all data types in pgvecto.rs since version v0.3.0.

Casts

This diagram shows the conversion between different data types, where the types connected by arrows can be cast to each other:

Among them, ARRAY is a native type of postgreSQL, others are types defined by pgvecto.rs.

Here are some examples of vector type casting:

-- OK: String ⇒ vector
SELECT '{1,2,3}'::vector;

-- OK: vector ⇒ ARRAY
SELECT '{1,2,3}'::vector::real[];

-- OK: ARRAY ⇒ vector ⇒ bvector
SELECT ARRAY[1.0 ,0 ,0]::real[]::vector::bvector;

-- ERROR: cannot cast type vecf16 to veci8
SELECT '[3, 2]'::vecf16::veci8;

-- OK: vecf16 ⇒ vector ⇒ veci8
SELECT '[3, 2]'::vecf16::vector::veci8;

7.4.1 - Half-Precision Vector: `vecf16`

Stored as a half-precision number format, vecf16 takes advantage of the 16-bit float, which uses half the memory and bandwidth compared to vector.

Stored as a half-precision number format, vecf16 takes advantage of the 16-bit float, which uses half the memory and bandwidth compared to vector.

Here’s an example of creating a table with a vecf16 column and inserting values:

CREATE TABLE items (
  id bigserial PRIMARY KEY,
  embedding vecf16(3) NOT NULL
);

INSERT INTO items (embedding) VALUES ('[0.1, 0.2, 0]'), ('[0, 0.1, 0.2]');

Index can be created on vecf16 type as well.

CREATE INDEX your_index_name ON items USING vectors (embedding vecf16_l2_ops);

SELECT * FROM items ORDER BY embedding <-> '[0.3,0.2,0.1]' LIMIT 1;

We support three operators to calculate the distance between two vecf16 values.

  • <-> (vecf16_l2_ops): squared Euclidean distance, defined as Σ(xi−yi)2\Sigma (x_i - y_i) ^ 2.
  • <#> (vecf16_dot_ops): negative dot product, defined as −Σxiyi- \Sigma x_iy_i.
  • <=> (vecf16_cos_ops): cosine distance, defined as 1−ΣxiyiΣxi2Σyi21 - \frac{\Sigma x_iy_i}{\sqrt{\Sigma x_i^2 \Sigma y_i^2}}.

7.4.2 - 8-Bit Integer Vector: `veci8`

Int8 is a data type used to represent signed integers. It stands for “integer 8-bit” and typically uses 8 bits of memory to store a value. This allows it to rep
since v0.3.0

Int8 is a data type used to represent signed integers. It stands for “integer 8-bit” and typically uses 8 bits of memory to store a value. This allows it to represent integer values ranging from -128 to 127.

Since each element in the vector only requires 8 bits (1 byte) of memory, veci8 can help reduce the overall memory footprint of your program.

Here’s an example of creating a table with a veci8 column and inserting values:

CREATE TABLE items (
  id bigserial PRIMARY KEY,
  embedding veci8(3) NOT NULL
);

INSERT INTO items (embedding) VALUES ('[1, 2, 0]'), ('[-1, 15, -2]');

Index can be created on veci8 type as well.

CREATE INDEX your_index_name ON items USING vectors (embedding veci8_l2_ops);

SELECT * FROM items ORDER BY embedding <-> '[0.3,0.2,0.1]' LIMIT 1;

We support three operators to calculate the distance between two veci8 values.

  • <-> (veci8_l2_ops): squared Euclidean distance, defined as Σ(xi−yi)2\Sigma (x_i - y_i) ^ 2.
  • <#> (veci8_dot_ops): negative dot product, defined as −Σxiyi- \Sigma x_iy_i.
  • <=> (veci8_cos_ops): cosine distance, defined as 1−ΣxiyiΣxi2Σyi21 - \frac{\Sigma x_iy_i}{\sqrt{\Sigma x_i^2 \Sigma y_i^2}}.

7.4.3 - Sparse Vector: `svector`

Unlike dense vectors, sparse vectors are very high-dimensional but contain few non-zero values.
since v0.3.0

Unlike dense vectors, sparse vectors are very high-dimensional but contain few non-zero values.

Typically, sparse vectors can be created from:

  • Word co-occurrence matrices
  • Term frequency-inverse document frequency (TF-IDF) vectors
  • User-item interaction matrices
  • Network adjacency matrices

Sparse vectors in pgvecto.rs are called svector.

Here’s an example of creating a table with a svector column and inserting values:

CREATE TABLE items (
  id bigserial PRIMARY KEY,
  embedding svector(10) NOT NULL
);

INSERT INTO items (embedding) VALUES ('[0.1,0,0,0,0,0,0,0,0,0]'), ('[0,0,0,0,0,0,0,0,0,0.5]');

Index can be created on svector type as well.

CREATE INDEX your_index_name ON items USING vectors (embedding svector_l2_ops);

SELECT * FROM items ORDER BY embedding <-> '[0.3,0,0,0,0,0,0,0,0,0]' LIMIT 1;

We support three operators to calculate the distance between two svector values.

  • <-> (svector_l2_ops): squared Euclidean distance, defined as Σ(xi−yi)2\Sigma (x_i - y_i) ^ 2.
  • <#> (svector_dot_ops): negative dot product, defined as −Σxiyi- \Sigma x_iy_i.
  • <=> (svector_cos_ops): cosine distance, defined as 1−ΣxiyiΣxi2Σyi21 - \frac{\Sigma x_iy_i}{\sqrt{\Sigma x_i^2 \Sigma y_i^2}}.

There is also a function to_svector to create a svector. It will set the value at the specified position.

F(index)={value[v]∃v∈N:position[v]=index0else F(index)=\left\{ \begin{array}{rcl} value[v] & & {\exists v \in \mathbb{N}: position[v] = index }\\ 0 & & {else}\\ \end{array} \right.
-- to_svector(dim: INTEGER, position: ARRAY, value: ARRAY) -> svector
SELECT to_svector(5, '{0, 4}', '{0.3, 0.5}');
-- [0.3, 0, 0, 0, 0.5]

7.4.4 - Binary Vector: `bvector`

The bvector type is a binary vector type in pgvecto.rs. It represents a binary vector, which is a vector where each component can take on two possible values, t
since v0.3.0

The bvector type is a binary vector type in pgvecto.rs. It represents a binary vector, which is a vector where each component can take on two possible values, typically 0 and 1.

Here’s an example of creating a table with a bvector column and inserting values:

CREATE TABLE items (
  id bigserial PRIMARY KEY,
  embedding bvector(3) NOT NULL
);

INSERT INTO items (embedding) VALUES ('[1,0,1]'), ('[0,1,0]');

Index can be created on bvector type as well.

CREATE INDEX your_index_name ON items USING vectors (embedding bvector_l2_ops);

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

We support three operators to calculate the distance between two bvector values.

  • <-> (bvector_l2_ops): squared Euclidean distance, defined as Σ(xi−yi)2\Sigma (x_i - y_i) ^ 2. The Hamming distance is equivalent to the squared Euclidean distance for binary vectors.
  • <#> (bvector_dot_ops): negative dot product, defined as −Σxiyi- \Sigma x_iy_i.
  • <=> (bvector_cos_ops): cosine distance, defined as 1−ΣxiyiΣxi2Σyi21 - \frac{\Sigma x_iy_i}{\sqrt{\Sigma x_i^2 \Sigma y_i^2}}.
  • <~> (bvector_jaccard_ops): Jaccard distance, defined as 1−∣X∩Y∣∣X∪Y∣1 - \frac{|X\cap Y|}{|X\cup Y|}.

There is also a function binarize to build bvector from vector. It truncates vector elements to integers of 0 or 1.

F(element)={0element<11element≥1 F(element)=\left\{ \begin{array}{rcl} 0 & & {element < 1}\\ 1 & & {element \geq 1}\\ \end{array} \right.
-- binarize(source: vector) -> bvector
SELECT binarize('[-2, -1, 0, 1, 2]'::vector);
-- [0, 0, 0, 1, 1]

Performance

The bvector type is optimized for storage and performance. It uses a bit-packed representation to store the binary vector. The distance calculation is also optimized for binary vectors.

Here are some performance benchmarks for the bvector type. We use the dbpedia-entities-openai3-text-embedding-3-large-3072-1M dataset for the benchmark. The VM is n2-standard-8 (8 vCPUs, 32 GB memory) on Google Cloud.

We upsert 1M binary vectors into the table and then run a KNN query for each embedding. It only takes about 600MB memory to index 1M binary vectors, while the vector type takes about 18GB memory to index the same number of vectors.

bvector

We can see that the bvector’s accuracy is not as good as the vector type, but it exceeds 95% if we adopt adaptive retrieval.

8 - Community

Contributing guide, project roadmap, and open-source adopters.

8.1 - Contributing to `pgvecto.rs`

We welcome all kinds of contributions from the open-source community, individuals, and partners. If you’re interested in contributing, hopefully, this document

We welcome all kinds of contributions from the open-source community, individuals, and partners. If you’re interested in contributing, hopefully, this document makes the process for contributing clear.

Contributors and people new to open source alike will find the following guides especially useful:

Code of conduct

A code of conduct is a document that establishes expectations for behavior for the project’s participants. Adopting, and enforcing, a code of conduct can help create a positive social atmosphere for the community.

If you do not know what code of conduct is, please have a look at Your Code of Conduct | Open Source Guides.

Get involved

Our community is always looking for enthusiasts to help in all different ways.

⚽ Simply start using pgvecto.rs

Go through the Getting Started guide. Does everything work as expected? If not, we’re always looking for improvements. Chat with us on 💬 Discord or file an issue on GitHub.

🙋 Triaging Issues and Pull Requests

One great way you can contribute to the project without writing any code is to help triage issues and pull requests as they come in.

  • Ask for more information if you believe the issue does not provide all the details required to solve it.
  • Suggest labels that can help categorize issues.
  • Flag issues that are stale or that should be closed.
  • Ask for test plans and review code.

👨‍💻 Contribute to pgvecto.rs

If you find an issue you would like to fix, open a pull request. Please read development tutorials to get started.

👉 Improve documentation

Everyone loves documentation. If you find anything that is confusing or can be improved, you can click “Edit this page” at the bottom of most docs, which takes you to the GitHub interface to make and propose changes.

💬 Discuss pgvecto.rs on Discord

Chat with us on 💬 Discord.

8.2 - Roadmap

Sparse vectors have shown excellent performance in tasks such as RAG and text retrieval, and can improve the results of dense vectors. Therefore, we plan to sup
Warning

We strongly suggest you explore our new VectorChord implementation, offering enhanced stability and performance. Users are advised to transition to VectorChord. Refer to the migration guide for assistance.

Midterm Plan

Sparse Vector Support

Sparse vectors have shown excellent performance in tasks such as RAG and text retrieval, and can improve the results of dense vectors. Therefore, we plan to support sparse vector indexing.

Better support for streaming workloads

As a database, it is important to support flexible insertion and deletion of vector data. However, the current mainstream algorithm HNSW does not have good support for deletion because it requires rebuilding the index. Therefore, we plan to follow the DiskANN article and support the Vamana algorithm. The Vamana algorithm is an incremental indexing algorithm based on Graph that can support deletion and insertion of vectors without rebuilding the index.

More vectors on a single machine

Supporting more vectors under the same resources is very important for users to save costs. Currently, we have supported users to store vectors in fp16 format, which requires only half of the memory and space with little loss of accuracy. We also plan to support more vector formats, such as fp8 and int8, to further reduce storage and memory consumption. At the same time, we are also exploring SSD-based vector indexing algorithms such as DiskANN.

Physical and Streaming Replication

pgvecto.rs is currently an independent storage system separate from PostgreSQL, managing index memory and storage on its own. Therefore it doesn’t support physical replication out of the box. We plan to implement a custom WAL resource manager to meet the requirements of physical replication and make it easier for users to use replication in PG.

Longterm plan

1B vectors inside PostgreSQL

Our ultimate goal is to support 1 billion vectors in PostgreSQL. However, there are many challenges that need to be overcome to reach this goal. It may be necessary to implement a custom vector storage solution using the table access method, or to use a distributed shard such as Citus. We have not yet explored this area extensively, so interested developers are encouraged to discuss it with us.

Talk with us

Want to jump in? Welcome discussions and contributions!

These items are not essential for an MVP, but are a part of our longer term plans. Feel free to jump in on these if you’re interested!

To be added in the future.

8.3 - Adopters

If you are using pgvecto.rs in your app, please edit this page. We are glad to get your feedback and help you to improve your experience with pgvecto.rs.

Add your app here

If you are using pgvecto.rs in your app, please edit this page. We are glad to get your feedback and help you to improve your experience with pgvecto.rs.

9 - Developers

Development environment and build tutorial for pgvecto.rs contributors.

9.1 - Development

You can set up development environment simply using envd. It will create a docker container and install all the dependencies for you.

Set up development environment

You can set up development environment simply using envd. It will create a docker container and install all the dependencies for you.

pip install envd
git clone https://github.com/tensorchord/pgvecto.rs.git
cd pgvecto.rs
envd up

Or you can setup development environment following these steps manually:

  1. Install base dependency.
sudo apt install -y \
    bison \
    build-essential \
    ccache \
    flex \
    gcc \
    git \
    gnupg \
    libreadline-dev \
    libssl-dev \
    libxml2-dev \
    libxml2-utils \
    libxslt-dev \
    lsb-release \
    pkg-config \
    tzdata \
    xsltproc \
    zlib1g-dev
  1. Install clang-16. Other versions of clang are not supported.
sudo sh -c 'echo "deb http://apt.llvm.org/$(lsb_release -cs)/ llvm-toolchain-$(lsb_release -cs)-16 main" >> /etc/apt/sources.list'
wget --quiet -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add -
sudo apt-get update
sudo apt-get install -y --no-install-recommends clang-16
  1. Install Rust. Do not install rustc using a package manager.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
  1. Clone the Repository.
git clone https://github.com/tensorchord/pgvecto.rs.git
cd pgvecto.rs
  1. Install cargo-pgrx.
cargo install cargo-pgrx --version $(grep -o 'pgrx = { version = "=[^"]*' Cargo.toml | cut -d = -f 4)
cargo pgrx init

cargo-pgrx is a helpful tool to develop a PostgreSQL extension. You can read the document in https://docs.rs/crate/cargo-pgrx/latest.

Cross compilation

Assuming that you build binary and schema for aarch64 target in x86_64 host environment, you can follow these steps:

  1. Install cross compilation toolchain.
sudo apt install crossbuild-essential-arm64
sudo apt install qemu-user-static

Set environment variables.

LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/aarch64-linux-gnu/lib:/usr/aarch64-linux-gnu/lib64"

Set emulator, linker and sysroot for Rust by adding the following section to the end of ~/.cargo/config.toml.

[target.aarch64-unknown-linux-gnu]
runner = ["qemu-aarch64-static", "-L", "/usr/aarch64-linux-gnu"]
linker = "aarch64-linux-gnu-gcc"

[env]
BINDGEN_EXTRA_CLANG_ARGS_aarch64_unknown_linux_gnu = "-isystem /usr/aarch64-linux-gnu/include/ -ccc-gcc-name aarch64-linux-gnu-gcc"
  1. Generate PostgreSQL header files on target architecture. It can be done with cargo-pgrx and you can also take advantages of files under vendor/pgrx_config and vendor/pgrx_binding. For details, please refer to the documentation of pgrx.

Debug

Debug information included in the compiled binary even in release mode so you can always use gdb for debugging.

For a debug build, backtrace is printed when a thread in background worker process panics, but not for a session process error. For a release build, backtrace is never printed. But if you set environment variable RUST_BACKTRACE to 1, all backtraces are printed. It’s recommended for you to debug a release build with the command RUST_BACKTRACE=1 cargo pgrx run --release.