Skip to content

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

Return to the regular view of this page.

vchord Documentation

Scalable, fast, and disk-friendly vector similarity search in PostgreSQL powered by RaBitQ quantization.

Welcome to the vchord documentation. Select a topic below to get started with installation, indexing (vchordrq and vchordg), multi-vector retrieval, hybrid search, benchmarks, and production operations.

1 - Getting Started

Overview, installation packages, Docker images, and the Vchord retrieval suite.

1.1 - Overview

vchord is a PostgreSQL extension designed for scalable, high-performance, and disk-efficient vector similarity search.

vchord is a PostgreSQL extension designed for scalable, high-performance, and disk-efficient vector similarity search.

With vchord, you can store 400,000 vectors for just $1, enabling significant savings: 6x more vectors compared to Pinecone’s optimized storage and 26x more than pgvector/pgvecto.rs for the same price1. For further insights, check out our launch blog post.

Features

vchord introduces remarkable enhancements over pgvecto.rs and pgvector:

⚡ Enhanced Performance: Delivering optimized operations with up to 5x faster queries, 16x higher insert throughput, and 16x quicker1 index building compared to pgvector’s HNSW implementation.

💰 Affordable Vector Search: Query 100M 768-dimensional vectors using just 32GB of memory, achieving 35ms P50 latency with top10 recall@95%, helping you keep infrastructure costs down while maintaining high search quality.

🔌 Seamless Integration: Fully compatible with pgvector data types and syntax while providing optimal defaults out of the box - no manual parameter tuning needed. Just drop in vchord for enhanced performance.

🔧 Accelerated Index Build: Leverage IVF to build indexes externally (e.g., on GPU) for faster KMeans clustering, combined with RaBitQ2 compression to efficiently store vectors while maintaining search quality through autonomous reranking.

📏 Long Vector Support: Store and search vectors up to 60,0003 dimensions, enabling the use of the best high-dimensional models like text-embedding-3-large with ease.

🌐 Scale As You Want: Based on horizontal expansion, the query of 5M / 100M 768-dimensional vectors can be easily scaled to 10000+ QPS with top10 recall@90% at a competitive cost4

Quick Start

For new users, we recommend using the Docker image to get started quickly. If you do not prefer Docker, please read installation guide for other installation methods.

docker run \
  --name vectorchord-demo \
  -e POSTGRES_PASSWORD=mysecretpassword \
  -p 5432:5432 \
  -d tensorchord/vchord-postgres:pg18-v1.1.1

In addition to the base image with the VectorChord extension, we provide an all-in-one image, tensorchord/vchord-suite:pg18-latest. This comprehensive image includes all official TensorChord extensions. Developers should select an image tag that is compatible with their extension’s version, as indicated in the support matrix.

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 -h localhost -p 5432 -U postgres

Now you can play with VectorChord!

VectorChord depends on pgvector, including the vector representation. Since you can use them directly, your application can be easily migrated without pain!

CREATE EXTENSION IF NOT EXISTS vchord CASCADE;

Similar to pgvector, you can create a table with vector column and insert some rows to it.

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

With VectorChord, you can create vchordrq indexes.

CREATE INDEX ON items USING vchordrq (embedding vector_l2_ops);

And then perform a vector search using SELECT ... ORDER BY ... LIMIT ....

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

For more usage, please read:

License

vchord is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0-only). We welcome any commercial collaboration or support inquiries—please email us at rh@vonng.com with any questions.


  1. Based on MyScale Benchmark with 768-dimensional vectors and 95% recall. Please check out our blog post for more details. ↩︎ ↩︎

  2. Gao, Jianyang, and Cheng Long. “RaBitQ: Quantizing High-Dimensional Vectors with a Theoretical Error Bound for Approximate Nearest Neighbor Search.” Proceedings of the ACM on Management of Data 2.3 (2024): 1-27. ↩︎

  3. There is a limitation at pgvector of 16,000 dimensions now. If you really have a large dimension(16,000<dim<60,000), consider changing VECTOR_MAX_DIM and compile pgvector yourself. ↩︎

  4. Please check our blog post for more details, the PostgreSQL scalability is powered by CloudNative-PG. ↩︎

1.2 - Installation

VectorChord is tested on the following operating system:

VectorChord is tested on the following operating system:

  • Ubuntu (x86_64, aarch64)
  • MacOS (x86_64, aarch64)
  • Windows (x86_64)
  • Alpine Linux (x86_64, aarch64) 1
  • Debian (powerpc64le, s390x, riscv64) 2

Please report a bug if you encounter issues on any of the above operating systems, or submit a feature request for additional platform support.

Docker

VectorChord Image

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

  1. Launch a VectorChord container in Docker.
docker run \
  --name vchord-demo \
  -e POSTGRES_PASSWORD=mysecretpassword \
  -p 5432:5432 \
  -d tensorchord/vchord-postgres:pg18-v1.1.1
  1. Connect to the database using the psql command line tool. The default username is postgres.
psql postgresql://postgres:mysecretpassword@localhost:5432/postgres
  1. Run the following SQL to ensure the extension is enabled.
CREATE EXTENSION IF NOT EXISTS vchord CASCADE;
Tip

To achieve full performance, please mount the volume to PostgreSQL data directory by adding the option like:

  • -v $PWD/pgdata:/var/lib/postgresql/18/docker, on PostgreSQL 18
  • -v $PWD/pgdata:/var/lib/postgresql/data, on PostgreSQL 14, 15, 16 and 17

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

VectorChord Suite Image

In addition to the base image with the VectorChord extension, we provide an all-in-one image, tensorchord/vchord-suite:pg18-latest. This comprehensive image includes all official TensorChord extensions. Developers should select an image tag that is compatible with their extension’s version, as indicated in the support matrix.

  1. Launch a VectorChord container in Docker.
docker run \
  --name vchord-suite \
  -e POSTGRES_PASSWORD=postgres \
  -p 5432:5432 \
  -d tensorchord/vchord-suite:pg18-latest
  # If you want to use ghcr image, you can change the image to `ghcr.io/tensorchord/vchord-suite:pg18-latest`.
  # if you want to use the specific version, you can use the tag `pg17-20250815`, supported version can be found in the support matrix.

Other sections may align with the above.

VectorChord Scratch Image

We provide a scratch image that contains only the files of VectorChord. You can install VectorChord in any image using Docker’s COPY, like

FROM tensorchord/vchord-scratch:pg18-v1.1.1 AS vchord_scratch
FROM postgres:18-bookworm
RUN apt-get update && apt-get install -y postgresql-18-pgvector
COPY --from=vchord_scratch / /
CMD ["postgres", "-c" ,"shared_preload_libraries=vchord,vector"]

This image can also be used as a CloudNativePG image volume extension. See also

Source

Tip

Build requirements:

  • any port of make
  • clang >= 16 with libclang
  • rust >= 1.93 with cargo

It’s recommended to use Rustup for installing Rust on most platforms, while on Alpine Linux, using the system package manager is advised.

You can set the environment variable CC to specify the desired C compiler for the build system. If you do not set this variable, the build system automatically searches for clang and gcc. To compile all C code with clang, set CC to the path of clang. To compile all C code with gcc, set CC to the path of gcc; note that in this case, there is a requirement gcc >= 14.

Rust version requirement is not a long-term guarantee; we will raise the required Rust version with each new release.

These build requirements apply only to x86_64 and aarch64. For all other architectures, including powerpc64le, s390x and riscv64, nightly Rust and latest Clang are required.

If you have not installed PostgreSQL yet, please install PostgreSQL. If you have not installed pgvector yet, you could install pgvector before the 3rd step.

  1. Download the source code, build and install it with make.
curl -fsSL https://github.com/tensorchord/VectorChord/archive/refs/tags/1.1.1.tar.gz | tar -xz
cd VectorChord-1.1.1
make build
make install # or `sudo make install`
  1. Configure your PostgreSQL by modifying the shared_preload_libraries to include the extension. And then restart the PostgreSQL cluster.
psql -U postgres -c 'ALTER SYSTEM SET shared_preload_libraries = "vchord"'
  1. Run the following SQL to ensure the extension is enabled.
CREATE EXTENSION IF NOT EXISTS vchord CASCADE;

Tuning

VectorChord performs runtime CPU dispatch for SIMD code. If you want the compiler to optimize the whole program for a particular CPU, you could instruct it to generate code for that CPU.

RUSTFLAGS="-Ctarget-cpu=icelake" make build

To see all valid CPU options and the default CPU for your target triple, run:

rustc --print target-cpus

To generate code for the host machine, run:

RUSTFLAGS="-Ctarget-cpu=native" make build

Please note that binaries generated with a specific target-cpu are not compatible with other CPUs.

You can also do it by using Cargo’s configuration.

cd VectorChord-1.1.1
mkdir -p .cargo
touch .cargo/config.toml
echo 'build.rustflags = ["-Ctarget-cpu=icelake"]' >> ./cargo/config.toml

Debian

If you have not installed PostgreSQL yet, please install PostgreSQL following https://www.postgresql.org/download/linux/debian/. If you have not installed pgvector yet, you could install pgvector by apt install postgresql-18-pgvector before the 3rd step.

Note on release URLs

New VectorChord releases are hosted at github.com/pgsty/vchord/releases, while historical releases (1.1.1 and earlier) remain available at github.com/tensorchord/VectorChord/releases.

  1. Download Debian packages in the release page, and install them by apt.
wget https://github.com/tensorchord/VectorChord/releases/download/1.1.1/postgresql-18-vchord_1.1.1-1_$(dpkg --print-architecture).deb
sudo apt install ./postgresql-18-vchord_1.1.1-1_$(dpkg --print-architecture).deb
  1. Configure your PostgreSQL by modifying the shared_preload_libraries to include the extension. And then restart the PostgreSQL cluster.
psql -U postgres -c 'ALTER SYSTEM SET shared_preload_libraries = "vchord"'
sudo systemctl restart postgresql.service
  1. Run the following SQL to ensure the extension is enabled.
CREATE EXTENSION IF NOT EXISTS vchord CASCADE;

Ubuntu

If you have not installed PostgreSQL yet, please install PostgreSQL following https://www.postgresql.org/download/linux/ubuntu/. If you have not installed pgvector yet, you could install pgvector by apt install postgresql-18-pgvector before the 3rd step.

Note on release URLs

New VectorChord releases are hosted at github.com/pgsty/vchord/releases, while historical releases (1.1.1 and earlier) remain available at github.com/tensorchord/VectorChord/releases.

  1. Download Debian packages in the release page, and install them by apt.
wget https://github.com/tensorchord/VectorChord/releases/download/1.1.1/postgresql-18-vchord_1.1.1-1_$(dpkg --print-architecture).deb
sudo apt install ./postgresql-18-vchord_1.1.1-1_$(dpkg --print-architecture).deb
  1. Configure your PostgreSQL by modifying the shared_preload_libraries to include the extension. And then restart the PostgreSQL cluster.
psql -U postgres -c 'ALTER SYSTEM SET shared_preload_libraries = "vchord"'
sudo systemctl restart postgresql.service
  1. Run the following SQL to ensure the extension is enabled.
CREATE EXTENSION IF NOT EXISTS vchord CASCADE;

PGXN

Tip

See Source for build requirements.

If you have not installed PostgreSQL yet, please install PostgreSQL. If you have not installed pgvector yet, you could install pgvector by pgrxclient install vector before the 3rd step.

  1. Install VectorChord from PostgreSQL Extension Network with:
pgxnclient install vchord # or `pgxnclient install vchord --sudo`
  1. Configure your PostgreSQL by modifying the shared_preload_libraries to include the extension. And then restart the PostgreSQL cluster.
psql -U postgres -c 'ALTER SYSTEM SET shared_preload_libraries = "vchord"'
  1. Run the following SQL to ensure the extension is enabled.
CREATE EXTENSION IF NOT EXISTS vchord CASCADE;
Tip

There is a broken VectorChord 0.4.1 package on PGXN. Please do not use it. Use version 0.4.2 or later instead.

Prebuilt binaries

deprecated
Warning

Prebuilt binaries may not match the PostgreSQL in ABI on your machine, which could cause silent errors. We strongly recommend that you build from source in this case.

If you have not installed PostgreSQL yet, please install PostgreSQL. If you have not installed pgvector yet, you could install pgvector before the 3rd step.

  1. Download prebuilt binaries in the release page, and repackage it referring to your distribution’s documentation. Then install it by system package manager. We do not recommend doing this, but if you wish, you can also manually copy the files to the system directory.
cp -r ./pkglibdir/. $(pg_config --pkglibdir)
cp -r ./sharedir/. $(pg_config --sharedir)
  1. Configure your PostgreSQL by modifying the shared_preload_libraries to include the extension. And then restart the PostgreSQL cluster.
psql -U postgres -c 'ALTER SYSTEM SET shared_preload_libraries = "vchord"'
  1. Run the following SQL to ensure the extension is enabled.
CREATE EXTENSION IF NOT EXISTS vchord CASCADE;

  1. VectorChord is tested with PostgreSQL 15 in community repository, 16 and 17 in main repository on Alpine Linux 3.22. ↩︎

  2. VectorChord is tested with PostgreSQL 13, 14, 15, 16, 17 and 18 in PostgreSQL’s official repository on powerpc64le, and PostgreSQL 15 and 17 in Debian’s official repository on s390x and riscv64, with QEMU user space emulator. ↩︎

1.3 - Vchord Suite

Retrieval-Augmented Generation (RAG) is revolutionizing our interaction with vast datasets and language models. Build production-ready RAG directly in PostgreSQL with the Vchord Suite.

Retrieval-Augmented Generation (RAG) is revolutionizing our interaction with vast datasets and language models. RAG systems offer more precise, contextually aware, and current answers by retrieving pertinent information before generating responses. Nonetheless, constructing robust, scalable, and efficient RAG pipelines poses considerable challenges, particularly for production use.

Many solutions feature complex architectures, using distinct databases for vector search, keyword search, and primary data storage. This commonly results in data synchronization challenges, greater infrastructure complexity, and higher operational costs.

What if you could build a powerful, production-ready RAG system directly within your trusted PostgreSQL database?

Enter the Vchord Suite, a collection of PostgreSQL extensions designed to bring high-performance vector search, native BM25 ranking, and flexible tokenization capabilities right into Postgres. Let’s explore the components and how they enable next-generation RAG solutions.

What is the Vchord Suite?

The suite comprises three key PostgreSQL extensions working in concert:

  1. vchord (pgsty/vchord): The core vector search engine. It’s specifically designed for scalable, high-performance, and disk-efficient vector similarity search within PostgreSQL.

  2. vchord_bm25 (pgsty/vchord_bm25): This extension implements the sophisticated BM25 ranking algorithm directly inside PostgreSQL, leveraging efficient Block-WeakAnd algorithms. BM25 is a standard for relevance ranking based on keyword frequency and document characteristics.

  3. pg_tokenizer (pgsty/pg_tokenizer): Provides essential text tokenization capabilities needed for effective full-text search, enabling fine-grained control over how text is processed for full-text search.

By combining these extensions, you unlock powerful capabilities for building advanced RAG systems entirely within PostgreSQL.

How to Use the Vchord Suite

You can use the tensorchord/vchord-suite Docker image to run multiple extensions which are provided by TensorChord. The image is based on the Docker official PostgreSQL image and includes the following extensions:

  • vchord
  • pg_tokenizer
  • vchord_bm25
  • vector
docker run \
  --name vchord-suite \
  -e POSTGRES_PASSWORD=postgres \
  -p 5432:5432 \
  -d tensorchord/vchord-suite:pg18-latest

# If you want to use ghcr image, you can change the image to `ghcr.io/tensorchord/vchord-suite:pg18-latest`.
docker run \
  --name vchord-suite \
  -e POSTGRES_PASSWORD=postgres \
  -p 5432:5432 \
  -d tensorchord/vchord-suite:pg18-latest

# If you want to use the specific version, you can use the tag `pg17-20250414`, supported version can be found in the support matrix.
docker run \
  --name vchord-suite \
  -e POSTGRES_PASSWORD=postgres \
  -p 5432:5432 \
  -d tensorchord/vchord-suite:pg17-20250414
CREATE EXTENSION IF NOT EXISTS vchord CASCADE;
CREATE EXTENSION IF NOT EXISTS pg_tokenizer CASCADE;
CREATE EXTENSION IF NOT EXISTS vchord_bm25 CASCADE;
\dx
pg_tokenizer | 0.1.1   | tokenizer_catalog | pg_tokenizer
vchord       | 1.1.1   | public            | vchord: Vector database plugin for Postgres, written in Rust, specifically designed for LLM
vchord_bm25  | 0.3.0   | bm25_catalog      | vchord_bm25: A postgresql extension for bm25 ranking algorithm
vector       | 0.8.1   | public            | vector data type and ivfflat and hnsw access methods

Use Cases for the Vchord Suite

Use Case 1: Powerful Hybrid Search with Native BM25 and vchord

Hybrid Search

In the RAG era, effective retrieval is paramount. Neither keyword search nor vector search alone is perfect:

  • Keyword Search (like BM25): Excels at precision, finding documents with exact keyword matches. It’s great for structured queries and term-specific searches. However, it struggles with synonyms, paraphrasing, and understanding the underlying meaning or semantic intent. (Leverages vchord_bm25 and pg_tokenizer).

  • Vector Search: Captures deep semantic meaning and relationships between concepts, allowing it to find relevant information even if the exact keywords aren’t present. However, it can sometimes lack precision for queries demanding specific term matches. (Leverages vchord).

The Solution: Hybrid Search. By combining the strengths of both approaches within PostgreSQL, the Vchord Suite bridges this gap. You can run a query that leverages:

  • vchord_bm25 (powered by pg_tokenizer) for keyword precision.

  • vchord for semantic understanding.

The results from both can be intelligently combined (e.g., using reciprocal rank fusion - RRF or Model-based rerank) to produce a final ranking that is more accurate, contextually relevant, and semantically aware than either method could achieve alone. This delivers significantly better retrieval performance for your RAG applications.

Use Case 2: Beyond Text - OCR-Free RAG with ColQwen2 & vchord

Building RAG systems for documents like PDFs or scanned images often involves cumbersome pre-processing pipelines. Traditional methods rely heavily on Optical Character Recognition (OCR) and layout analysis to extract text. This process can be:

  • Slow: OCR can be computationally expensive.

  • Error-Prone: OCR accuracy varies significantly depending on document quality.

  • Lossy: Crucial visual contexts like tables, figures, formatting, and relative positioning is often lost during text extraction.

The Solution: OCR-Free, Visually-Aware RAG. What if you could query documents based on how they look and their content, without explicit OCR?

This is now achievable by combining:

  1. Multi-Modal Vision Language Models (VLMs): Models like ColQwen2 can process images (document pages) and generate embeddings that capture both textual content and visual layout information.

  2. vchord’s Multi-Vector Capabilities: vchord can efficiently store and search multiple vectors per document within PostgreSQL – allowing you to store embeddings representing different aspects (e.g., text content, visual layout).

With this setup, you can query your document database using prompts that reference visual elements (“Find documents with a bar chart comparing sales figures”) or combined textual and visual cues, directly within PostgreSQL. This simplifies your RAG stack, potentially boosts retrieval accuracy by preserving visual context, and eliminates the bottlenecks associated with traditional OCR pipelines.

Why Build RAG in PostgreSQL?

Leveraging the Vchord Suite within PostgreSQL offers significant advantages:

  • Unified Data: Keep your source data, text, metadata, and vector embeddings all in one place.

  • Reduced Complexity: Eliminate the need for separate vector databases and synchronization pipelines.

  • Leverage Existing Infrastructure: Utilize your existing PostgreSQL expertise, tooling, and operational practices.

  • Transactional Integrity: Benefit from PostgreSQL’s robust ACID compliance.

  • Rich Ecosystem: Access the wide array of tools and features available within the PostgreSQL ecosystem.

Conclusion

The Vchord Suite transforms PostgreSQL into a powerful, production-ready platform for building advanced RAG solutions. By integrating high-performance vector search (vchord), sophisticated keyword ranking (vchord_bm25), and flexible text processing (pg_tokenizer), you can implement cutting-edge techniques like hybrid search and OCR-free multi-modal retrieval directly within your database. Simplify your architecture, enhance retrieval accuracy, and unlock the full potential of RAG with Vchord in PostgreSQL.

Blog Posts

In our recent blog posts, we shared some user cases where all Docker images can be replaced with VectorChord Suite image.

References

2 - Usage

Indexing with vchordrq and vchordg, MaxSim multi-vector retrieval, quantization types, range queries, and tuning.

2.1 - Indexing

VectorChord’s index type vchordrq divides vectors into lists and searches only a subset of lists closest to the query vector. It provides fast build time and lo

VectorChord’s index type vchordrq divides vectors into lists and searches only a subset of lists closest to the query vector. It provides fast build time and low memory consumption, while delivering significantly better performance than both hnsw and ivfflat.

Let’s start by creating a table named items with an embedding column of type vector(n), and then populate it with sample data.

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

To create a vchordrq index, you can use the following SQL.

CREATE INDEX ON items USING vchordrq (embedding vector_l2_ops);

After the index is built, you can perform a vector search using it to retrieve the 1010 nearest neighbors to a vector.

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

You can also add filters to vector search queries as needed.

SELECT * FROM items WHERE id % 7 <> 0 ORDER BY embedding <-> '[3,1,2]' LIMIT 10;

Tuning

When there are less than 100,000100,000 rows in the table, you usually don’t need to set the index options.

CREATE INDEX ON items USING vchordrq (embedding vector_l2_ops);

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

However, as the number of rows grows, partitioning becomes necessary and can be configured using index options. options are specified using a TOML: Tom’s Obvious Minimal Language string. You can refer to #Index Options for more information.

CREATE INDEX ON items USING vchordrq (embedding vector_l2_ops) WITH (options = $$
[build.internal]
lists = [1000]
$$);

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

The parameter lists should be tuned based on the number of rows. The following table provides guidelines for choosing an appropriate value. When querying, choose vchordrq.probes accordingly.

Number of Rows NN Recommended Number of Partitions LL Example lists
N∈[0,105)N \in [0, 10^5) N/A []
N∈[105,2×106)N \in [10^5, 2 \times 10^6) L=N500L = \frac{N}{500} [2000]
N∈[2×106,5×107)N \in [2 \times 10^6, 5 \times 10^7) L∈[4N,8N]L \in [4 \sqrt{N}, 8 \sqrt{N}] [10000]
N∈[5×107,∞)N \in [5 \times 10^7, \infty) L∈[8N,16N]L \in [8 \sqrt{N}, 16\sqrt{N}] [80000]

The process of building an index involves two steps: partitioning the vector space first, and then inserting rows into the index. The first step, partitioning the vector space, can be sped up using multiple threads.

CREATE INDEX ON items USING vchordrq (embedding vector_l2_ops) WITH (options = $$
[build.internal]
lists = [1000]
build_threads = 8
$$);

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

The second step, inserting rows, can be parallelized using multiple processes. Refer to PostgreSQL Tuning.

For most datasets using cosine similarity, enabling residual_quantization and build.internal.spherical_centroids improves both QPS and recall.

CREATE INDEX ON items USING vchordrq (embedding vector_cosine_ops) WITH (options = $$
residual_quantization = true
[build.internal]
lists = [1000]
spherical_centroids = true
build_threads = 8
$$);

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

To improve the build speed, you may opt to use more shared memory to accelerate the process by setting build.pin to 2.

CREATE INDEX ON items USING vchordrq (embedding vector_l2_ops) WITH (options = $$
residual_quantization = true
build.pin = 2
[build.internal]
lists = [1000]
spherical_centroids = true
build_threads = 8
$$);

For large tables, time and memory overhead are dominated by the partitioning phase. See Partitioning Tuning for further tuning.

You can also refer to External Build to offload the partitioning phase to other machines.

Reference

Operator Classes (vchordrq)

vchordrq

The following table lists all available operator classes supported by vchordrq.

Operator Class Description Operator 1 Operator 2
vector_l2_ops index works for vector type and Euclidean distance <->(vector,vector) <<->>(vector,vector)
vector_ip_ops index works for vector type and negative inner product <#>(vector,vector) <<#>>(vector,vector)
vector_cosine_ops index works for vector type and cosine distance <=>(vector,vector) <<=>>(vector,vector)
halfvec_l2_ops index works for halfvec type and Euclidean distance <->(halfvec,halfvec) <<->>(halfvec,halfvec)
halfvec_ip_ops index works for halfvec type and negative inner product <#>(halfvec,halfvec) <<#>>(halfvec,halfvec)
halfvec_cosine_ops index works for halfvec type and cosine distance <=>(halfvec,halfvec) <<=>>(halfvec,halfvec)
rabitq8_l2_ops index works for rabitq8 type and Euclidean distance <->(rabitq8,rabitq8) <<->>(rabitq8,rabitq8)
rabitq8_ip_ops index works for rabitq8 type and negative inner product <#>(rabitq8,rabitq8) <<#>>(rabitq8,rabitq8)
rabitq8_cosine_ops index works for rabitq8 type and cosine distance <=>(rabitq8,rabitq8) <<=>>(rabitq8,rabitq8)
rabitq4_l2_ops index works for rabitq4 type and Euclidean distance <->(rabitq4,rabitq4) <<->>(rabitq4,rabitq4)
rabitq4_ip_ops index works for rabitq4 type and negative inner product <#>(rabitq4,rabitq4) <<#>>(rabitq4,rabitq4)
rabitq4_cosine_ops index works for rabitq4 type and cosine distance <=>(rabitq4,rabitq4) <<=>>(rabitq4,rabitq4)
vector_maxsim_ops index works for vector[] type and scalable vector-similarity @#(vector[],vector[]) N/A
halfvec_maxsim_ops index works for halfvec[] type and scalable vector-similarity @#(halfvec[],halfvec[]) N/A
rabitq8_maxsim_ops index works for rabitq8[] type and scalable vector-similarity @#(rabitq8[],rabitq8[]) N/A
rabitq4_maxsim_ops index works for rabitq4[] type and scalable vector-similarity @#(rabitq4[],rabitq4[]) N/A

<->, <#> and <=> are operators defined by pgvector.

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

<<->>, <<#>>, <<=>> are operators defined by VectorChord. For more information about <<->>, <<#>>, <<=>>, refer to Similarity Filter.

@# is an operator defined by VectorChord. For more information about @#, refer to Multi-Vector Retrieval.

rabitq8, rabitq4 are types defined by VectorChord. For more information about rabitq8, rabitq4, refer to Quantization Types.

The operator classes for MaxSim are available since version 0.3.0. The operator classes for rabitq8 and rabitq4 are available since version 1.1.0.

Indexing Options (vchordrq)

vchordrq

residual_quantization

  • Description: This option determines whether residual quantization is used. If you are not familiar with residual quantization, you can read this blog for more information. In short, residual quantization is a technique that improves the accuracy of vector search by quantizing the residuals of the vectors.
  • Type: boolean
  • Default: false
  • Example:
    • residual_quantization = false means that residual quantization is not used.
    • residual_quantization = true means that residual quantization is used.
  • Note: This option is not supported for rabitq8 and rabitq4.

degree_of_parallelism

since v1.0.0
  • Description: This option is a hint that specifies the degree of parallelism. In most cases, you do not need to change it. If you are using a CPU with more than 32 threads and wish to utilize more threads for PostgreSQL, you may set it to the number of threads for better performance.
  • Type: integer
  • Default: 32
  • Domain: [1, 256]
  • Example:
    • degree_of_parallelism = 32 hints to the index that 32 or less processes may access on the index concurrently.
    • degree_of_parallelism = 64 hints to the index that 64 or less processes may access on the index concurrently.

build.pin

since v0.2.1
  • Description: This option determines whether shared memory is used for indexing. For large tables, you can choose to enable this option to speed up the build process.
  • Type: union of integer and boolean
  • Default:
    • -1 since v1.0.0
    • false until v0.5.3
  • Domain:
    • {-1, 0, 1, 2, false, true} since v1.0.0
    • {false, true} until v0.5.3
  • Example:
    • build.pin = 2 means the hot portion of the index is cached in memory.
    • build.pin = 1 means a subset of the hot portion of the index is cached in memory, consuming less memory.
    • build.pin = 0 means that this feature is enabled but nothing is actually cached. This option is for debugging purposes only.
    • build.pin = -1 means that this feature is disabled.
    • build.pin = false is the legacy form of build.pin = -1.
    • build.pin = true is the legacy form of build.pin = 1.

Default Build Options

since v0.5.3

This is the default value of index building. The index will not be partitioned. In terms of semantics, build.default = {} is similar to build.internal.lists = [].

Internal Build Options (vchordrq)

vchordrq

build.internal.lists

  • Description: This option determines the hierarchical structure of the vector space partitioning.
  • Type: list of integers
  • Default:
    • [] since v0.3.0
    • [1000] until v0.2.2: implicit behavior is not ideal
  • Example:
    • build.internal.lists = [] means that the vector space is not partitioned.
    • build.internal.lists = [4096] means the vector space is divided into 40964096 cells.
    • build.internal.lists = [4096, 262144] means the vector space is divided into 40964096 cells, and those cells are further divided into 262144262144 smaller cells.
  • Note: The index partitions the vector space into multiple Voronoi cells based on centroids, iteratively creating a hierarchical space partition tree. Each leaf node in this tree represents a region with an associated list storing vectors in that region. During insertion, vectors are placed in lists corresponding to their appropriate leaf nodes. For queries, the index optimizes search by excluding lists whose leaf nodes are distant from the query vector, effectively pruning the search space. If the length of lists is 11, the lists option should be no less than 4N4\sqrt{N}, where NN is the number of vectors in the table.

build.internal.spherical_centroids

  • Description: This option determines whether to perform spherical K-means – the centroids are L2 normalized after each iteration, you can refer to option spherical in here.
  • Type: boolean
  • Default: false
  • Example:
    • build.internal.spherical_centroids = false means that spherical k-means is not performed.
    • build.internal.spherical_centroids = true means that spherical k-means is performed.
  • Note: Set this to true if your model generates embeddings that use cosine similarity as the metric.

build.internal.sampling_factor

since v0.2.0
  • Description: This option determines the number of vectors the K-means algorithm samples per cluster. The higher this value, the slower the build, the greater the memory consumption in building, and the better search performance.
  • Type: integer
  • Domain: [0, 1024]
  • Default: 256
  • Example:
    • build.internal.sampling_factor = 256 means that the K-means algorithm samples 256C256 C vectors, where CC is the maximum value in build.internal.lists.
    • build.internal.sampling_factor = 32 means that the K-means algorithm samples 32C32 C vectors, where CC is the maximum value in build.internal.lists. This reduces K-means’ time and memory usage to approximately 18\frac{1}{8} of what it would be with the default value of 256.

build.internal.kmeans_iterations

since v0.2.2
  • Description: This option determines the number of iterations for K-means algorithm. The higher this value, the slower the build.
  • Type: integer
  • Domain: [0, 1024]
  • Default: 10
  • Example:
    • build.internal.kmeans_iterations = 10 means that the K-means algorithm performs 1010 iterations.
    • build.internal.kmeans_iterations = 100 means that the K-means algorithm performs 100100 iterations.

build.internal.build_threads

  • Description: This option determines the number of threads used by K-means algorithm. The higher this value, the faster the build, and greater load on the server in building.
  • Type: integer
  • Domain: [1, 255]
  • Default: 1
  • Example:
    • build.internal.build_threads = 1 means that the K-means algorithm uses 11 thread.
    • build.internal.build_threads = 4 means that the K-means algorithm uses 44 threads.

build.internal.kmeans_algorithm

since v1.0.0
  • Description: This option determines the K-means algorithm to be used.
  • Type: object
  • Example:
    • build.internal.kmeans_algorithm.lloyd = {}. This uses Lloyd’s algorithm. This is the default value.
    • build.internal.kmeans_algorithm.hierarchical = {}. This uses hierarchical clustering. Compared to Lloyd’s algorithm, this approach is much faster, but it may cause a loss of accuracy.

build.internal.kmeans_dimension

since v1.0.0
  • Description: This option determines the dimension to use for K-means input and output. This feature employs dimensionality reduction and expansion via resampling, effectively reducing K-means’ time and memory consumption, but it may cause a loss of accuracy.
  • Type: union of integer and null
  • Default: null
  • Example:
    • If this option is not set, this feature is disabled.
    • build.internal.kmeans_dimension = 100 means that K-means will process vectors with 100100 dimensions. For original vectors of 900900 dimensions, this reduces K-means’ time and memory usage to approximately 19\frac{1}{9} of what it would be without this feature.

Search Parameters (vchordrq)

vchordrq

vchordrq.enable_scan

since v0.5.0
  • Description: Enables or disables the query planner’s use of vchordrq index scan. It’s for testing purposes.
  • Type: boolean
  • Default: on
  • Example:
    • vchordrq.enable_scan = off disables the query planner’s use of vchordrq index scan.
    • vchordrq.enable_scan = on enables the query planner’s use of vchordrq index scan.

vchordrq.probes

  • Description: This GUC parameter vchordrq.probes controls how the vector space assists in query pruning. The more probes, the more accurate the search, but also the slower it is.
  • Type: list of integers
  • Default:
    • since v0.3.0
    • 10 until v0.2.2: the default value was 10 before `lists` defaulted to empty
    • This parameter supports fallback.
  • Example:
    • SET vchordrq.probes = 1 means that only one probe is used.
    • SET vchordrq.probes = 10 means that ten probes are used.
  • Note: The default value is an empty list. The length of this option must match the length of lists.
    • If lists = [], then probes must be .
    • If lists = [11, 22], then probes can be 2,4 or 4,8. It must not be incorrectly shaped, for example, , 3, 7,8,9, 5,5,5,5.

vchordrq.epsilon

  • Description: Even after pruning, the number of retrieved vectors remains substantial. The index employs the RaBitQ algorithm to quantize vectors into bit vectors, which require just 132\frac{1}{32} the memory of single-precision floating-point vectors. Most computations are integer-based, leading to faster processing. Unlike conventional quantization algorithms, RaBitQ estimates not only distances but also their lower bounds. The index computes the lower bound for each vector and dynamically adjusts the number of vectors needing recalculated distances, based on the query count, thus balancing performance and accuracy. The GUC parameter vchordrq.epsilon controls the conservativeness of the lower bounds of distances. The higher the value, the higher the accuracy, but the worse the performance. The default value tends to favor higher accuracy than needed for most use cases, so you can try lowering this parameter to achieve better performance.
  • Type: real
  • Default:
  • Domain: [0.0, 4.0]
  • Example:
    • SET vchordrq.epsilon = 0.1 indicates you are using a very optimistic lower bound estimation. You set it this way because your dataset is not sensitive to the lower bound estimation, for the precision you need.
    • SET vchordrq.epsilon = 4.0 indicates you are using a very pessimistic lower bound estimation. You set it this way because your dataset is not very sensitive to the lower bound estimation, for the precision you need.

vchordrq.prewarm_dim

removed in v0.4.0
  • Description: The vchordrq.prewarm_dim GUC parameter is used to precompute the RaBitQ projection matrix for the specified dimensions. This can help to reduce the latency of the first query after the PostgreSQL cluster is started.
  • Type: list of integers
  • Default: 64,128,256,384,512,768,1024,1536
  • Example:
    • ALTER SYSTEM SET vchordrq.prewarm_dim = '64,128' means that the projection matrix will be precomputed for dimensions 64 and 128.
  • Note:
    • This setting requires a database restart to take effect.
    • Since v0.4.0, a new algorithm made this option obsolete.

vchordrq.max_scan_tuples

since v0.2.0
  • Description: The GUC parameter vchordrq.max_scan_tuples controls the maximum number of tuples that can be scanned in a vector search. In most cases, you do not need to set this parameter, because the LIMIT clause serves a similar purpose. However, when a WHERE clause is present, the LIMIT clause applies after filtering, while this GUC parameter applies before filtering. This parameter is intended to prevent performance degradation when the filtering selectivity is very low.
  • Type: integer
  • Default: -1
  • Domain: [-1, 2147483647]
  • Example:
    • SET vchordrq.max_scan_tuples = 999 indicates the index scan returns at most 999999 tuples.
    • SET vchordrq.max_scan_tuples = -1 indicates no limit on the number of tuples.
  • Note: This parameter has no effect when set to -1.

2.2 - Multi-Vector Retrieval

Multi-Vector Retrieval is an advanced technique used in Retrieval-Augmented Generation (RAG) systems to enhance document retrieval by leveraging multiple vector
since v0.3.0

Multi-Vector Retrieval is an advanced technique used in Retrieval-Augmented Generation (RAG) systems to enhance document retrieval by leveraging multiple vectors per document.

Unlike Single-Vector Retrieval, Multi-Vector Retrieval using the MaxSim operator enables capturing fine-grained semantic relationships.

Refer to our blog for more details on building an end-to-end Multi-Vector Retrieval application.

How to start

Late interaction models embed documents and queries as vector arrays separately and compute similarity through an operator called MaxSim. The definition of the MaxSim operator is ∑imax⁡jqi⋅dj\sum_i \max_j q_i \cdot d_j, meaning that for each vector in the query vector array, the closest vector in the document vector array is found, their dot product is calculated, and the results are summed.

In VectorChord, the symbol for this operator is @#. The left operand is the document vector array, and the right operand is the query vector array. While the original MaxSim operator is similarity-based, while the operator in VectorChord is based on distance. VectorChord’s implementation interprets it as a distance metric by negating the similarity score.

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

CREATE TABLE items (id bigserial PRIMARY KEY, embeddings vector(3)[]);

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

Queries using the MaxSim operator can be accelerated with indexing, like other vector operators.

CREATE INDEX ON items USING vchordrq (embeddings vector_maxsim_ops);

Then you can perform a vector search with the index.

SELECT * FROM items
ORDER BY embeddings @# 
    ARRAY[
        '[3,1,2]'::vector,
        '[2,2,2]'::vector
    ] LIMIT 5;

Reference

This feature is not supported by vchordg.

Operator Classes

Refer to

Search Parameters (vchordrq)

vchordrq

The indexing mechanism for MaxSim operators works similarly to other vector operators. When an index is built on a column of vector arrays, each vector within the arrays is individually inserted into the index data structure. During querying, the index performs a separate vector search for each vector in the query array. By default, the index does not rerank the results. Instead, it uses RaBitQ’s estimated distances as a substitute for actual distances. The index then merges the results from all separated vector searches to produce the final output.

There are a few extra parameters used by maxsim indexes.

vchordrq.maxsim_refine

since v0.3.0
  • Description: This GUC parameter vchordrq.maxsim_refine makes the index rerank the results, replacing RaBitQ’s estimated distances with actual distances, until the distances of the top-k nearest vectors have all been recalculated.
  • Type: integer
  • Domain: [0, 2147483647]
  • Default:
  • Example:
    • SET vchordrq.maxsim_refine = 0 means that the index always uses RaBitQ’s estimated distances.
    • SET vchordrq.maxsim_refine = 1024 means that the index reranks the results, replacing RaBitQ’s estimated distances with actual distances, until the distances of the top-1024 nearest vectors have all been recalculated.

vchordrq.maxsim_threshold

since v0.3.0
  • Description: This GUC parameter vchordrq.maxsim_threshold enables more aggressive estimation of missing values. With this setting, the index identifies the first cluster in the lowest-level lists whose cumulative size meets or exceeds vchordrq.maxsim_threshold, and uses its distance to the query vector for estimating the missing values.
  • Type: integer
  • Domain: [0, 2147483647]
  • Default:
  • Example:
    • SET vchordrq.maxsim_threshold = 0 means that the more aggressive estimation strategy is not enabled.
    • SET vchordrq.maxsim_threshold = 1024 means that the index identifies the first cluster in the lowest-level lists whose cumulative size meets or exceeds 1024, and uses its distance to the query vector for estimating the missing values.

2.3 - Graph Index

VectorChord’s index type vchordg is a disk-based graph index. It has low memory consumption.
since v0.5.0

VectorChord’s index type vchordg is a disk-based graph index. It has low memory consumption.

Let’s start by creating a table named items with an embedding column of type vector(n), and then populate it with sample data.

To create a vchordg index, you can use the following SQL.

CREATE INDEX ON items USING vchordg (embedding vector_l2_ops);
Tip

This feature is in preview.

Tuning

When building an index, two options usually need tuning: m and ef_construction. m is the maximum number of neighbors per vertex, and ef_construction is the size of the dynamic list containing the nearest neighbors during insertion.

In search, you need to tune ef_search. ef_search is the size of the dynamic list containing the nearest neighbors during search.

CREATE INDEX ON items USING vchordg (embedding vector_l2_ops) WITH (options = $$
m = 64
ef_construction = 128
$$);

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

As a disk-based index, vchordg usually requires only the quantized vectors to be kept in the buffer pool to maintain performance. By default, vchordg quantizes a DD-dimensional vector to 2D2D bits. Let the number of rows be NN. Then, the total memory required for the index is 2DN2DN bits. If you have very limited memory and are using ultra-high dimensional vectors, consider quantizing a vector to DD bits. Then, the total memory required for the index is DNDN bits.

CREATE INDEX ON items USING vchordg (embedding vector_l2_ops) WITH (options = $$
bits = 1
m = 64
ef_construction = 128
$$);

The index building can be sped up by using multiple processes. Refer to PostgreSQL Tuning.

Reference

Operator Classes (vchordg)

vchordg

The following table lists all available operator classes supported by vchordg.

Operator Class Description Operator 1 Operator 2
vector_l2_ops index works for vector type and Euclidean distance <->(vector,vector) <<->>(vector,vector)
vector_ip_ops index works for vector type and negative inner product <#>(vector,vector) <<#>>(vector,vector)
vector_cosine_ops index works for vector type and cosine distance <=>(vector,vector) <<=>>(vector,vector)
halfvec_l2_ops index works for halfvec type and Euclidean distance <->(halfvec,halfvec) <<->>(halfvec,halfvec)
halfvec_ip_ops index works for halfvec type and negative inner product <#>(halfvec,halfvec) <<#>>(halfvec,halfvec)
halfvec_cosine_ops index works for halfvec type and cosine distance <=>(halfvec,halfvec) <<=>>(halfvec,halfvec)
rabitq8_l2_ops index works for rabitq8 type and Euclidean distance <->(rabitq8,rabitq8) <<->>(rabitq8,rabitq8)
rabitq8_ip_ops index works for rabitq8 type and negative inner product <#>(rabitq8,rabitq8) <<#>>(rabitq8,rabitq8)
rabitq8_cosine_ops index works for rabitq8 type and cosine distance <=>(rabitq8,rabitq8) <<=>>(rabitq8,rabitq8)
rabitq4_l2_ops index works for rabitq4 type and Euclidean distance <->(rabitq4,rabitq4) <<->>(rabitq4,rabitq4)
rabitq4_ip_ops index works for rabitq4 type and negative inner product <#>(rabitq4,rabitq4) <<#>>(rabitq4,rabitq4)
rabitq4_cosine_ops index works for rabitq4 type and cosine distance <=>(rabitq4,rabitq4) <<=>>(rabitq4,rabitq4)

<<->>, <<#>>, <<=>> are operators defined by VectorChord. For more information about <<->>, <<#>>, <<=>>, refer to Similarity Filter.

rabitq8, rabitq4 are types defined by VectorChord. For more information about rabitq8, rabitq4, refer to Quantization Types.

The first 6 operator classes are available since version 0.5.0, and the last 6 operator classes are available since version 1.1.0.

Indexing Options (vchordg)

vchordg

bits

since v0.5.0
  • Description: The ratio of bits to dimensions after RaBitQ quantization. bits = 2 provides better recall and QPS, while bits = 1 consumes less memory.
  • Type: integer
  • Default: 2
  • Example:
    • bits = 2 means a DD-dimensional vector is quantized to 2D2D bits.
    • bits = 1 means a DD-dimensional vector is quantized to DD bits .

m

since v0.5.0
  • Description: The maximum number of neighbors per vertex. The larger m is, the better the performance, but the higher the storage requirement. m corresponds to m0m_0 in HNSW and mm in DiskANN.
  • Type: integer
  • Default: 32
  • Example:
    • m = 32 means that there are at most 3232 neighbors per vertex.
    • m = 64 means that there are at most 6464 neighbors per vertex.

ef_construction

since v0.5.0
  • Description: The size of the dynamic list containing the nearest neighbors during insertion. The larger ef_construction is, the better the performance, but the slower the insertion. ef_construction corresponds to efconstruction\text{ef}_\text{construction} in HNSW and efC\text{ef}_C in DiskANN.
  • Type: integer
  • Default: 64
  • Example:
    • ef_construction = 64 means that the size of the dynamic list containing the nearest neighbors is 6464 during insertion.
    • ef_construction = 128 means that the size of the dynamic list containing the nearest neighbors is 128128 during insertion.

alpha

since v0.5.0
  • Description: The alpha values selected during pruning. alpha corresponds to α\alpha in DiskANN. This option must be an ascending list, where the first element is 1.0 and the last element is less than 2.0.
  • Type: list of floats
  • Default: [1.0, 1.2]
  • Example:
    • alpha = [1.0, 1.2] is equivalent to setting alpha = 1.2 in DiskANN.
    • alpha = [1.0] is equivalent to the default pruning strategy in HNSW.
  • Note: this option is ineffective when the distance metric is negative inner product.

beam_construction

since v0.5.0
  • Description: Beam width used during insertion. Beam width refers to the number of vertices accessed at once. Since the time to randomly read a small number of sectors from the SSD is almost the same as reading a single sector, a larger beam width effectively reduces the number of round trips to the SSD, resulting in better performance. Since it increases computation, it is disadvantageous for indexes that fit entirely in memory.
  • Type: integer
  • Default: 1
  • Example:
    • beam_construction = 8 means that the index accesses 8 vertices at once during insertion.
    • beam_construction = 1 means that the index accesses 1 vertex at once during insertion.

Search Parameters (vchordg)

vchordg

vchordg.enable_scan

since v0.5.0
  • Description: Enables or disables the query planner’s use of vchordg index scan. It’s for testing purposes.
  • Type: boolean
  • Default: on
  • Example:
    • vchordg.enable_scan = off disables the query planner’s use of vchordg index scan.
    • vchordg.enable_scan = on enables the query planner’s use of vchordg index scan.

since v0.5.0
  • Description: The size of the dynamic list containing the nearest neighbors in search. The larger vchordg.ef_search is, the better the recall, but the worse the QPS. vchordg.ef_search corresponds to ef\text{ef} in HNSW and DiskANN.
  • Type: integer
  • Default:
  • Domain: [1, 65535]
  • Example:
    • SET vchordg.ef_search = 64 indicates the size of the dynamic list containing the nearest neighbors is 6464 during search.
    • SET vchordg.ef_search = 128 indicates the size of the dynamic list containing the nearest neighbors is 128128 during search.

since v0.5.0
  • Description: Beam widths used during search. Beam width refers to the number of vertices accessed at once. Since the time to randomly read a small number of sectors from the SSD is almost the same as reading a single sector, a larger beam width effectively reduces the number of round trips to the SSD, resulting in better performance. Since it increases computation, it is disadvantageous for indexes that fit entirely in memory.
  • Type: integer
  • Default: 1
  • Domain: [1, 65535]
  • Example:
    • SET vchordg.beam_search = 8 indicates that the index accesses 8 vertices at once during search.
    • SET vchordg.beam_search = 1 indicates that the index accesses 1 vertex at once during search.

vchordg.max_scan_tuples

since v0.5.0
  • Description: The GUC parameter vchordg.max_scan_tuples controls the maximum number of tuples that can be scanned in a vector search. In most cases, you do not need to set this parameter, because the LIMIT clause serves a similar purpose. However, when a WHERE clause is present, the LIMIT clause applies after filtering, while this GUC parameter applies before filtering. This parameter is intended to prevent performance degradation when the filtering selectivity is very low.
  • Type: integer
  • Default: -1
  • Domain: [-1, 2147483647]
  • Example:
    • SET vchordg.max_scan_tuples = 999 indicates the index scan returns at most 999999 tuples.
    • SET vchordg.max_scan_tuples = -1 indicates no limit on the number of tuples.
  • Note: This parameter has no effect when set to -1.

2.4 - Quantization Types

Quantization can be applied not only to indexes but also directly to vectors. To this end, VectorChord provides data types for quantized vectors.
since v1.1.0

Quantization can be applied not only to indexes but also directly to vectors. To this end, VectorChord provides data types for quantized vectors.

rabitq8 is the data type of a quantized vector. For the same dimension, its cost is about 14\frac{1}{4} that of a full-precision vector (vector) and 12\frac{1}{2} that of a half-precision vector (halfvec). This type is named after its quantization algorithm.1

CREATE TABLE items (id bigserial PRIMARY KEY, embedding rabitq8(3));
INSERT INTO items (embedding) VALUES (quantize_to_rabitq8('[0,0,0]'::vector));
INSERT INTO items (embedding) VALUES (quantize_to_rabitq8('[1,1,1]'::vector));
INSERT INTO items (embedding) VALUES (quantize_to_rabitq8('[2,2,2]'::vector));
--- ...
SELECT id FROM items ORDER BY embedding <-> quantize_to_rabitq8('[0.9,0.9,1.1]'::vector) LIMIT 100;

Like them, an index can be created on columns of type rabitq8, too.

CREATE INDEX ON items USING vchordrq (embedding rabitq8_l2_ops);

By using rabitq8, both the table size and index size become 14\frac{1}{4} of what they would be when the type of the column is vector. The cost is that both performance and precision are compromised by quantization noticeably. Therefore, it is applicable in scenarios that retrieve a large number of results.

In addition, there is also the rabitq4 type. rabitq8 quantized vectors use 88 bits per dimension, whereas rabitq4 quantized vectors use only 44 bits per dimension. It is designed for constrained situations that favor low storage over high precision.

Reference

Operator Classes

Refer to

Types

rabitq8

since v1.1.0
  • Description: The data type of a quantized vector that uses uint8 for internal storage. It’s based on extended RaBitQ.1
  • Textual representation: Unlike vector/halfvec, the textual representation of rabitq8 is more complex. Suppose the dimension is dd; it consists of a concatenation of an array of 44 floating-point numbers enclosed in parentheses and an array of dd uint8 integers enclosed in brackets. For example, (2.999999, 160.63234, 647, 2.8498733)[255, 205, 187]. Its interpretation is internal, but you can get the vector it represents by using dequantize_to_vector(rabitq8)/dequantize_to_halfvec(rabitq8).

rabitq4

since v1.1.0
  • Description: The data type of a quantized vector that uses uint4 for internal storage. It’s based on extended RaBitQ.1
  • Textual representation: Unlike vector/halfvec, the textual representation of rabitq4 is more complex. Suppose the dimension is dd; it consists of a concatenation of an array of 44 floating-point numbers enclosed in parentheses and an array of dd uint4 integers enclosed in brackets. For example, (2.999999, 9.420722, 38, 2.8498733)[15, 12, 11]. Its interpretation is internal, but you can get the vector it represents by using dequantize_to_vector(rabitq4)/dequantize_to_halfvec(rabitq4).

Functions

quantize_to_rabitq8(vector)

since v1.1.0
  • Description: Quantize a vector to rabitq8. Note that this function may produce sightly different results for identical inputs with different versions of VectorChord or with different machines.
  • Result: rabitq8, the quantized vector
  • Arguments:
    • vector, the vector to be quantized
  • Example:
    • SELECT quantize_to_rabitq8('[1,1,1]'::vector)
  • Note: This function does not have a corresponding CAST. If needed, you can create one manually using CREATE CAST (vector AS rabitq8) WITH FUNCTION quantize_to_rabitq8(vector) AS ASSIGNMENT.

quantize_to_rabitq8(halfvec)

since v1.1.0
  • Description: Quantize a vector to rabitq8. Note that this function may produce sightly different results for identical inputs with different versions of VectorChord or with different machines.
  • Result: rabitq8, the quantized vector
  • Arguments:
    • halfvec, the vector to be quantized
  • Example:
    • SELECT quantize_to_rabitq8('[1,1,1]'::halfvec)
  • Note: This function does not have a corresponding CAST. If needed, you can create one manually using CREATE CAST (halfvec AS rabitq8) WITH FUNCTION quantize_to_rabitq8(halfvec) AS ASSIGNMENT.

dequantize_to_vector(rabitq8)

since v1.1.1
  • Description: Dequantize a quantized vector to vector. Note that this function may produce sightly different results for identical inputs with different versions of VectorChord or with different machines. The original vector cannot be precisely reconstructed using this function.
  • Result: vector, the dequantized vector
  • Arguments:
    • rabitq8, the quantized vector to be dequantized
  • Example:
    • SELECT dequantize_to_vector(quantize_to_rabitq8('[1,1,1]'::vector))
  • Note: This function does not have a corresponding CAST. If needed, you can create one manually using CREATE CAST (rabitq8 AS vector) WITH FUNCTION dequantize_to_vector(rabitq8) AS ASSIGNMENT.

dequantize_to_halfvec(rabitq8)

since v1.1.1
  • Description: Dequantize a quantized vector to halfvec. Note that this function may produce sightly different results for identical inputs with different versions of VectorChord or with different machines. The original vector cannot be precisely reconstructed using this function.
  • Result: halfvec, the dequantized vector
  • Arguments:
    • rabitq8, the quantized vector to be dequantized
  • Example:
    • SELECT dequantize_to_halfvec(quantize_to_rabitq8('[1,1,1]'::halfvec))
  • Note: This function does not have a corresponding CAST. If needed, you can create one manually using CREATE CAST (rabitq8 AS halfvec) WITH FUNCTION dequantize_to_halfvec(rabitq8) AS ASSIGNMENT.

quantize_to_rabitq4(vector)

since v1.1.0
  • Description: Quantize a vector to rabitq4. Note that this function may produce sightly different results for identical inputs with different versions of VectorChord or with different machines.
  • Result: rabitq4, the quantized vector
  • Arguments:
    • vector, the vector to be quantized
  • Example:
    • SELECT quantize_to_rabitq4('[1,1,1]'::vector)
  • Note: This function does not have a corresponding CAST. If needed, you can create one manually using CREATE CAST (vector AS rabitq4) WITH FUNCTION quantize_to_rabitq4(vector) AS ASSIGNMENT.

quantize_to_rabitq4(halfvec)

since v1.1.0
  • Description: Quantize a vector to rabitq4. Note that this function may produce sightly different results for identical inputs with different versions of VectorChord or with different machines.
  • Result: rabitq4, the quantized vector
  • Arguments:
    • halfvec, the vector to be quantized
  • Example:
    • SELECT quantize_to_rabitq4('[1,1,1]'::halfvec)
  • Note: This function does not have a corresponding CAST. If needed, you can create one manually using CREATE CAST (halfvec AS rabitq4) WITH FUNCTION quantize_to_rabitq4(halfvec) AS ASSIGNMENT.

dequantize_to_vector(rabitq4)

since v1.1.1
  • Description: Dequantize a quantized vector to vector. Note that this function may produce sightly different results for identical inputs with different versions of VectorChord or with different machines. The original vector cannot be precisely reconstructed using this function.
  • Result: vector, the dequantized vector
  • Arguments:
    • rabitq4, the quantized vector to be dequantized
  • Example:
    • SELECT dequantize_to_vector(quantize_to_rabitq4('[1,1,1]'::vector))
  • Note: This function does not have a corresponding CAST. If needed, you can create one manually using CREATE CAST (rabitq4 AS vector) WITH FUNCTION dequantize_to_vector(rabitq4) AS ASSIGNMENT.

dequantize_to_halfvec(rabitq4)

since v1.1.1
  • Description: Dequantize a quantized vector to halfvec. Note that this function may produce sightly different results for identical inputs with different versions of VectorChord or with different machines. The original vector cannot be precisely reconstructed using this function.
  • Result: halfvec, the dequantized vector
  • Arguments:
    • rabitq4, the quantized vector to be dequantized
  • Example:
    • SELECT dequantize_to_halfvec(quantize_to_rabitq4('[1,1,1]'::halfvec))
  • Note: This function does not have a corresponding CAST. If needed, you can create one manually using CREATE CAST (rabitq4 AS halfvec) WITH FUNCTION dequantize_to_halfvec(rabitq4) AS ASSIGNMENT.

Operators

<->(rabitq8, rabitq8)

since v1.1.0
  • Description: Calculates L2 distance of two quantized vectors.
  • Result: real, L2 distance of two quantized vectors
  • Arguments:
    • rabitq8
    • rabitq8

<#>(rabitq8, rabitq8)

since v1.1.0
  • Description: Calculates negative dot product distance of two quantized vectors.
  • Result: real, negative dot product distance of two quantized vectors
  • Arguments:
    • rabitq8
    • rabitq8

<=>(rabitq8, rabitq8)

since v1.1.0
  • Description: Calculates cosine distance of two quantized vectors.
  • Result: real, cosine distance of two quantized vectors
  • Arguments:
    • rabitq8
    • rabitq8

<->(rabitq4, rabitq4)

since v1.1.0
  • Description: Calculates L2 distance of two quantized vectors.
  • Result: real, L2 distance of two quantized vectors
  • Arguments:
    • rabitq4
    • rabitq4

<#>(rabitq4, rabitq4)

since v1.1.0
  • Description: Calculates negative dot product distance of two quantized vectors.
  • Result: real, negative dot product distance of two quantized vectors
  • Arguments:
    • rabitq4
    • rabitq4

<=>(rabitq4, rabitq4)

since v1.1.0
  • Description: Calculates cosine distance of two quantized vectors.
  • Result: real, cosine distance of two quantized vectors
  • Arguments:
    • rabitq4
    • rabitq4

  1. Jianyang Gao, Yutong Gou, Yuexuan Xu, Yongyi Yang, Cheng Long, Raymond Chi-Wing Wong, “Practical and Asymptotically Optimal Quantization of High-Dimensional Vectors in Euclidean Space for Approximate Nearest Neighbor Search”, SIGMOD 2025, available at https://arxiv.org/abs/2409.09913. ↩︎ ↩︎ ↩︎

2.5 - Similarity Filter

Usually, ORDER BY and LIMIT clauses are used to perform vector search. In some cases, you may want to use a WHERE clause to filter out vectors that are far from

Usually, ORDER BY and LIMIT clauses are used to perform vector search. In some cases, you may want to use a WHERE clause to filter out vectors that are far from the query. Naturally, you might write the following SQL.

EXPLAIN (COSTS FALSE) 
SELECT * FROM items WHERE embedding <-> '[0, 0, 0]' < 0.1 ORDER BY embedding <-> '[0, 0, 0]' LIMIT 10;
                                  QUERY PLAN                                   
-------------------------------------------------------------------------------
 Limit
   ->  Index Scan using items_embedding_idx on items
         Order By: (embedding <-> '[0,0,0]'::vector)
         Filter: ((embedding <-> '[0,0,0]'::vector) < '0.1'::double precision)

The returned results meet expectations, while this approach suffers from poor performance. This is because if fewer than 1010 vectors fall within the distance threshold, PostgreSQL forces the index to continue to search to get 1010 results, exhausting the entire search space.

To avoid this situation, you can use specific syntax to push the filter down to the vector index, allowing it to stop the search as soon as the search region moves beyond the specified distance. We refer to this type of distance-based condition that can be pushed down to the index as a similarity filter.

EXPLAIN (COSTS FALSE) 
SELECT * FROM items WHERE embedding <<->> sphere('[0, 0, 0]'::vector, 0.1) ORDER BY embedding <-> '[0, 0, 0]' LIMIT 10;
                               QUERY PLAN                               
------------------------------------------------------------------------
 Limit
   ->  Index Scan using items_embedding_idx on items
         Index Cond: (embedding <<->> '("[0,0,0]",0.1)'::sphere_vector)
         Order By: (embedding <-> '[0,0,0]'::vector)

embedding <<->> sphere('[0, 0, 0]'::vector, 0.1) evaluates to true if and only if the L2 distance between the two vectors is less than 0.1. Use the <<#>> operator for negative inner product, and the <<=>> operator for cosine distance.

Additionally, if you specify only the WHERE clause without an ORDER BY, the index can still be used effectively.

EXPLAIN (COSTS FALSE) 
SELECT * FROM items WHERE embedding <<->> sphere('[0, 0, 0]'::vector, 0.1);
                            QUERY PLAN                            
------------------------------------------------------------------
 Index Scan using items_embedding_idx on items
   Index Cond: (embedding <<->> '("[0,0,0]",0.1)'::sphere_vector)

Reference

Operator Classes

Refer to

Types

sphere_vector

  • Description: A sphere.
  • Definition: (center vector, radius REAL).

sphere_halfvec

  • Description: A sphere.
  • Definition: (center halfvec, radius REAL).

sphere_rabitq8

since v1.1.0
  • Description: A sphere.
  • Definition: (center rabitq8, radius REAL).

sphere_rabitq4

since v1.1.0
  • Description: A sphere.
  • Definition: (center rabitq4, radius REAL).

Functions

sphere(vector, real)

  • Description: Create a sphere by its center and radius.
  • Definition: SELECT ROW($1, $2).
  • Result: sphere_vector
  • Arguments:
    • vector, the center of the sphere
    • real, the radius of sphere
  • Example:
    • SELECT sphere('[0,0,0]'::vector, 1.0)

sphere(halfvec, real)

  • Description: Create a sphere by its center and radius.
  • Definition: SELECT ROW($1, $2).
  • Result: sphere_halfvec
  • Arguments:
    • halfvec, the center of the sphere
    • real, the radius of sphere
  • Example:
    • SELECT sphere('[0,0,0]'::halfvec, 1.0)

sphere(rabitq8, real)

since v1.1.0
  • Description: Create a sphere by its center and radius.
  • Definition: SELECT ROW($1, $2).
  • Result: sphere_rabitq8
  • Arguments:
    • rabitq8, the center of the sphere
    • real, the radius of sphere
  • Example:
    • SELECT sphere(quantize_to_rabitq8('[0,0,0]'::vector), 1.0)

sphere(rabitq4, real)

since v1.1.0
  • Description: Create a sphere by its center and radius.
  • Definition: SELECT ROW($1, $2).
  • Result: sphere_rabitq4
  • Arguments:
    • rabitq4, the center of the sphere
    • real, the radius of sphere
  • Example:
    • SELECT sphere(quantize_to_rabitq4('[0,0,0]'::vector), 1.0)

Operators

<<->>(vector, sphere_vector)

  • Description: Test if the vector lies inside the sphere using the L2 metric.
  • Result: boolean
  • Arguments:
    • vector, the vector to be tested
    • sphere_vector, the sphere

<<#>>(vector, sphere_vector)

  • Description: Test if the vector lies inside the sphere using the negative dot product metric.
  • Result: boolean
  • Arguments:
    • vector, the vector to be tested
    • sphere_vector, the sphere

<<=>>(vector, sphere_vector)

  • Description: Test if the vector lies inside the sphere using the cosine metric.
  • Result: boolean
  • Arguments:
    • vector, the vector to be tested
    • sphere_vector, the sphere

<<->>(halfvec, sphere_halfvec)

  • Description: Test if the vector lies inside the sphere using the L2 metric.
  • Result: boolean
  • Arguments:
    • halfvec, the vector to be tested
    • sphere_halfvec, the sphere

<<#>>(halfvec, sphere_halfvec)

  • Description: Test if the vector lies inside the sphere using the negative dot product metric.
  • Result: boolean
  • Arguments:
    • halfvec, the vector to be tested
    • sphere_halfvec, the sphere

<<=>>(halfvec, sphere_halfvec)

  • Description: Test if the vector lies inside the sphere using the cosine metric.
  • Result: boolean
  • Arguments:
    • halfvec, the vector to be tested
    • sphere_halfvec, the sphere

<<->>(rabitq8, sphere_rabitq8)

since v1.1.0
  • Description: Test if the vector lies inside the sphere using the L2 metric.
  • Result: boolean
  • Arguments:
    • rabitq8, the vector to be tested
    • sphere_rabitq8, the sphere

<<#>>(rabitq8, sphere_rabitq8)

since v1.1.0
  • Description: Test if the vector lies inside the sphere using the negative dot product metric.
  • Result: boolean
  • Arguments:
    • rabitq8, the vector to be tested
    • sphere_rabitq8, the sphere

<<=>>(rabitq8, sphere_rabitq8)

since v1.1.0
  • Description: Test if the vector lies inside the sphere using the cosine metric.
  • Result: boolean
  • Arguments:
    • rabitq8, the vector to be tested
    • sphere_rabitq8, the sphere

<<->>(rabitq4, sphere_rabitq4)

since v1.1.0
  • Description: Test if the vector lies inside the sphere using the L2 metric.
  • Result: boolean
  • Arguments:
    • rabitq4, the vector to be tested
    • sphere_rabitq4, the sphere

<<#>>(rabitq4, sphere_rabitq4)

since v1.1.0
  • Description: Test if the vector lies inside the sphere using the negative dot product metric.
  • Result: boolean
  • Arguments:
    • rabitq4, the vector to be tested
    • sphere_rabitq4, the sphere

<<=>>(rabitq4, sphere_rabitq4)

since v1.1.0
  • Description: Test if the vector lies inside the sphere using the cosine metric.
  • Result: boolean
  • Arguments:
    • rabitq4, the vector to be tested
    • sphere_rabitq4, the sphere

2.6 - PostgreSQL Tuning

Indexing may be parallelized. Parallelism increases server load and is therefore not enabled by default in PostgreSQL. To enable it, refer to the following SQL.

Indexing may be parallelized. Parallelism increases server load and is therefore not enabled by default in PostgreSQL. To enable it, refer to the following SQL.

-- Sets the maximum number of background processes that the cluster can support.
-- It's recommended to set it to at least the sum of your desired parallelism plus 8.
-- For example, if you want to use 16 threads for parallel processing, set the value at least to 24.
-- Depending on your workload, this value might be useful to be higher.
-- Note: A restart is required for this setting to take effect.
ALTER SYSTEM SET max_worker_processes = 24;

-- Sets the maximum number of workers that the cluster can support for parallel operations.
-- It's recommended to set it to the sum of your desired parallelism minus 1.
-- For example, if you want to use 16 threads for parallel processing, set the value to 15.
SET max_parallel_workers = 15;

-- Sets the maximum number of parallel workers that can be started by a single utility command.
-- It's recommended to set it to the sum of your desired parallelism minus 1.
-- For example, if you want to use 16 threads for parallel processing, set the value to 15.
SET max_parallel_maintenance_workers = 15;

The number of parallel workers also depends on the table’s configuration. By default, this is automatically determined by PostgreSQL. If PostgreSQL identifies it as disabled, parallel indexing will not take effect. You can override PostgreSQL’s behavior by adjusting the table’s parallel_workers setting to enable parallel indexing.

ALTER TABLE items set (parallel_workers = 15);

You need to tune a few settings to improve I/O performance.

-- Sets the number of concurrent disk I/O operations that PostgreSQL expects can be executed simultaneously.
-- It's recommended to set it to 20 when using SSDs for storage.
SET effective_io_concurrency = 20;
SET maintenance_io_concurrency = 20;

We have observed that in certain cases, malfunctioning JIT can negatively impact performance. If you do not rely on it, you may choose to disable it.

SET jit = off;

Finally, you need to set the size of PostgreSQL’s shared buffers.

-- Sets the amount of memory the database server uses for shared memory buffers.
-- Depending on your workload, this value is recommended to be 25%~40% of the total memory.
-- For vector database workloads, this value might be useful to be 80% of the total memory.
-- Note: A restart is required for this setting to take effect.
ALTER SYSTEM SET shared_buffers = '16GB';

Asynchronous I/O

PostgreSQL 17 introduced the streaming I/O API for the first time. Building on this, PostgreSQL 18 introduces asynchronous I/O. With asynchronous I/O, PostgreSQL can initiate multiple read operations and continue processing while waiting for the results. This can improve the performance of queries that need to read large amounts of data from disk. PostgreSQL offers multiple asynchronous I/O methods. You can adjust them as needed.

This section applies only to PostgreSQL 18.

io_uring

io_uring method uses Linux’s io_uring, which can reduce the overhead of context switches. This interface is available in kernel 5.1 and later. You can enable this method through the GUC io_method.

-- Note: A restart is required for this setting to take effect.
ALTER SYSTEM SET io_method = 'io_uring';

Since io_uring uses additional file descriptors and locked memory, PostgreSQL may reach RLIMIT_NOFILE and RLIMIT_MEMLOCK limits. You may need to increase these limits on your system. See also

For security reasons, container runtimes, cloud providers, and system administrators may disable io_uring. If this happens, check with your vendor for details. See also

io_uring is often considered recommended. However, performance depends on your workload. See also

This section applies only to Linux.

worker

This is the default in PostgreSQL 18. PostgreSQL avoids blocking backend by sending I/O requests to a process pool. You can explicitly enable this method through the GUC io_method.

-- Note: A restart is required for this setting to take effect.
ALTER SYSTEM SET io_method = 'worker';

-- Selects the number of I/O worker processes to use.
-- It's recommended to set it to the minimum of your parallelism and maximum value.
-- For example, if your parallelism is 16, set the value to 16.
-- The maximum value is 32, so you can't set it to anything higher.
-- Note: A restart is required for this setting to take effect.
ALTER SYSTEM SET io_workers = 16;

sync

sync method works the same way as in PostgreSQL 17. Reads are synchronous and blocking, and posix_fadvise is used to provide the operating system with hints about upcoming reads. You would only use this method for debugging. You can enable this method through the GUC io_method.

-- Note: A restart is required for this setting to take effect.
ALTER SYSTEM SET io_method = 'sync';

2.7 - Monitoring

You can monitor the progress of building a PostgreSQL index by querying the pgstatprogresscreateindex view.

You can monitor the progress of building a PostgreSQL index by querying the pg_stat_progress_create_index view.

SELECT command, phase, round(100.0 * blocks_done / nullif(blocks_total, 0), 1) AS "%" FROM pg_stat_progress_create_index;

Reference

Phases (vchordrq)

vchordrq

There are 5 steps in the index construction process of vchordrq. The phase name of each step are as follows:

Step Message Description Waiting time
1 initializing Start building index Short
2 initializing index, by default build This step is only for default build Short
2 initializing index, by internal build This step is only for internal build. It involves table sampling and performing partitioning to obtain centroids Long
2 initializing index, by external build This step is only for external build. It loads centroids from a specified table Short
3 initializing index Initialize the data structure and storage of the index Short
4 inserting tuples from table to index Assign all vectors to their corresponding lists Long
5 compacting tuples in index Optimize the structure of the vector index to enhance performance Medium

The 4th step, inserting tuples from table to index, takes up the majority of the time during index construction. The tuples_done, blocks_done and blocks_total columns indicate the progress of this step.

Phases (vchordg)

vchordg

There are 2 steps in the index construction process of vchordg. The phase name of each step are as follows:

Step Message Description Waiting time
1 initializing Start building index Short
2 inserting tuples from table to index Insert all vectors into the graph Long

The 2nd step, inserting tuples from table to index, takes up the majority of the time during index construction. The tuples_done, blocks_done and blocks_total columns indicate the progress of this step.

2.8 - Fallback Parameters

Search parameters are typically configured via GUCs. VectorChord also offers an alternative method, allowing these parameters to be attached directly to an inde
since v1.1.0

Search parameters are typically configured via GUCs. VectorChord also offers an alternative method, allowing these parameters to be attached directly to an index instead of a session or transaction. In other words, you can store them as part of the index’s storage parameters.

You could set probes at index building using the following syntax.

CREATE INDEX items_embedding_idx ON items USING vchordrq (embedding vector_l2_ops) WITH (options = $$
build.internal.lists = [1000]
$$, probes = '10');

You can now perform searches without setting GUCs:

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

If the index is already built, you can set probes like this:

ALTER INDEX items_embedding_idx SET (probes = '100');

You can still set GUCs for search parameters. When you do so, the GUCs take precedence over the index storage parameters. This allows you to temporarily override these settings for the current session or transaction without locking the index.

SET vchordrq.probes TO '100';
--- probes = '100'
SELECT * FROM items ORDER BY embedding <-> '[3,1,2]' LIMIT 5;

This syntax is useful in the following situations:

  • When there are multiple indexes.
  • When you prefer not to set GUCs.
  • When you want to configure search parameters offline.

As long as GUCs are set, even at the system level or database level, they will override the index settings. So, to avoid hard-to-detect issues, we recommend using only one method to set search parameters, either via GUCs or via index storage parameters.

Not all parameters support this feature. See Reference for all supported parameters.

Reference

Index Storage Parameters (vchordrq)

vchordrq

probes

since v1.1.0
  • Description: Fallback value of vchordrq.probes.
  • Type: list of integers
  • Default:

epsilon

since v1.1.0
  • Description: Fallback value of vchordrq.epsilon.
  • Type: real
  • Default: 1.9
  • Domain: [0.0, 4.0]

maxsim_refine

since v1.1.0

maxsim_threshold

since v1.1.0

Index Storage Parameters (vchordg)

vchordg

since v1.1.0
  • Description: Fallback value of vchordg.ef_search.
  • Type: integer
  • Default: 64
  • Domain: [1, 65535]

2.9 - Measure Recall

In the context of vector search, recall is the ratio of true nearest neighbors to approximate nearest neighbors returned by the index. For example, if the index
since v0.5.0

In the context of vector search, recall is the ratio of true nearest neighbors to approximate nearest neighbors returned by the index. For example, if the index retrieves 100100 approximate nearest neighbors and 9797 of them are true nearest neighbors, then the recall is 97100=0.97\frac{97}{100} = 0.97.

Tip

This feature is in preview.

Recall Evaluation

since v0.5.0

Recall measures the ratio of relevant results to all results retrieved from a search. When using an index, you likely want to know the recall and QPS corresponding to different parameters. The function vchordrq_evaluate_query_recall is designed for evaluating recall. It receives a query that returns row identifiers and returns the corresponding recall.

The vchordrq_evaluate_query_recall function internally relies on ctid to compare the query results with the ground truth. Therefore, the query argument must start with SELECT ctid.

SET vchordrq.probes = '100';
SET vchordrq.epsilon = 1.0;
SELECT vchordrq_evaluate_query_recall(query => $$
  SELECT ctid FROM items ORDER BY embedding <-> '[3,1,2]' LIMIT 10
$$);

By default, the function evaluates the recall by generating estimated ground truth with another index scan. If you would like to evaluate the recall by generating ground truth with a table scan, set the function parameter exact_search to true.

SET vchordrq.probes = '100';
SET vchordrq.epsilon = 1.0;
SELECT vchordrq_evaluate_query_recall(query => $$
  SELECT ctid FROM items ORDER BY embedding <-> '[3,1,2]' LIMIT 10
$$, exact_search => true);

Query sampling

since v0.5.2

To evaluate query recall, VectorChord includes a built-in method to sample queries.

SET vchordrq.query_sampling_enable = on;
SET vchordrq.query_sampling_max_records = 1000;
SET vchordrq.query_sampling_rate = 1;

Suppose you have executed an example vector search SELECT * FROM items ORDER BY embedding <-> '[3,1,2]' LIMIT 10.

After queries have been executed and sampled, you can inspect the captured components of them, including the schema, index, table, column, operator, and vector.

SELECT * from vchordrq_sampled_queries('items_embedding_idx');
-- schema_name | index_name          | table_name | column_name | operator |    value
---------------+---------------------+------------+-------------+----------+--------------
-- public      | items_embedding_idx | items      | embedding   | <->      | [0.5,0.25,1]

To measure the recall of the recorded queries, you can construct the target query statement.

SELECT AVG(recall_value) FROM (
    SELECT vchordrq_evaluate_query_recall(
            format(
                'SELECT ctid FROM %I.%I ORDER BY %I OPERATOR(%s) %L LIMIT 10',
                lq.schema_name,
                lq.table_name,
                lq.column_name,
                lq.operator,
                lq.value
            )
    ) AS recall_value
    FROM vchordrq_sampled_queries('items_embedding_idx') AS lq
) AS eval_results;

If you are using the replication feature with a primary server and standby servers, please note that sampled queries are not synchronized by replication or backup. Rather, each server maintains its own set of queries based on those it processes.

Reference

This feature is not supported by vchordg and Multi-Vector Retrieval.

Search Parameters (vchordrq)

vchordrq

vchordrq.query_sampling_enable

since v0.5.2
  • Description: Enables or disables the query sampling of vchordrq index scan.
  • Type: boolean
  • Default: off

vchordrq.query_sampling_max_records

since v0.5.2
  • Description: Controls the maximum number of queries recorded per index. This setting is only active when vchordrq.query_sampling_enable is on.
  • Type: integer
  • Default: 0
  • Domain: [0, 10000]
  • Example:
    • vchordrq.query_sampling_max_records = 0 will maintain 0 queries for each index.
    • vchordrq.query_sampling_max_records = 1000 will maintain up to the 1000 most recent queries for each index.

vchordrq.query_sampling_rate

since v0.5.2
  • Description: Controls the sampling rate at which queries are recorded. This setting is only active when vchordrq.query_sampling_enable is on.
  • Type: real
  • Default: 0
  • Domain: [0, 1]
  • Example:
    • vchordrq.query_sampling_rate = 0 records no query.
    • vchordrq.query_sampling_rate = 0.01 records approximately 1% of queries.
    • vchordrq.query_sampling_rate = 1 records every query. It’s for testing purposes.

Functions (vchordrq)

vchordrq

vchordrq_sampled_queries

since v0.5.2
  • Description: Retrieves the set of recorded queries for a vchordrq index.
  • Result: table
    • schema_name: text, the schema of the table in the query
    • index_name: text, the name of the index used in the query
    • table_name: text, the name of the table in the query
    • column_name: text, the column involved in the index scan
    • operator: text, the distance operator used in the query.
    • value: text, the text representation of the target vector from the query
  • Arguments:
    • regclass, the object identifier of a vchordrq index
  • Example:
    • SELECT vchordrq_sampled_queries('items_embedding_idx');

vchordrq_sampled_values

since v0.5.2
  • Description: Retrieves the target vector of recorded queries for a vchordrq index.
  • Result: text, the text representation of the target vector from the query.
  • Arguments:
    • regclass, the object identifier of a vchordrq index
  • Example:
    • SELECT vchordrq_sampled_values('items_embedding_idx');
  • Note: this view is only intended for debug.

vchordrq_evaluate_query_recall

since v0.5.0
  • Description: Evaluates the recall of a given vchordrq query.
  • Result: real
  • Arguments:
    • query: text, the query whose recall is evaluated
    • exact_search: boolean, use ground truth instead of estimated ground truth
    • accu_probes: text, vchordrq.probes for estimated ground truth
    • accu_epsilon: real, vchordrq.epsilon for estimated ground truth
  • Example:
    • SELECT vchordrq_evaluate_query_recall(query => $$ SELECT ctid FROM t ORDER BY val <-> '[0.5, 0.25, 1.0]' LIMIT 10 $$);
    • SELECT vchordrq_evaluate_query_recall(query => $$ SELECT ctid FROM t ORDER BY val <-> '[0.5, 0.25, 1.0]' LIMIT 10 $$, exact_search => true);
    • SELECT vchordrq_evaluate_query_recall(query => $$ SELECT ctid FROM t ORDER BY val <-> '[0.5, 0.25, 1.0]' LIMIT 10 $$, accu_probes => '9999', accu_epsilon => 4.0);

Views (vchordrq)

vchordrq

vchordrq_sampled_queries

since v0.5.2
  • Description: Retrieves the set of recorded queries for all vchordrq indexes.
  • Example:
    • SELECT * from vchordrq_sampled_queries;
  • Note: this view is only intended for debug.

2.10 - Prewarm

After PostgreSQL starts, tables and indexes are loaded from storage into the buffer pool only when they are accessed for the first time. As queries are executed

After PostgreSQL starts, tables and indexes are loaded from storage into the buffer pool only when they are accessed for the first time. As queries are executed, the frequently accessed parts of the index are naturally cached in the buffer pool. However, as initial queries are executed, the buffer pool is empty, which causes the queries to be slow.

The PostgreSQL extension pg_prewarm includes the pg_prewarm function, which can partially address this issue. After PostgreSQL starts, you simply provide the name of the index to this function, and it will load the index from storage into the buffer pool.

SELECT pg_prewarm('items_pkey');

However, it only works if the size of the buffer pool is much larger than the size of the index. For vector indexes, the index size is often much larger than memory size. Vector indexes are designed so that the index data is divided into two parts: a small, hot portion and a large, cold portion. Vector indexes only need the hot portion in memory to achieve reasonable performance. Therefore, a specialized prewarm function exists and it loads only the hot portion of the index into memory.

SELECT vchordrq_prewarm('items_embedding_idx');

It works well even if the index size is much larger than memory size.

Reference

Functions (vchordrq)

vchordrq

vchordrq_prewarm

  • Description: This function warms the vchordrq index by loading index to buffer pool.
  • Result: text
  • Arguments:
    • regclass, the object identifier of a vchordrq index
  • Example:
    • SELECT vchordrq_prewarm('items_embedding_idx')

Functions (vchordg)

vchordg

vchordg_prewarm

since v0.5.0
  • Description: This function warms the vchordg index by loading index to buffer pool.
  • Result: text
  • Arguments:
    • regclass, the object identifier of a vchordg index
  • Example:
    • SELECT vchordg_prewarm('items_embedding_idx')

2.11 - Prefilter

In a filtered index scan, the index performs the scan first and then PostgreSQL checks whether the filter conditions are satisfied. On this page, “filter” refer
since v0.4.0

In a filtered index scan, the index performs the scan first and then PostgreSQL checks whether the filter conditions are satisfied. On this page, “filter” refers to the filter on the index scan node of the query plan in PostgreSQL. This is usually constructed from the WHERE clause in the SQL statement. The filter does not need to have a specific form.

EXPLAIN (COSTS FALSE)
SELECT * FROM items WHERE id <= 5000 AND vector_norm(embedding) < 0.5;
                          QUERY PLAN                          
--------------------------------------------------------------
 Index Scan using items_pkey on items
   Index Cond: (id <= 5000)
   Filter: (vector_norm(embedding) < '0.5'::double precision)

However, this is not always the most efficient approach for vector search. Consider the following query.

EXPLAIN (COSTS FALSE) 
SELECT * FROM items WHERE id % 97 = 0 ORDER BY embedding <-> '[0, 0, 0]' LIMIT 10;
                     QUERY PLAN                      
-----------------------------------------------------
 Limit
   ->  Index Scan using items_embedding_idx on items
         Order By: (embedding <-> '[0,0,0]'::vector)
         Filter: ((id % '97'::bigint) = 0)

To retrieve 1010 vectors that satisfy the filter condition, the index returned approximately 970970 rows. This suggests that the index executed a large amount of redundant computation. If the index could leverage the filter condition for pruning, the computation would be significantly reduced. The vector index provides a GUC parameter vchordrq.prefilter that allows pruning of the search space based on the filter condition.

SET vchordrq.prefilter = on;

Prefilter enables the vector index to perform the search based on the filter. This prunes the search space, and a smaller search space leads to a more efficient search. However, checking whether the filter conditions are satisfied also introduces overhead. So prefilter is only recommended when the filter is strict (eliminating many rows) and cheap (computational cost is much lower than computing vector distances). To aid understanding, we present two incorrect usage examples:

  • id % 97 > 0: this filter is relaxed
  • email ~ '^([a-zA-Z]+)*$': this filter is expensive

Based on our experimental results, the QPS speedup at different selectivity is as follows:

  • 200% speedup at 1% selectivity
  • 5% speedup at 10% selectivity
Prefilter on LAION-5m

Reference

This feature is not supported by vchordg.

Search Parameters (vchordrq)

vchordrq

vchordrq.prefilter

since v0.4.0
  • Description: The vchordrq.prefilter GUC parameter enables condition evaluation before distance computation. For example, in the query SELECT * FROM items WHERE id % 2 = 0 ORDER BY embedding <-> '[3,1,2]' LIMIT 5, the index normally computes all useful embedding <-> '[3,1,2]' distances first and then pass the rows to PostgreSQL, which filters out rows where id % 2 != 0. This parameter allows the index to pre-evaluate the condition and discard non-matching rows before computing their distances, improving query efficiency.
  • Type: boolean
  • Default: false

2.12 - Prefetch

Prefetch dramatically accelerates queries that involve I/O operations. It works by proactively reading buffers into operating system page cache or PostgreSQL’s
since v0.4.0

Prefetch dramatically accelerates queries that involve I/O operations. It works by proactively reading buffers into operating system page cache or PostgreSQL’s buffer pool based on the actual needs of the index.

Prefetch helps overlap I/O with computation, as well as I/O with other I/O operations, significantly reducing the time spent waiting for buffers during query execution, thereby improving overall query performance.

Tip

This feature is enabled by default. Any query can benefit from prefetching with VectorChord 0.4.0 and later versions.

Prior to version 0.4.0, VectorChord used PostgreSQL’s synchronous I/O interface. Since then, VectorChord utilizes asynchronous I/O interface on newer versions of PostgreSQL. The I/O interface used by VectorChord can be controlled via GUC parameters.

In the GUC parameters, the synchronous I/O interface is referred to as read_buffer, the asynchronous I/O interface as read_stream, and VectorChord’s simulated asynchronous interface as prefetch_buffer.

For indexes much smaller than the memory, there is almost no difference among the three prefetch modes. read_stream accelerates all disk-based indexes; however, it is only available on PostgreSQL 17 and 18. To address this, VectorChord provides a simulated asynchronous I/O interface called prefetch_buffer, which is available on all PostgreSQL versions as a fallback for older versions. prefetch_buffer is less effective than read_stream.

Prefetch Mode Description Supported PostgreSQL Versions
read_stream Asynchronous I/O interface in PostgreSQL PostgreSQL 17, 18
prefetch_buffer VectorChord’s simulated asynchronous I/O interface used as fallback on older PostgreSQL versions PostgreSQL 13, 14, 15, 16, 17, 18
read_buffer Synchronous I/O interface in PostgreSQL PostgreSQL 13, 14, 15, 16, 17, 18

To fully leverage this feature with read_stream, you need to enable asynchronous I/O on PostgreSQL 18. See also PostgreSQL Tuning.

Reference

Search Parameters (vchordrq)

vchordrq

since v0.4.0
  • Description: This GUC parameter controls the I/O prefetching strategy for reading bit vectors in vector search, which can impact search performance on disk-based vectors.
  • Type: string
  • Domain: Depends on PostgreSQL version
    • PostgreSQL 13, 14, 15, 16: {"read_buffer", "prefetch_buffer"}
    • PostgreSQL 17, 18: {"read_buffer", "prefetch_buffer", "read_stream"}
  • Default: Depends on PostgreSQL version
    • PostgreSQL 13, 14, 15, 16: prefetch_buffer
    • PostgreSQL 17, 18: read_stream
  • Note:
    • read_buffer indicates a preference for ReadBuffer.
    • prefetch_buffer indicates a preference for both PrefetchBuffer and ReadBuffer.
    • read_stream indicates a preference for read_stream.

vchordrq.io_rerank

since v0.4.0
  • Description: This GUC parameter controls the I/O prefetching strategy for reading full precision vectors in vector search, which can significantly impact search performance on disk-based vectors.
  • Type: string
  • Domain: Depends on PostgreSQL version
    • PostgreSQL 13, 14, 15, 16: {"read_buffer", "prefetch_buffer"}
    • PostgreSQL 17, 18: {"read_buffer", "prefetch_buffer", "read_stream"}
  • Default: Depends on PostgreSQL version
    • PostgreSQL 13, 14, 15, 16: prefetch_buffer
    • PostgreSQL 17, 18: read_stream
  • Note:
    • read_buffer indicates a preference for ReadBuffer.
    • prefetch_buffer indicates a preference for both PrefetchBuffer and ReadBuffer.
    • read_stream indicates a preference for read_stream.

Search Parameters (vchordg)

vchordg

since v0.5.0
  • Description: This GUC parameter controls the I/O prefetching strategy for reading bit vectors in vector search, which can impact search performance on disk-based vectors.
  • Type: string
  • Domain: Depends on PostgreSQL version
    • PostgreSQL 13, 14, 15, 16: {"read_buffer", "prefetch_buffer"}
    • PostgreSQL 17, 18: {"read_buffer", "prefetch_buffer", "read_stream"}
  • Default: Depends on PostgreSQL version
    • PostgreSQL 13, 14, 15, 16: prefetch_buffer
    • PostgreSQL 17, 18: read_stream
  • Note:
    • read_buffer indicates a preference for ReadBuffer.
    • prefetch_buffer indicates a preference for both PrefetchBuffer and ReadBuffer.
    • read_stream indicates a preference for read_stream.

vchordg.io_rerank

since v0.5.0
  • Description: This GUC parameter controls the I/O prefetching strategy for reading full precision vectors in vector search, which can significantly impact search performance on disk-based vectors.
  • Type: string
  • Domain: Depends on PostgreSQL version
    • PostgreSQL 13, 14, 15, 16: {"read_buffer", "prefetch_buffer"}
    • PostgreSQL 17, 18: {"read_buffer", "prefetch_buffer", "read_stream"}
  • Default: Depends on PostgreSQL version
    • PostgreSQL 13, 14, 15, 16: prefetch_buffer
    • PostgreSQL 17, 18: read_stream
  • Note:
    • read_buffer indicates a preference for ReadBuffer.
    • prefetch_buffer indicates a preference for both PrefetchBuffer and ReadBuffer.
    • read_stream indicates a preference for read_stream.

2.13 - Rerank in Table

PostgreSQL indexes store row identifiers and the values of the indexed columns. Typically, indexed columns have simple data types, for example, integers or stri
since v0.2.1

PostgreSQL indexes store row identifiers and the values of the indexed columns. Typically, indexed columns have simple data types, for example, integers or strings with short length limits.

However, if the indexed column is of a vector type, both the table and the index store vectors, which means you pay twice the storage cost for the vectors. If you would like to reduce this cost, you can enable rerank_in_table when building the index. The index will then use a tricky approach where it does not store the vectors itself but reads them from the table when needed.

CREATE INDEX ON items USING vchordrq (embedding vector_l2_ops) WITH (options = $$
rerank_in_table = true
residual_quantization = true
$$);

Enabling this option results in degraded query performance and prefetch does not work when rerank_in_table is enabled.

It’s a trade-off of time for space, which is generally not recommended, so it’s intended only to address the specific issue of excessively high storage usage.

Reference

This feature is not supported by vchordg.

Indexing Options (vchordrq)

vchordrq

rerank_in_table

since v0.2.1
  • Description: This option determines whether the vectors are fetched from the table. If so, the index will require less storage, but the query latency will increase significantly. It should only be enabled when disk space is extremely limited.
  • Type: boolean
  • Default: false
  • Example:
    • rerank_in_table = false means that vectors are stored in both the index and the table, and fetched from the index in search.
    • rerank_in_table = true means that vectors are stored in the table only, and fetched from the table in search.

2.14 - Partitioning Tuning

For large tables, the partitioning phase dominates both build time and memory consumption.

For large tables, the partitioning phase dominates both build time and memory consumption.

Time complexity of partitioning is O(FC2DL)O(FC^{2}DL), and the memory consumption in bytes can be approximated by 4DC(F+T+1)4DC(F + T + 1),

where:

  • DD is the vector dimension, or build.internal.kmeans_dimension if set.
  • FF is build.internal.sampling_factor.
  • CC is build.internal.lists[-1].
  • TT is build.internal.build_threads.
  • LL is build.internal.kmeans_iterations.

This page introduces some tuning techniques to improve build time and memory consumption.

Hierarchical K-means

Hierarchical K-means is an algorithm that is faster than Lloyd’s algorithm. When applied this algorithm, the asymptotic time complexity is reduced to O(FC1.5DL)O(FC^{1.5}DL). As a trade-off, it has a slight negative impact on search performance and recall.

Usage:

build.internal.kmeans_algorithm.hierarchical = {}

Reduce Sampling Factor

Reducing sampling factor improves build time and memory consumption linearly. As a trade-off, it has a moderate negative impact on search performance and recall.

Usage:

build.internal.sampling_factor = 64

Dimensionality reduction

Dimensionality reduction uses the fast Johnson–Lindenstrauss transform, reducing the dimensionality of the vectors involved in clustering to below their original dimension, thereby improving build time and memory consumption linearly. As a trade-off, it has a slight negative impact on search performance and recall.

Usage:

build.internal.kmeans_dimension = 100

Reference settings

LAION 100M

Dataset:

Test machine (AWS EC2 i7i.4xlarge):

  • CPU: Intel Xeon Platinum 8559C
  • Number of vCPUs: 16
  • Memory: 128 GiB
  • Storage: 3750 GiB NVMe SSD
CREATE INDEX ON laion USING vchordrq (embedding vector_ip_ops) WITH (options = $$
build.pin = 2
build.internal.lists = [400, 160000]
build.internal.build_threads = 16
build.internal.spherical_centroids = true
build.internal.kmeans_algorithm.hierarchical = {}
build.internal.kmeans_dimension = 100
build.internal.sampling_factor = 64
$$);

Result:

  • Total Build Time: 18 minutes
  • Peak Memory: 6 GiB
  • Test Case 1
    • Parameters
      • vchordrq.probes = '40,140'
      • vchordrq.epsilon = 1.4
    • QPS @ Top10: 120
    • Recall @ Top10: 0.949

DEEP 1B

Dataset:

Test machine (AWS EC2 i7ie.6xlarge):

  • CPU: Intel Xeon Platinum 8559C
  • Number of vCPUs: 24
  • Memory: 192 GiB
  • Storage: 7500 GiB NVMe SSD
CREATE INDEX ON deep USING vchordrq (embedding vector_l2_ops) WITH (options = $$
build.pin = 2
residual_quantization = true
build.internal.lists = [800, 640000]
build.internal.build_threads = 24
build.internal.kmeans_algorithm.hierarchical = {}
$$);

Result:

  • Total Build Time: 107 minutes
  • Peak Memory: 64 GiB
  • Test Case 1
    • Parameters
      • vchordrq.probes = '40,250'
    • QPS @ Top10: 68
    • Recall @ Top10: 0.951

2.15 - External Build

In addition to using the methods in Partitioning Tuning to achieve shorter build time and lower memory consumption at the cost of QPS and recall, you can also c

In addition to using the methods in Partitioning Tuning to achieve shorter build time and lower memory consumption at the cost of QPS and recall, you can also choose to run the partitioning phase offline on other machines or on GPUs. This feature is not supported by vchordg, since it does not have a partitioning phase.

Assume the table is t and the column to be indexed is val.

CREATE TABLE t (val vector(3));

Specifically, you need to sample the column for which the index will be built.

CREATE EXTENSION IF NOT EXISTS tsm_system_rows;
SELECT val FROM t TABLESAMPLE SYSTEM_ROWS(1000);

Based on these samples, you can partition the vector space into Voronoi cells. After that, you will create a table and insert your partitions.

CREATE TABLE public.t_build (id INTEGER NOT NULL UNIQUE, parent INTEGER, vector vector NOT NULL);
INSERT INTO public.t_build (id, parent, vector) VALUES (0, NULL, '{0.1, 0.2, 0.3}');
INSERT INTO public.t_build (id, parent, vector) VALUES (1, 0, '{0.1, 0.2, 0.3}');
INSERT INTO public.t_build (id, parent, vector) VALUES (2, 0, '{0.4, 0.5, 0.6}');
INSERT INTO public.t_build (id, parent, vector) VALUES (3, 0, '{0.7, 0.8, 0.9}');

The index can be created using the following syntax.

CREATE INDEX ON t USING vchordrq (embedding vector_l2_ops) WITH (options = $$
build.external.table = 'public.t_build'
$$);

Format

The table that stores the partition information must strictly follow the following schema:

  • The first column must be id. Its type is integer. It must not be null. It must be unique.
  • The second column must be parent. Its type is integer. It could not be null.
  • The third column must be vector. Its type is vector. It must not be null. halfvec or other types are not supported yet.

Logically, this forms a tree. The distance from the root to each leaf must be the same.

If any of the above properties are not satisfied, an error will be reported.

Partitioning

To get started, here is a minimal code example for performing partitioning using faiss.

from typing import List
from faiss import Kmeans
import numpy

def partition(
    samples: List[numpy.Array[float]], lists: List[int]
) -> List[numpy.Array[float]]:
    dim = dataset.shape[1]
    results = []
    for i in range(len(lists) + 1):
        kmeans = Kmeans(
            dim,
            lists[len(lists) - 1 - i] if len(lists) - 1 - i >= 0 else 1,
            gpu=False,
            verbose=True,
            niter=10,
            seed=42,
            spherical=False,
        )
        kmeans.train(samples)
        results.push(kmeans.centroids)
        samples = kmeans.centroids
    return results

This computes the generators of all Voronoi cells across different levels. The parent-child relationships in the tree are determined by computing the shortest distances, but the details are omitted here. Any algorithm capable of generating multi-level Voronoi cells can be used, such as spherical K-means, balanced K-means or GPU K-means.

3 - Use Cases

End-to-end recipes for hybrid BM25 + dense search, ColBERT reranking, and multimodal OCR-free RAG.
Title Description Link Code
Hybrid search with Postgres Native BM25 and VectorChord This article explores how to implement Hybrid Search in Postgres using VectorChord-bm25 for keyword-based retrieval and VectorChord for semantic search. Hybrid Search Github Repo
Supercharge vector search with ColBERT rerank in PostgreSQL This blog will demonstrate using the PostgreSQL extension VectorChord and pgvector with ColBERT rerank. ColBERT Rerank Github Repo
Beyond Text: Unlock OCR-Free RAG in PostgreSQL with Modal & VectorChord This post is your guide to building exactly that: an OCR-free RAG system directly within your familiar PostgreSQL database. We’ll leverage the power of the ColQwen2 Vision Language Model, the efficiency of VectorChord for multi-vector search in Postgres, and the scalability of Modal for GPU-powered embedding generation. OCR-Free RAG Github Repo

3.1 - Hybrid search with Postgres Native BM25 and VectorChord

In the era of RAG (Retrieval-Augmented Generation), efficiently retrieving relevant data from vast datasets is crucial for businesses and developers. Traditiona

In the era of RAG (Retrieval-Augmented Generation), efficiently retrieving relevant data from vast datasets is crucial for businesses and developers. Traditional keyword-based retrieval methods, such as those utilizing BM25 as a scoring mechanism, excel in ranking documents based on term frequency and exact keyword matches. This makes them highly effective for structured queries and keyword-heavy content. However, they struggle with understanding synonyms, contextual nuances, and semantic intent. Conversely, vector-based retrieval captures deep semantic meaning, enabling better generalization across varied queries but sometimes sacrificing precision in exact keyword matches. Hybrid Search bridges this gap by combining BM25’s precision with vector search’s contextual understanding, delivering faster, more accurate, and semantically aware results.

This article explores how to implement Hybrid Search in Postgres using VectorChord-bm25 for keyword-based retrieval and VectorChord for semantic search. By leveraging VectorChord-bm25’s BM25 ranking with the Block-WeakAnd algorithm and VectorChord’s advanced vector similarity capabilities, you can build a robust search system that seamlessly integrates keyword precision with semantic understanding. Whether you’re building a recommendation engine, a document retrieval system, or an enterprise search solution, this guide will walk you through the steps to unlock the full potential of hybrid search.

All related benchmark codes can be found here.

Hybrid Search Explained

Hybrid Search

Hybrid Search merges the results from vector search and keyword search. Vector search is based on the semantic similarity between the query and the documents, while keyword search relies on the BM25 ranking algorithm. However, it is important to note that BM25 itself is only responsible for scoring documents, while the broader keyword search process also includes steps such as tokenization, indexing, and query parsing. Hybrid Search utilizes reranker or fuses the results from both methods. Fuses include RRF (Reciprocal Rank Fusion) or weighted scoring for multiple recall strategies. Model-based rerank includes cross encoder models like bge-reranker-v2-m3 and multi-vector representation model ColBERT.

What is the VectorChord?

You may be familiar with pgvector, an open-source vector similarity search extension for Postgres. However, scalable or performance-critical scenarios may require a more advanced vector search solution. VectorChord is an excellent choice.

Check out the blog post “VectorChord: Store 400k Vectors for $1 in PostgreSQL” to learn more about its motivation and design.

What is VectorChord-BM25

VectorChord-BM25 is a PostgreSQL extension for keyword search. It not only implements BM25 ranking but also includes a tokenizer and a Block-WeakAnd index to improve speed.

What is BM25?

BM25 (Best Matching 25) is a probabilistic ranking function used in information retrieval to assess how well a document matches a query. It calculates relevance scores based on term frequency (TF) and inverse document frequency (IDF), while also applying document length normalization. The formula ensures that terms appearing frequently within a document (TF) and those rare across the corpus (IDF) are appropriately weighted, improving search accuracy and relevance. IDF measures how often a word appears across the document collection. The fewer the occurrences, the higher its value. TF represents the frequency of a specific word from the query appearing in the given document. A higher TF indicates a stronger relevance between the query and the document.

Why a Postgres native BM25 Ranking implementation is needed?

In our previous blog post, VectorChord-BM25: Revolutionizing PostgreSQL Search with BM25 Ranking — 3x Faster Than Elasticsearch, my colleague Allen has already provided a detailed and sufficient explanation, which can be referred to.

Hybrid Search 1

In this tutorial, we will guide you through the steps to implement Hybrid Search using VectorChord-BM25 and VectorChord in PostgreSQL. We will cover the following topics:

  • Semantic Search with VectorChord

  • Keyword Search with VectorChord-BM25

  • Rerank

Prerequisites

  1. Postgres with VectorChord-BM25 and VectorChord

If you want to reproduce the tutorial quickly, you can use the tensorchord/vchord-suite image to run multiple extensions which are provided by TensorChord.

You can run the following command to build and start the Postgres with VectorChord-BM25 and VectorChord.

docker run \
  --name vchord-suite \
  -e POSTGRES_PASSWORD=postgres \
  -p 5432:5432 \
  -d tensorchord/vchord-suite:pg17-latest
CREATE EXTENSION IF NOT EXISTS vchord CASCADE;
CREATE EXTENSION IF NOT EXISTS pg_tokenizer CASCADE;
CREATE EXTENSION IF NOT EXISTS vchord_bm25 CASCADE;
\dx
pg_tokenizer | 0.1.0   | tokenizer_catalog | pg_tokenizer
vchord       | 0.3.0   | public            | vchord: Vector database plugin for Postgres, written in Rust, specifically designed for LLM
vchord_bm25  | 0.2.0   | bm25_catalog      | vchord_bm25: A postgresql extension for bm25 ranking algorithm
vector       | 0.8.0   | public            | vector data type and ivfflat and hnsw access methods
  1. Prepare the Embedding Model and Data

For embedding, we can use a pre-trained embedding model like BGE-M3 to generate embeddings for the documents. BGE-M3 is a high-quality embedding model known for its versatility in multi-functionality, multi-linguality, and multi-granularity.

For validation, we use the BEIR dataset, a heterogeneous benchmark for information retrieval. It is easy to use and allows you to evaluate your models across 15+ diverse IR datasets.

First, you need to load your data into PostgreSQL. Then you can use the following SQL query to generate embeddings for the documents.

with self.conn.cursor() as cursor:
    cursor.execute(
        f"CREATE TABLE IF NOT EXISTS {self.dataset}_corpus (id TEXT, text TEXT, emb vector({self.vector_dim}), bm25 bm25vector);"
    )

    for did, doc in tqdm(zip(doc_ids, docs), desc="insert corpus"):
        emb = self.sentence_encoder.encode_doc(doc)
        cursor.execute(
            f"INSERT INTO {self.dataset}_corpus (id, text, emb) VALUES (%s, %s, %s)",
            (did, doc, emb),
        )

For embedding:

from FlagEmbedding import BGEM3FlagModel
class SentenceEmbedding:
    def __init__(self, model_name: str = "BAAI/bge-m3"):
        self.model = BGEM3FlagModel(
            model_name,
            use_fp16=True,
        )
    def encode_docs(self, documents: list[str]):
        return self.model.encode(
            documents,
            batch_size=32,
            max_length=8192,
        )['dense_vecs']

The data process and embedding generation code can be found here.

Semantic Search with VectorChord

For semantic search, we utilize the RaBitQ algorithm, which is highly optimized in the VectorChord PostgreSQL extension. RaBitQ is a quantization algorithm for high-dimensional spaces, designed to improve the storage and retrieval efficiency of high-dimensional data, such as embedded vectors.

It achieves this by mapping high-dimensional vectors to a low-dimensional discrete space while preserving the similarity information of the original vectors. This process reduces storage requirements and computational costs while maintaining high retrieval accuracy.

centroids = min(4 * int(self.num**0.5), self.num // 40)
ivf_config = f"""
residual_quantization = true
[build.internal]
lists = [{centroids}]
build_threads = {workers}
spherical_centroids = false
"""
with self.conn.cursor() as cursor:
    cursor.execute(f"SET max_parallel_maintenance_workers TO {workers}")
    cursor.execute(f"SET max_parallel_workers TO {workers}")
    cursor.execute(
        f"CREATE INDEX {self.dataset}_rabitq ON {self.dataset}_corpus USING vchordrq (emb vector_l2_ops) WITH (options = $${ivf_config}$$)"
    )

If you find that building the index on your own dataset is too slow, you can utilize external build to accelerate the process. For more details, please refer to our previous blog: Benefits and Steps of External Centroids Building in VectorChord.

Once the index is built, you can use the following SQL query to search for similar documents:

probe = int(0.1 * min(4 * int(self.num**0.5), self.num // 40))
with self.conn.cursor() as cursor:
    cursor.execute(f"SET vchordrq.probes = {probe}")
    cursor.execute(
        f"select q.id as qid, c.id, c.score from {self.dataset}_query q, lateral ("
        f"select id, {self.dataset}_corpus.emb <-> q.emb as score from "
        f"{self.dataset}_corpus order by score limit {topk}) c;"
    )

Key-Word Search with VectorChord-BM25

For keyword search, we need to use a tokenizer to convert the text into a BM25 vector, which is similar to a sparse vector that stores the vocabulary ID and frequency.

cursor.execute(
    f"SELECT create_tokenizer('{self.dataset}_token', $$",
    f"tokenizer = 'unicode'",
    f"stopwords = 'nltk'",
    f"table = '{self.dataset}_corpus'",
    f"column = 'text'",
    f"$$);"
)
cursor.execute(
    f"UPDATE {self.dataset}_corpus SET bm25 = tokenize(text, '{self.dataset}_token')"
)

Let me explain this operation in detail. The tokenize function is used to tokenize the text with the specified tokenizer. In this case, we used the Unicode tokenizer. The output of the tokenize function is a BM25 vector, which is a sparse vector that stores the vocabulary ID and frequency of each word in the text. For example, 1035:7 means the word with vocabulary ID 1035 appears 7 times in the text.

postgres=# select bm25 from fiqa_corpus limit 1;
-- Output: {1035:7, 1041:1, 1996:1, 1997:1, 1999:3, 2010:3, 2015:7, 2019:1, 2022:1, 2028:4, 2036:2, 2041:1, 2051:2, 2054...

After creating the index, you can calculate the BM25 score between the query and the vectors. Note that the BM25 score is negative, meaning that the higher the score, the more relevant the document is. We intentionally make it negative so that you can use the default ORDER BY to retrieve the most relevant documents first.

with self.conn.cursor() as cursor:
    cursor.execute(
        f"SELECT q.id AS qid, c.id, c.score FROM {self.dataset}_query q, LATERAL ("
        f"SELECT id, {self.dataset}_corpus.bm25 <&> to_bm25query('{self.dataset}_text_bm25', q.text , '{self.dataset}_token') AS score "
        f"FROM {self.dataset}_corpus "
        f"ORDER BY score "
        f"LIMIT {topk}) c;"
    )

Rerank/Fuse

Once you obtain the results from both semantic search and keyword search, you can use a fuse or rerank process to merge the results.

  1. Fuse (Reciprocal Rank Fusion (RRF))

The advantage of RRF lies in its independence from specific score units; it is based on ranking for fusion, making it suitable for sorting systems with different scoring criteria.

RRF(d)=∑i=1n1k+ranki(d) \text{RRF}(d) = \sum_{i=1}^{n} \frac{1}{k + \text{rank}_i(d)}

Among them:

  • d is the document.

  • n is the number of ranking systems.

  • rank is the ranking in the sorting system.

  • k is the adjustment parameter, the value is usually 60 (empirical value), used to control the impact of ranking on scores.

for rank, (query_id, doc_id, _) in enumerate(result, start=1):
    if query_id not in rrf_scores:
        rrf_scores[query_id] = {}
    if doc_id not in rrf_scores[query_id]:
        rrf_scores[query_id][doc_id] = 0
    # Calculate and accumulate RRF scores
    rrf_scores[query_id][doc_id] += 1 / (k + rank)
  1. Cross-Encoder model Rerank
Cross-Encoder

In semantic search, we already use Bi-Encoder vectorized the documents and queries separately. But this independent encoding leads to a lack of interaction between the query and the document. Cross-Encoder model will input the query and document as a whole into the model, the model will see the content of both at the same time, capturing the fine-grained semantic relationship between them through the Transformer layer. Compared with RRF, Cross-Encoder model can capture the fine-grained semantic relationship between the query and the document, and it can be more accurate but slower.

reranker = FlagReranker(
    'BAAI/bge-reranker-v2-m3',
    query_max_length=256,
    passage_max_length=512,
    use_fp16=True,
    devices=["cuda:0"] # change ["cpu"] if you do not have gpu, but it will be very slow
) # Setting use_fp16 to True speeds up computation with a slight performance degradation

for query_id, docs in tqdm(results.items()):
    scores = reranker.compute_score(pairs, normalize=True)
    for i, doc_id in enumerate(docs):
        bge_scores[query_id][doc_id] = scores[i]

Evaluation

We have tested the NDCG@10 with those methods on several BEIR datasets. Here are the top10 results:

Dataset Semantic Search BM25 Cross-Encoder Rerank RRF
FiQA-2018 0.40343 0.25301 0.42706 0.37632
Quora 0.88433 0.78687 0.89069 0.87014
SCIDOCS 0.16055 0.15584 0.17942 0.17299
SciFact 0.57631 0.68495 0.74635 0.67855

The table above demonstrates several conclusions:

  • Compared to BM25 and semantic search, the cross-encoder model rerank can significantly improve search performance across different datasets.

  • On some datasets, RRF may lead the performance decrease. Please conduct verification tests before deciding whether to use RRF. If effective, choosing RRF will be a very economical choice, as it is much, much faster than reranking with the cross-encoder model, and almost no resource consumption.

All related benchmark codes can be found here. If you have any questions, please feel free to contact us on Discord or email us at rh@vonng.com.

Future Work

The results highlight the great potential of the hybrid search method. In the future, we will focus on the following aspects to advance RAG:

  • Improve the reranking method: Explore the other model-based rerank, such as ColBERT or ColPali, to enhance reranking performance.
  • Integrate graph search: Investigate the use of the Apache AGE extension for graph search and integrate it with the hybrid search method to further improve search performance.

References

3.2 - Supercharge vector search with ColBERT rerank in PostgreSQL

Traditional vector search methods typically employ sentence embeddings to locate similar content. However, generating sentence embeddings through pooling token

Traditional vector search methods typically employ sentence embeddings to locate similar content. However, generating sentence embeddings through pooling token embeddings can potentially sacrifice fine-grained details present at the token level. ColBERT overcomes this by representing text as token-level multi-vectors rather than a single, aggregated vector. This approach, leveraging contextual late interaction at the token level, allows ColBERT to retain more nuanced information and improve search accuracy compared to methods relying solely on sentence embeddings.

ColBERT structure (from the original paper)

As illustrated in the above image, ColBERT encodes each document/query into a list of token vectors and computes the MaxSim during the query time.

Token-level late interaction requires more computing power and storage. This makes using ColBERT search in large datasets challenging, especially when low latency is important.

One possible solution is to combine sentence-level vector search with token-level late interaction rerank, which leverages the efficiency of approximate vector search and the high quality of multi-vector similarity search.

The multi-vector approach is not limited to pure text retrieval tasks; it can also be used in visual document understanding. For multimodal retrieval models, state-of-the-art models like ColPali and ColQwen directly encode document images into multi-vectors and demonstrate stronger performance compared to OCR-to-text approaches.

This blog will demonstrate using the PostgreSQL extension VectorChord and pgvector with ColBERT rerank.

Tutorial

Assume we already have the documents, let’s create a table to store all of them:

import psycopg
from pgvector.psycopg import register_vector

class PgClient:
    def __init__(self, url: str, dataset: str, sentence_emb_dim: int, token_emb_dim: int):
        self.dataset = dataset
        self.sentence_emb_dim = sentence_emb_dim
        self.token_emb_dim = token_emb_dim
        self.conn = psycopg.connect(url, autocommit=True)
        with self.conn.cursor() as cursor:
            cursor.execute("CREATE EXTENSION IF NOT EXISTS vchord CASCADE;")
        register_vector(self.conn)

    def create(self):
        with self.conn.cursor() as cursor:
            cursor.execute(
                f"CREATE TABLE IF NOT EXISTS {self.dataset}_corpus "
                "(id INT BY DEFAULT AS IDENTITY PRIMARY KEY, text TEXT, "
                f"emb vector({self.sentence_emb_dim}), embs vector({self.token_emb_dim})[]);"
            )

Here we created a table with sentence-level embedding and token-level embeddings.

There are numerous embedding APIs and open-source models. You can choose the one that fits your use case.

For token-level embedding:

from colbert.infra import ColBERTConfig
from colbert.modeling.checkpoint import Checkpoint

class TokenEncoder:
    def __init__(self):
        self.config = ColBERTConfig(doc_maxlen=220, query_maxlen=32)
        self.checkpoint = Checkpoint(
            "colbert-ir/colbertv2.0", colbert_config=self.config, verbose=0
        )

    def encode_doc(self, doc: str):
        return self.checkpoint.docFromText([doc], keep_dims=False)[0].numpy()

    def encode_query(self, query: str):
        return self.checkpoint.queryFromText([query])[0].numpy()

ColBERT model produces 128-dim vectors by default.

To insert the data:

class PgClient:
    ...
    def insert(self, documents: list[str]):
        with self.conn.cursor() as cursor:
            for doc in tqdm(documents):
                sentence_emb = sentence_encoder.encode_doc(doc)
                token_embs = [emb for emb in token_encoder.encode(doc)]
                cursor.execute(
                    f"INSERT INTO {self.dataset}_corpus (text, emb, embs) VALUES (%s, %s, %s)"
                    (doc, sentence_emb, token_embs)
                )

For the vector search part, we can use VectorChord to build a high-performance RaBitQ index:

class PgClient:
    ...
    def index(self, num_doc: int, workers: int):
        n_cluster = 1 << math.ceil(math.log2(num_doc ** 0.5 * 4))
        config = f"""
        residual_quantization = true
        [build.internal]
        lists = [{n_cluster}]
        build_threads = {workers}
        spherical_centroids = false
        """
        with self.conn.cursor() as cursor:
            cursor.execute(f"SET max_parallel_maintenance_workers TO {workers}")
            cursor.execute(f"SET max_parallel_workers TO {workers}")
            cursor.execute(
                f"CREATE INDEX {self.dataset}_rabitq ON {self.dataset}_corpus USING "
                f"vchordrq (emb vector_l2_ops) WITH (options = $${config}$$)"
            )

To speed up the indexing building process, we can utilize the external centroids build. Check “Benefits and Steps of External Centroids Building in VectorChord” for more details.

Now, we can query the PostgreSQL:

class PgClient:
    ...
    def query(self, doc: str, topk: int):
        sentence_emb = sentence_encoder.encode_query(doc)
        with self.conn.cursor() as cursor:
            cursor.execute(
                f"SELECT id, text FROM {self.dataset}_corpus ORDER BY emb <-> %s LIMIT {topk}"
            )
            res = cursor.fetchall()
        return res

To support MaxSim rerank, we’ll need to create a function:

class PgClient:
    def __init__(self, url: str, dataset: str, sentence_emb_dim: int, token_emb_dim: int):
        ...
        self.conn.execute("""
            CREATE OR REPLACE FUNCTION max_sim(document vector[], query vector[]) RETURNS double precision AS $$
            WITH queries AS (
                SELECT row_number() OVER () AS query_number, * FROM (SELECT unnest(query) AS query)
            ),
            documents AS (
                SELECT unnest(document) AS document
            ),
            similarities AS (
                SELECT query_number, document <=> query AS similarity FROM queries CROSS JOIN documents
            ),
            max_similarities AS (
                SELECT MAX(similarity) AS max_similarity FROM similarities GROUP BY query_number
            )
            SELECT SUM(max_similarity) FROM max_similarities
            $$ LANGUAGE SQL
        """)

Now, we can rerank the documents retrieved by vector search:

class PgClient:
    def rerank(self, query: str, ids: list[int], topk: int):
        token_embs = [emb for emb in token_encoder.encode_query(query)]
        with self.conn.cursor() as cursor:
            cursor.execute(
                f"SELECT id, text FROM {self.dataset}_corpus WHERE id = ANY(%s) ORDER BY "
                f"max_sim(embs, %s) DESC LIMIT {topk}"
                (ids, token_embs)
            )
            res = cursor.fetchall()
        return res

Evaluation

We have tested this method on several BEIR datasets. Here are the results:

Dataset Search NDCG@10 Rerank NDCG@10
fiqa 0.23211 0.3033
quora 0.31599 0.3934

This shows that ColBERT reranking can significantly enhance the results of vector searches.

All the related benchmark codes can be found here.

Future work

Vector search and full-text search with ColBERT rerank can further improve performance. We’re also working on the PostgreSQL BM25 extensions. Stay tuned.

References

3.3 - Beyond Text: Unlock OCR-Free RAG in PostgreSQL with Modal & VectorChord

Building effective Retrieval-Augmented Generation (RAG) systems for documents often feels like wrestling with messy, complex pipelines. Especially when dealing

Building effective Retrieval-Augmented Generation (RAG) systems for documents often feels like wrestling with messy, complex pipelines. Especially when dealing with PDFs or scanned images, traditional methods rely heavily on Optical Character Recognition (OCR) and layout analysis. These steps can be slow, error-prone, and often lose crucial visual context like tables, figures, and formatting. But what if you could query documents based on how they look, not just the extracted text?

This post is your guide to building exactly that: an OCR-free RAG system directly within your familiar PostgreSQL database. We’ll leverage the power of the ColQwen2 Vision Language Model, the efficiency of VectorChord for multi-vector search in Postgres, and the scalability of Modal for GPU-powered embedding generation. Get ready to simplify your RAG stack and potentially boost your retrieval accuracy, all without complex pre-processing.

We’ll cover:

  • What ColQwen2 is and why it’s a game-changer.

  • How VectorChord makes advanced vector search possible in Postgres.

  • A step-by-step tutorial to build and evaluate the system.

What is ColQwen2? The Power of Visual Understanding

To grasp ColQwen2, let’s first look at its foundation: ColPali. As introduced in the paper “ColPali: Efficient Document Retrieval with Vision Language Models”, ColPali represents a novel approach using Vision Language Models (VLMs). Instead of relying on imperfect OCR, it directly indexes documents using their rich visual features – text, images, tables, layout, everything the eye can see.

Think about the limitations of traditional OCR: complex layouts get mangled, tables become gibberish, and images are often ignored entirely. It’s like trying to understand a book by only reading a flawed transcript. ColPali avoids this by using a powerful VLM (originally PaliGemma) to create embeddings that capture the document’s holistic visual nature. Two key concepts make it shine:

  1. Contextualized Vision Embeddings: Generating rich embeddings directly from the document image using a VLM.

  2. Late Interaction: This clever technique allows the query’s textual meaning to directly interact with the document’s detailed visual features at search time. It’s not just matching text summaries; it’s comparing the query concept against the visual evidence within the document page.

ColPali Architecture

The ColPali architecture (Image from the ColPali paper)

ColQwen2 builds upon this powerful ColPali architecture but swaps the underlying VLM for the more recent Qwen2-VL-2B. It generates ColBERT-style multi-vector representations, capturing fine-grained details from both text and images. As seen on the vidore-leaderboard, ColQwen2 delivers impressive performance with practical model size.

How Does VectorChord Enable ColQwen2 in Postgres?

This is where VectorChord becomes the crucial piece of the puzzle, bringing this cutting-edge VLM capability into your PostgreSQL database. ColQwen2 (and ColPali) relies heavily on those multi-vector representations and the Late Interaction mechanism, specifically requiring an efficient MaxSim (Maximum Similarity) operation.

Calculating MaxSim – finding the highest similarity score between any vector in the query set and any vector in the document set – can be computationally brutal, especially across millions of document vectors. VectorChord tackles this head-on:

  • Native Multi-Vector Support: It’s designed from the ground up to handle multi-vector data efficiently within Postgres.

  • Optimized MaxSim: Drawing inspiration from the WARP paper, VectorChord uses techniques like dynamic similarity imputation to dramatically speed up MaxSim calculations, making large-scale visual document retrieval feasible.

  • Hybrid Search Ready: Beyond multi-vector, it also supports dense, sparse, and hybrid search (check out our previous post!).

  • Scalable & Disk-Friendly: Designed for performance without demanding excessive resources.

In short, VectorChord transforms PostgreSQL into a powerhouse capable of handling the advanced vector search techniques required by models like ColQwen2 or ColPali.

Tutorial: Building Your OCR-Free RAG System

Alright, theory’s great, but let’s roll up our sleeves and build this thing! We’ll walk through setting up the environment, processing data using Modal for scalable embedding generation, indexing into VectorChord within Postgres, and finally, evaluating our shiny new OCR-free RAG system.

Prerequisites

Before we start, ensure you have:

  • A PostgreSQL instance (Docker recommended) with the VectorChord extension installed OR a VectorChord Cloud cluster.

  • A Modal account (free tier available). Modal’s fast GPU provisioning and scaling are perfect for the embedding generation step. To efficiently process a large volume of documents using the ColQwen2 model, it’s crucial to leverage Modal’s rapid startup and GPU expansion features. This approach will significantly reduce the time required for local processing.

If you want to reproduce the tutorial quickly, you can use the tensorchord/vchord-suite image to run multiple extensions that TensorChord provides.

You can run the following command to build and start Postgres with VectorChord-BM25 and VectorChord.

docker run \
  --name vchord-suite \
  -e POSTGRES_PASSWORD=postgres \
  -p 5432:5432 \
  -d tensorchord/vchord-suite:pg17-latest
CREATE EXTENSION IF NOT EXISTS vchord CASCADE;
CREATE EXTENSION IF NOT EXISTS pg_tokenizer CASCADE;
CREATE EXTENSION IF NOT EXISTS vchord_bm25 CASCADE;
\dx
pg_tokenizer | 0.1.0   | tokenizer_catalog | pg_tokenizer
vchord       | 0.3.0   | public            | vchord: Vector database plugin for Postgres, written in Rust, specifically designed for LLM
vchord_bm25  | 0.2.0   | bm25_catalog      | vchord_bm25: A postgresql extension for bm25 ranking algorithm
vector       | 0.8.0   | public            | vector data type and ivfflat and hnsw access methods

Set up Modal:

$ pip install modal
$ python3 -m modal setup  # click the link to authorize

Step 1: Load the Data (Using Modal Volumes)

We’ll use the ViDoRe Benchmark dataset. To handle this data efficiently across potentially distributed Modal functions, we’ll download it to a Modal Volume. Volumes provide persistent, shared storage ideal for this ‘download once, process many times’ scenario.

image = modal.Image.debian_slim().pip_install("datasets","huggingface_hub","Pillow")
DATASET_DIR = "/data"
DATASET_VOLUME  = modal.Volume.from_name(
    "colpali-dataset", create_if_missing=True
)
app = modal.App(image=image)

@app.function(volumes={DATASET_DIR: DATASET_VOLUME}, timeout=3000)
def download_dataset(cache=False) -> None:
    from datasets import load_dataset
    from tqdm import tqdm

    collection_dataset_names = get_collection_dataset_names("vidore/vidore-benchmark-667173f98e70a1c0fa4db00d")
    for dataset_name in tqdm(collection_dataset_names, desc="vidore benchmark dataset(s)"):
        dataset = load_dataset(dataset_name, split="test",num_proc=10)
        unique_indices = dataset.to_pandas().drop_duplicates(subset="image_filename", keep="first").index #to remove repeating PDF pages with different queries
        dataset = dataset.select(unique_indices)
        dataset.save_to_disk(f"{DATASET_DIR}/{dataset_name}")

Modal offers a straightforward Python API for interacting with its platform. With modal.Image, you can set the base image for the Modal App, and the function decorator helps define the function to be executed within the Modal application. To download the dataset to the Modal Volume, execute the following command. Modal will then automatically build the Docker image and promptly launch the container to run your function.

Run the download function:

$ modal run dataset.py::download_dataset

Modal handles building the environment and running the script in the cloud.

Step 2: Process Data & Generate Embeddings (With Modal & ColQwen2)

This is the heavy lifting: converting document images into ColQwen2 multi-vector embeddings. Doing this locally for many documents would be slow. Modal shines here:

  • Easy GPU Access & Autoscaling: Launch a ColQwen2 model service on Modal and use it to generate the image embeddings and query embeddings for the dataset. The decision to implement an HTTP service instead of using the SDK directly was made to ensure seamless auto-scaling during large-scale embedding operations. If you want to deploy the embedding service as a persistent web endpoint, you can directly change modal.cls to modal.web_server for ColPaliServer and running modal deploy.

  • Recovery: We checkpoint progress to a separate Modal Volume, allowing us to resume embedding generation if interrupted.

# embedding.py (Illustrative - keep original code)
# ... imports, volumes setup ...
modal_app = modal.App() # Define app

# Function to coordinate embedding generation
@modal_app.function(...)
def embed_dataset(down_scale: float = 1.0, batch_size: int = BATCH_SIZE):
    # ... (logic for loading dataset names, handling checkpoints) ...
    colpali_server = ColPaliServer() # Get handle to our GPU class
    # ... (loop through datasets, batch items, call server for embeddings) ...
    # ... (save embeddings and update checkpoint) ...
    print("Embedding generation complete.")

# server.py (Illustrative - keep original code)
# Class running on GPU to serve embedding requests
@modal_app.cls(gpu=GPU_CONFIG, ...)
class ColPaliServer:
    @modal.enter()
    def load_model_and_start_server(self):
        # ... (Load ColQwen2 model, start internal FastAPI server) ...
        self.client = httpx.AsyncClient(...) # Client to talk to internal server

    @modal.exit()
    def shutdown_server(self):
        # ... (Cleanup) ...

    # Method called by embed_dataset function
    @modal.method()
    async def embed_images(self, images: List[str]) -> np.ndarray:
        # ... (Prepare batch, send request to internal server via self.client) ...
        # ... (Decode response using msgspec for speed) ...
        return embeddings_numpy_array

    # ... (Potentially add embed_queries method too) ...

# colpali.py
class ColPaliModel:
    def __init__(self, model_name: str = "vidore/colqwen2-v1.0", cache_dir: str="/model"):
        # ...
        if self.model_name == "vidore/colqwen2-v1.0":
            from colpali_engine.models import ColQwen2, ColQwen2Processor
            from transformers.utils.import_utils import is_flash_attn_2_available

            # load model
            model = ColQwen2.from_pretrained(
                self.model_name,
                torch_dtype=torch.bfloat16,
                device_map="cuda:0",
                attn_implementation="flash_attention_2" if is_flash_attn_2_available() else None,
                cache_dir=self.cache_dir,
            ).eval()

            colpali_processor = ColQwen2Processor.from_pretrained(
                self.model_name,
                cache_dir=self.cache_dir,
            )
    # ... functions for embedding images and queries
    # @modal.method()
    # async def batch_embed_images(self, images_base64: List[str]) -> np.ndarray:
    # @modal.method()
    # async def batch_embed_queries(self, queries: List[str]) -> np.ndarray:

Generate the embeddings:

$ modal run embedding.py::embed_dataset

Step 3: Create Index in VectorChord (Using Vechord SDK)

With embeddings generated and saved in a Modal Volume, we first need to bring them local.

$ modal volume get colpali-embedding-checkpoint /path/to/local/vidore_embeddings

Now, we’ll use the vechord SDK to load these embeddings into our PostgreSQL database and create the necessary multi-vector index. vechord provides a Pythonic, ORM-like way to interact with VectorChord in Postgres.

%[https://github.com/tensorchord/vechord]

MultiVector = List[Vector[128]] # Assuming 128 dimensions for ColQwen2

lists = 2500 # lists is the number of the cluster
# Define the database table schema using vechord
class Image(Table, kw_only=True):
    uid: Optional[PrimaryKeyAutoIncrease] = None
    image_embedding: Annotated[MultiVector, MultiVectorIndex(lists=lists)] # Stores the document's visual embeddings
    query_embedding: Annotated[MultiVector, MultiVectorIndex(lists=lists)]  # Stores the query's embeddings
    query: str = None
    dataset: Optional[str] = None
    dataset_id: Optional[int] = None

# Connect to your PostgreSQL database
# Ensure the URL points to your local Docker or VectorChord Cloud instance
DB_URL = "postgresql://postgres:postgres@127.0.0.1:5432/postgres" # Default DB
vr = VechordRegistry("colpali", DB_URL)
vr.register([Image]) # Creates the table and multi-vector index if they don't exist

# Function to load embeddings from disk and yield Image objects
@vr.inject(output=Image)
def load_image_embeddings(path: str) -> Iterator[Image]:
    # ... (logic to load numpy arrays from disk, convert to List[Vector]) ...
    # ... (yield Image(...) instances) ...
    print(f"Loaded embeddings from {path}")

if __name__ == "__main__":
    # Path where you downloaded the embeddings from Modal
    embedding_dir = "/path/to/local/vidore_embeddings"
    load_image_embeddings(embedding_dir) # This triggers vechord to insert data
    print("Data loaded and indexed into VectorChord.")

Run the indexer script, vechord handles table creation, index creation, and data insertion.

Step 4: Evaluation - Does it Work?

The final step is crucial: evaluating retrieval performance. Does this OCR-free system deliver accurate results, and can it be fast? We’ll use vechord to run queries against our indexed data and look at NDCG@10 and Recall@10. We’ll also test the impact of VectorChord’s WARP optimization, which accelerates MaxSim calculations. In this tutorial, we will use vidore/arxivqa_test_subsampled dataset for evaluation queries, the data are shown below.

Evaluation Queries
TOP_K = 10

# Define structure for results (optional but good practice)
class Evaluation(msgspec.Struct):
    map: float
    ndcg: float
    recall: float

TOP_K = 10
def evaluate(queries: list[Image], probes: int, max_maxsim_tuples: int) -> list[Evaluation]:
    result  = []
    for query in queries:
        vector = query.query_embedding
        docs: list[Image] = vr.search_by_multivec(
            Image, vector, topk=TOP_K, probe=probes, max_maxsim_tuples=max_maxsim_tuples
        )
        score = BaseEvaluator.evaluate_one(query.uid, [doc.uid for doc in docs])
        result.append(Evaluation(
            map=score.get("map"),
            ndcg=score.get("ndcg"),
            recall=score.get(f"recall_{TOP_K}"),
        ))
    return result

if __name__ == "__main__":
    # Select some queries from the benchmark dataset stored in the DB
    # Example: Get 100 queries from a specific subset
    test_queries: list[Image] = vr.select_by(
        Image.partial_init(dataset="vidore/arxivqa_test_subsampled"), limit=100
    )

    # ... optimize the param ...
    res: list[Evaluation] = evaluate(queries, probes=probes, maxsim_refine=maxsim_refine)
    print("ndcg@10", sum(r.ndcg for r in res) / len(res))
    print("recall@10", sum(r.recall for r in res) / len(res))
    print(f"Total execution time: {total_time:.4f} seconds")

Running the evaluation script yields impressive results:

# Disable WARP
ndcg@10 0.8615
recall@10 0.92
Total execution time: 810 seconds # Baseline accuracy and time

# Enable WARP
# There is no need to focus on specific times,
# only relative times need to be taken into account,
# as the tests were performed on a local model with poor performance.
ndcg@10 0.8353
recall@10 0.90
Total execution time: 41 seconds # Dramatic WARP Speed Boost

Analysis:

  • High Baseline Accuracy: First, let’s look at the baseline performance without WARP optimization. The system achieves an excellent NDCG@10 of 0.8615 and Recall@10 of 0.92. This confirms the fundamental effectiveness of ColQwen2 embeddings paired with VectorChord’s precise MaxSim search, delivering state-of-the-art results for visual document retrieval even without speed optimizations.

  • Dramatic WARP Speed Boost: Now, observe the impact of enabling VectorChord’s WARP optimization. The total execution time plummets from a substantial 810 seconds down to just 41 seconds! This represents a massive ~18.7x speedup for the evaluation query set. This clearly demonstrates WARP’s power in dramatically accelerating the computationally intensive MaxSim operations required for late-interaction models like ColQwen2.

  • Minimal Accuracy Trade-off: Impressively, this significant speed gain comes at the cost of only a very minor dip in retrieval accuracy. The NDCG@10 slightly decreases to 0.8353 (a difference of less than 0.3), and Recall@10 reduces minimally to 0.9 (a difference of only 0.02).

Conclusion: Visual RAG Made Simpler in Postgres

In this tutorial, we successfully built a high-performance, OCR-free RAG system by bringing together the visual understanding capabilities of ColQwen2, the scalable multi-vector search of VectorChord within PostgreSQL, and the efficient GPU processing of Modal. We saw how this stack allows us to directly query documents based on their visual content, bypassing the pitfalls of traditional OCR pipelines.

The ability to seamlessly integrate this visual dimension into your RAG system directly within Postgres opens up exciting possibilities for applications dealing with visually rich documents like scientific papers, invoices, product manuals, historical archives, and much more.

Ready to try it yourself?

This approach represents a significant step towards simpler, more robust, and visually-aware document retrieval systems.

References

4 - Benchmarks

Price, throughput, latency, and recall benchmarks against pgvector, pgvectorscale, and Elasticsearch.

4.1 - Price Benchmark

Using VectorChord, you can achieve a QPS of 131 with 0.95 precision on 100 million 768-dimensional vectors for the top 10 queries. This setup costs only $250 mo

Using VectorChord, you can achieve a QPS of 131 with 0.95 precision on 100 million 768-dimensional vectors for the top 10 queries. This setup costs only $250 monthly and can be hosted on a single machine. If you are interested in the details of our implementation, please refer to our blog VectorChord: Store 400k Vectors for $1 in PostgreSQL.

This means you can store 400k vectors for only $1, allowing you to save significantly: 6x more vectors compared to Pinecone (storage optimized instance) and 26x more than pgvector/pgvecto.rs for the same price.

Pricing Benchmark

In the monthly cost comparison for storing vectors, based on MyScale benchmark data, the chart highlights how VectorChord emerges as an affordable option, priced at just $247 for storing 100 million vectors. In contrast, Pinecone, despite its optimized storage, costs $1,600 per month, while Qdrant is priced at $4,374. pgvector/pgvecto.rs has a considerably higher cost of $6,580.

Pricing Benchmark
Tip

The price benchmark is tested on December 5, 2024. We are ongoingly enhancing performance and optimizing costs. For example, VectorChord 0.2 builds on its predecessor with a 30% smaller index size and 50% faster builds. If you want to know the latest benchmark, please contact us at discord.

4.2 - Performance Benchmarking

Here we present a comprehensive benchmark comparing our implementation of VectorChord with other vector databases, focusing on QPS and scalability. If you are

Here we present a comprehensive benchmark comparing our implementation of VectorChord with other vector databases, focusing on QPS and scalability. If you are interested in the details of our implementation, please refer to our blog VectorChord: Store 400k Vectors for $1 in PostgreSQL.

QPS

LAION 5M

QPS on LAION 5M top 10

In LAION 5M dataset, VectorChord achieved more than 3x QPS at 0.95 recall compared to different vector databases, including MyScale, pgvector, pinecone, weaviate, qdrant and zilliz(milvus).

LAION 100M on i4i.xlarge

QPS on LAION 100M

This setup utilizes 4 CPUs and 32 GB of memory, with 937 GB of SSD storage for the 100 million vectors. We achieved a QPS of 16.2 at a recall rate of 0.95 for the top 10 results and 4.3 at 0.95 for the top 100, using a single-threaded query. The results are truly impressive.

Scalability

If you are interested VectorChord scalable capability, you can refer our blog Vector Search at 10,000 QPS in PostgreSQL with VectorChord.

LAION 100M && LAION 5M

dataset minimal hardware for 10000+ QPS number of vcpu estimate cost per month
LAION-5m r7i.xlarge × 7 4 × 7 = 28 vcpu $0.2646 × 7 × 720 = $1334
LAION-100m r7i.16xlarge × 2 64 × 2 = 128 vcpu $4.2336 × 2 × 720 = $6096
QPS on LAION 100M and LAION 5M
Tip

The price benchmark is tested on December 5, 2024. We are ongoingly enhancing performance and optimizing costs. For example, VectorChord 0.2 builds on its predecessor with a 30% smaller index size and 50% faster builds. If you want to know the latest benchmark, please contact us at discord.

4.3 - Benchmark with pgvector

In this section, we compare VectorChord with pgvector, a PostgreSQL extension that provides vector similarity search capabilities. We will focus on four key met

In this section, we compare VectorChord with pgvector, a PostgreSQL extension that provides vector similarity search capabilities. We will focus on four key metrics: QPS (Queries Per Second), Recall@100, Index Building Time, and Insertion Time. If you are interested in the details of our implementation, please refer to our blog Vector Search Over PostgreSQL: A Comparative Analysis of Memory and Disk Solutions.

QPS & Recall

QPS on GIST 1m, Top 10

Here are some initial benchmark results for the GIST dataset, which consists of 1 million vectors in 960 dimensions. With equivalent recall, VectorChord’s QPS could be twice that of pgvector. More details will be provided in the Benchmark section.

Index Build Time & Insert Time & Query Completion Time

Index Build Time && Insert Time && Query Completion Time on GIST 1m

VectorChord takes 186 seconds to build the index by utilizing a separate machine for KMeans clustering, making it 16 times faster than pgvector. Additionally, the insert time is also 14 times faster than that of pgvector. As we know, indexing is the most resource-intensive part of vector databases, requiring significant computation and increasing the demand for CPUs and memory. By utilizing a more capable machine to build the index and then importing it to a smaller machine for querying, it becomes possible to support billions of vectors on a single machine.

4.4 - Benchmark with Elasticsearch

Here we present a comprehensive benchmark comparing our implementation of VectorChord-BM25 with ElasticSearch, focusing on two key metrics: QPS (Queries Per Sec

Here we present a comprehensive benchmark comparing our implementation of VectorChord-BM25 with ElasticSearch, focusing on two key metrics: QPS (Queries Per Second) and NDCG@10 (Normalized Discounted Cumulative Gain at rank 10). If you are interested in the details of our implementation, please refer to our blog VectorChord-BM25: Revolutionize PostgreSQL Search with BM25 Ranking — 3x Faster Than ElasticSearch.

RPS

RPS

We compare the Top1000 Queries Per Second (QPS)—a metric that measures how many queries a system can process per second while retrieving the top 1000 results for each query—between our implementation and ElasticSearch across various datasets from bm25-benchmarks. On average, our implementation achieves 3 times higher QPS compared to ElasticSearch across the tested datasets, showcasing its efficiency and scalability.

NDCG@10

NDCG@10

We have dedicated substantial effort to align VectorChord-BM25 with ElasticSearch’s behavior, ensuring a fair and precise comparison. As demonstrated in the table, our implementation achieves NDCG@10 scores that are comparable across most datasets, with certain cases even surpassing ElasticSearch (e.g., trec-covid and scifact).

4.5 - Benchmark with pgvectorscale

In this section, we compare VectorChord with pgvectorscale, a PostgreSQL extension that provides vector similarity search capabilities. We will focus on four ke

In this section, we compare VectorChord with pgvectorscale, a PostgreSQL extension that provides vector similarity search capabilities. We will focus on four key metrics: QPS (Queries Per Second), Recall@10/100, Index Building Time, and Insertion Time. Memory and disk performance will also be discussed. If you are interested in the details of our implementation, please refer to our blog Vector Search Over PostgreSQL: A Comparative Analysis of Memory and Disk Solutions.

QPS & Recall

Memory

QPS on LAION 5m memory

The graphs illustrate the trade-off between query speed (QPS) and search quality (Recall@10/100). Recall@10 measures the percentage of true nearest neighbors found within the top 10 results. In general, higher QPS can be achieved at the cost of lower recall, and vice versa. A better system may achieve higher recall at the same QPS, or higher QPS at the same recall level. Our goal is typically to maximize QPS while maintaining a high recall target (e.g., 95%).

Disk

QPS on LAION 5m disk

As always, VectorChord consistently achieves higher QPS at high recall levels (e.g., >95% Recall@10) compared to the other extensions, followed by pgvector and pgvectorscale with similar QPS.

Index Building

All About Index Building

VectorChord maximizes CPU usage, reduces memory consumption, and shortens index-building time compared to pgvectorscale.

Insertion Time

Insertion Time

As you can see, VectorChord (1565 Insert/Sec) performs much better than pgvector (246 Insert/Sec) and pgvectorscale (107 Insert/Sec) when inserting data. For most workloads, 100 Insert/Sec is more than enough. However, if you find that a lot of vectors are continuously inserted in your production (for example, 500 Insert/Sec), this could be a problem.

5 - Administration

Migrating from pgvecto.rs or pgvector, horizontal read scalability, and Kubernetes deployment.

5.1 - Migration

Most of VectorChord’s vector types are supported by pgvector.

outline: [2,3]

VectorChord Migration Overview

From PGVecto.rs

Most of VectorChord’s vector types are supported by pgvector. Therefore, migrating from PGVecto.rs to VectorChord is a simple inversion of migrating from pgvector to PGVecto.rs.

Tip

We provide migration image modelzai/pgvectors-vchord for pgvecto.rs to VectorChord and vice versa.

Prerequisites

Let’s suppose that you have a table with vector data, and the extension PGVecto.rs installed. A simple example would be:

CREATE TABLE table_vector (id bigserial PRIMARY KEY, vec_f32 vectors.vector(3));
CREATE TABLE table_float16 (id bigserial PRIMARY KEY, vec_f16 vectors.vecf16(3));
CREATE TABLE table_binary (id bigserial PRIMARY KEY, vec_bits vectors.bvector(3));

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

INSERT INTO table_vector (vec_f32) VALUES ('[0.1, 0.1, 0.1]'), ('[0.2, 0.2, 0.2]');
INSERT INTO table_float16 (vec_f16) VALUES ('[0.1, 0.1, 0.1]'), ('[-0.2, 0.2, 0.2]');
INSERT INTO table_binary (vec_bits) VALUES ('[0, 0, 0]'), ('[0, 0, 1]');

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

CREATE INDEX index_vector ON table_vector USING vectors (vec_f32 vectors.vector_l2_ops) WITH (options = "[indexing.hnsw]");
-- CREATE INDEX index_float16 ON table_float16 USING vectors (vec_f16 vectors.vecf16_cos_ops) WITH (options = "[indexing.hnsw]");
-- CREATE INDEX index_binary ON table_binary USING vectors (vec_bits vectors.bvector_hamming_ops) WITH (options = "[indexing.hnsw]");

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

  • Please note that your pgvector and VectorChord 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 VectorChord

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

CREATE EXTENSION vchord CASCADE;
Note

This will install vectorchord and pgvector at public schema, if other schema is picked, please substitute public with your schema at the migration command.

Details

The two extensions VectorChord and PGVecto.rs are now installed on PostgreSQL, but inside different schemas. To validate it, please run \dx in psql. You will see that there are 3 extensions:

  • vectors for PGVecto.rs at schema vectors
  • vector for pgvector at schema public
  • vchord for VectorChord at schema public
postgres=# \dx
                                                 List of installed extensions
  Name   | Version |   Schema   |                                         Description                                          
---------+---------+------------+----------------------------------------------------------------------------------------------
 plpgsql | 1.0     | pg_catalog | PL/pgSQL procedural language
 vchord  | 0.1.0   | public     | vchord: Vector database plugin for Postgres, written in Rust, specifically designed for LLM
 vector  | 0.8.0   | public     | vector data type and ivfflat and hnsw access methods
 vectors | 0.4.0   | vectors    | vectors: Vector database plugin for Postgres, written in Rust, specifically designed for LLM

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=# \di
                       List of relations
 Schema |        Name        | Type  |  Owner   |     Table     
--------+--------------------+-------+----------+---------------
 public | index_float16      | index | postgres | table_float16
 public | index_vector       | index | postgres | table_vector
 public | table_binary_pkey  | index | postgres | table_binary
 public | table_float16_pkey | index | postgres | table_float16
 public | table_vector_pkey  | index | postgres | table_vector

3. Migrate vector columns

To migrate from a PGVecto.rs vector column, the first step is to cast column datatypes between extensions.

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

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

For sparse vector type svector, we need additional helper function to finish the casting, see the appendix.

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 can be found in step 2
DROP INDEX IF EXISTS index_vector;

-- From vector type
ALTER TABLE table_vector ALTER COLUMN vec_f32 TYPE public.vector(3) USING vec_f32::real[]::public.vector;

-- From halfvec type
ALTER TABLE table_float16 ALTER COLUMN vec_f16 TYPE public.halfvec(3) USING vec_f16::vectors.vector::real[]::public.halfvec;

-- For bit type
ALTER TABLE table_binary ALTER COLUMN vec_bits TYPE bit(3) USING array_to_string(vec_bits::vectors.vector::real[], '')::bit(3);

4. Recreate index

The next step is to recreate index on vector columns for PGVecto.rs.

CREATE INDEX ON index_vector USING vchordrq (vec_f32 vector_l2_ops) WITH (options = $$
residual_quantization = true
[build.internal]
lists = [4096]
spherical_centroids = false
$$);

For more information on creating index at VectorChord, see indexing.

5. Clean the environment

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

DROP extension vectors;

Appendix: cast of sparse vector

We have discussed most types, but not the sparse vector. It is a little more complicated than other types. Suppose we have a table with a sparse vector column:

CREATE TABLE table_sparse (id bigserial PRIMARY KEY, vec_sparse vectors.svector(3));
INSERT INTO table_sparse (vec_sparse) VALUES ('{0:-0.1, 1:0.1, 2:0.1}/3'), ('{0:-0.4, 1:-0.4, 2:-0.4}/3');

Sparse vectors have a different text representation between PGVecto.rs and pgvector: The index starts from 0 in PGVecto.rs and from 1 in pgvector.

For example, a typical sparse vector [-1, 0, 1, 0, 2] has a text representation of:

  • {0:-1, 2:1, 4:2}/5 in PGVecto.rs
  • {1:-1, 3:1, 5:2}/5 in pgvector

So we need a helper function to handle this change by processing the string:

CREATE OR REPLACE FUNCTION migrate_sparse(input_text text) RETURNS text AS $$
DECLARE
    output_text text := '';
    cache_index_number text := '';
    number_started boolean := false;
BEGIN
    FOR i IN 1..length(input_text) LOOP
        IF substring(input_text from i for 1) = ':' THEN
            output_text := output_text || (cache_index_number::int + 1)::text || ':';
            number_started := false;
        ELSIF substring(input_text from i for 1) ~ '[,{]' THEN
            cache_index_number := '';
            number_started := true;
            output_text := output_text || substring(input_text from i for 1);
        ELSIF substring(input_text from i for 1) ~ '[0-9]' THEN
            IF number_started THEN
                cache_index_number := cache_index_number || substring(input_text from i for 1);
            ELSE
                output_text := output_text || substring(input_text from i for 1);
            END IF;
        ELSE
            output_text := output_text || substring(input_text from i for 1);
        END IF;
    END LOOP;
    RETURN output_text;
END;
$$ LANGUAGE plpgsql;

After that, the migration can continue smoothly:

ALTER TABLE table_sparse ALTER COLUMN vec_sparse TYPE public.sparsevec USING migrate_sparse(vec_sparse::text)::public.sparsevec;

5.2 - Scalability

High queries per second (QPS) vector search is essential for real-time applications such as recommendation engines, image recognition, and natural language proc

Background

High queries per second (QPS) vector search is essential for real-time applications such as recommendation engines, image recognition, and natural language processing. These applications demand rapid, efficient searches to deliver immediate results, which is critical for maintaining user engagement and operational effectiveness. By leveraging PostgreSQL for vector search, you not only benefit from its mature, robust relational capabilities and advanced indexing but also avoid the overhead of managing a separate, specialized vector database. This unified approach simplifies integration and reduces system complexity while still meeting the performance needs of modern applications.

Architecture

Using VectorChord on a multi-node cluster is more difficult than on a single machine. As with other services, a reliable distributed vector search usually has the following requirements:

  • Usability: Declarative configuration management

  • Elasticity: Automatic scaling for nodes based on QPS requirements

  • Load Balancing: Requests are handled effectively across different replica instances

  • High Availability: Having multiple copies of the database and automatically recovering from a replica in case of failure

  • Durability: Secure, persistent storage to ensure data integrity and prevent loss in the event of failures

Therefore, we deploy VectorChord services on AWS EKS with OpenTofu and CloudNativePG to manage the infrastructure that provides the above capabilities. Here are some modules to build the example clusters:

  • OpenTofu: Open source alternative of terraform, used for infrastructure as code (IaC) on AWS EKS

  • CloudNativePG: Manage the full lifecycle of a highly available PostgreSQL database cluster with a primary/standby architecture

  • PGBouncer: Lightweight connection pooler for PostgreSQL, provided by module Pooler of CloudNativePG

Architecture

Similar to DiskANN, VectorChord can fully utilize disk resources and still maintain competitive performance when memory is insufficient. Besides, VectorChord can also cache indexes in memory to achieve higher query performance.

For more on the technical details of VectorChord, please see our previous blog.

To reach the QPS limit, we should assume that the memory can fully hold the dataset and select the AWS machine type based on this standard.

In the following table, we provide the appropriate machine which can hold everything in memory for different sizes of datasets:

dataset dim / size machine / memory
LAION-5m 768 / 5,000,000 r7i.xlarge / 32GB
LAION-100m 768 / 100,000,000 r7i.16xlarge / 512GB

Results

As outlined in our documentation, VectorChord accepts two key parameters: nprob and epsilon. Here, nprob defines the number of candidate vectors to scan during a query, while epsilon serves as the threshold for determining whether to rerank results. Both parameters play a critical role in influencing both QPS and recall, allowing users to fine-tune the balance between search speed and accuracy.

  • Lower nprob will result in higher QPS and lower recall.

  • Lower epsilon will result in higher but more unstable QPS and lower recall.

If these parameters are fixed, the precision of each experiment will remain consistent. However, the QPS will still vary depending on the CPU and I/O performance of the underlying hardware. To ensure the precision stays above 0.9, we configured the parameters based on earlier experiments conducted with smaller datasets.

dataset nprob / epsilon recall
LAION-5m 20 / 1.0 0.9324
LAION-100m 20 / 1.0 0.9042

We deployed an r7i.4xlarge instance as the client within the same Availability Zone to query the VectorChord clusters. The clusters were tested with multiple workers ranging from 16 to 128, simulating the concurrency levels typically applied to the database in real-world scenarios. This setup allows us to evaluate how VectorChord performs under varying workloads and concurrency conditions.

Tip

With 16 vCPUs, r7i.4xlarge can effectively reduce CPU contention from the client side when the concurrency requests reach up to 128.

In line with common pgbouncer practices, we employed two types of instances: a primary instance for data ingestion and multiple replica instances for read-only workloads. Pgbouncer served as the load balancer, efficiently distributing queries across these backend instances. For the benchmark, we directed our query endpoint to the read-only (ro) pooler, while the read-write (rw) primary instance remained idle during the test. This setup ensures optimal resource utilization and scalability for read-heavy workloads.

For the 5 million dataset, we scaled the number of instances from 5 to 15 to achieve 10,000 QPS. In contrast, for the 100 million dataset, only 2 instances were sufficient to meet the performance standard. The results of these experiments are illustrated in the accompanying figure, demonstrating the scalability and efficiency of VectorChord across different dataset sizes.

Result

Cost

In summary, this is the minimum hardware cost if you need 10000 QPS on such datasets:

dataset minimal hardware for 10000+ QPS number of vcpu estimate cost per month
LAION-5m r7i.xlarge × 7 4 × 7 = 28 vcpu $0.2646 × 7 × 720 = $1334
LAION-100m r7i.16xlarge × 2 64 × 2 = 128 vcpu $4.2336 × 2 × 720 = $6096
Tip

The cost of the primary instance is excluded from this calculation, as it can be shared and utilized with other PostgreSQL connection poolers, further optimizing resource allocation and reducing overall expenses.

This experiment empowers you to design and scale your own cluster to achieve the ideal level of VectorChord search performance, whether in the public cloud or a private data center. Even in scenarios where performance is the top priority, VectorChord delivers exceptionally competitive pricing, making it a cost-effective solution for high-performance vector search needs.

Summary

Whether you’re launching a startup on a budget-friendly VPS or scaling a large cluster for uncompromised performance, VectorChord opens up unlimited possibilities. While 10,000 QPS is the focus of this post, it’s far from the limit of what VectorChord can achieve.

With its powerful indexing algorithm, VectorChord not only surpasses standard PostgreSQL solutions like pgvector by efficiently utilizing memory and high-speed SSDs, but also outperforms specialized vector databases in handling massive data volumes and high QPS demands. At the same cost, it delivers superior performance while eliminating the complexity of maintaining a separate vector search system.

If you’re eager to explore large-scale vector search solutions, join our Discord community or reach out to us at rh@vonng.com. Let’s unlock the full potential of your data together!

5.3 - Kubernetes

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

If you want to use vectorchord 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
Kubernetes (Image Volume Extensions) >= 1.31.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-12-30 16:37:55.9520917 +0800 CST   deployed        cloudnative-pg-0.23.0               1.25.0   
$ kubectl get pod -n cnpg-system   
NAME                                   READY   STATUS    RESTARTS   AGE
cnpg-cloudnative-pg-54f97dbcdf-jm97n   1/1     Running   0          1m

PostgreSQL image with vectorchord

In the repository, we already provide a docker image with vectorchord installed. You can find it in ghcr.io/tensorchord/cloudnative-vectorchord. If you want to build your own image, you can use the following Dockerfile as a reference.

ARG CNPG_TAG

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

ARG CNPG_TAG
ARG VECTORCHORD_TAG
ARG TARGETARCH

# drop to root to install packages
USER root
ADD https://github.com/tensorchord/VectorChord/releases/download/$VECTORCHORD_TAG/postgresql-${CNPG_TAG%.*}-vchord_${VECTORCHORD_TAG#"v"}-1_$TARGETARCH.deb /tmp/vchord.deb
RUN apt-get install -y /tmp/vchord.deb && rm -f /tmp/vchord.deb

USER postgres

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 vchord shared library.
  • Execute CREATE EXTENSION IF NOT EXISTS vchord CASCADE; to create vchord 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: vchord
spec:
  instances: 1
  bootstrap:
    initdb:
      database: tensorchord
      postInitApplicationSQL:
        - CREATE EXTENSION IF NOT EXISTS vchord CASCADE;
      owner: tensorchord
      secret:
        name: tensorchord
      dataChecksums: true
      encoding: 'UTF8'
  storage:
    size: 1Gi
  imageName: ghcr.io/tensorchord/cloudnative-vectorchord:17
  postgresql:
    shared_preload_libraries: # load vchord shared library
    - "vchord"

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

$ sudo kubectl get pod
vchord-1                                                 1/1     Running     0             42s

$ kubectl cnpg status vchord
Cluster Summary
Name:                vchord
Namespace:           default
System ID:           7527554386501582867
PostgreSQL Image:    ghcr.io/tensorchord/cloudnative-vectorchord:17
Primary instance:    vchord-1
Primary start time:  2025-07-16 05:55:51 +0000 UTC (uptime 53s)
Status:              Cluster in healthy state
Instances:           1
Ready instances:     1
Current Write LSN:   0/204FB50 (Timeline: 1 - WAL File: 000000010000000000000002)

Certificates Status
Certificate Name    Expiration Date                Days Left Until Expiration
----------------    ---------------                --------------------------
vchord-ca           2025-10-14 05:50:08 +0000 UTC  90.00
vchord-replication  2025-10-14 05:50:08 +0000 UTC  90.00
vchord-server       2025-10-14 05:50:08 +0000 UTC  90.00

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
----      -------------  -----------  ----------------  ------  ---         ---------------  ----
vchord-1                 0/204FB50    Primary           OK      BestEffort  1.25.0           ip-10-0-2-29.ec2.internal

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/vchord-rw 5432:5432
$ psql -h 127.0.0.1 -d tensorchord -U tensorchord
tensorchord=> \dx
                                                 List of installed extensions
  Name   | Version |   Schema   |                                         Description                                         
---------+---------+------------+---------------------------------------------------------------------------------------------
 plpgsql | 1.0     | pg_catalog | PL/pgSQL procedural language
 vchord  | 1.1.1   | public     | vchord: Vector database plugin for Postgres, written in Rust, specifically designed for LLM
 vector  | 0.8.1   | public     | vector data type and ivfflat and hnsw access methods
(3 rows)

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

Lightweight Extension Images

Managing extensions is one of the biggest challenges when running PostgreSQL on Kubernetes. If you want to add a new extension to this image, you must rebuild the image, back up your PostgreSQL cluster, apply the new image with the added extension, and then restore the cluster. This process reflects the concept of immutable containers in Kubernetes.

Thanks to Andrew Dunstan and Matheus Alcantara, a proposal for PostgreSQL 18 introduces a new configuration option (GUC), extension_control_path, which allows users to specify additional directories for extension control files.

For Kubernetes, the ImageVolume feature allows us to mount a container image as a read-only and immutable volume inside a running pod. This enables PostgreSQL extensions packaged as independent OCI-compliant container images to be mounted inside CloudNativePG clusters at a known directory.

Based on these two features, we can create lightweight vectorchord extension image vchord-scratch.

Tip

If you want to use Image Volume Extensions, you need to meet the following requirements:

  • Use Kubernetes version 1.31.0 or above (1.33.0 is recommended), and make sure the ImageVolume feature gate is enabled.
  • Use CloudNative-PG helm chart version 0.26.0 or above.

Create PostgreSQL cluster with Image Volume Extensions

You can use the following sample yaml file to create a PostgreSQL cluster with vectorchord extension image. You can modify it according to your needs. You need pay attention to the following points:

  • Set shared_preload_libraries to load vchord shared library.
  • Execute CREATE EXTENSION IF NOT EXISTS vchord CASCADE; to create vchord extension.
  • Set extensions to specify the vchord-scratch extension image.
  • Set extension_control_path and dynamic_library_path to specify the paths of extension control files and shared library files.
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: vchord 
spec:
  instances: 1 
  imageName: ghcr.io/cloudnative-pg/postgresql:18-system-bookworm
  bootstrap:
    initdb:
      database: tensorchord
      postInitApplicationSQL:
        - CREATE EXTENSION IF NOT EXISTS vchord CASCADE;
      owner: tensorchord
      secret:
        name: tensorchord
      dataChecksums: true
      encoding: 'UTF8'
  storage:
    size: 1Gi
  postgresql:
    extensions:
      - name: vchord
        image:
          reference: ghcr.io/tensorchord/vchord-scratch:pg18-v1.1.1
        dynamic_library_path:
          - /usr/lib/postgresql/18/lib/
        extension_control_path:
          - /usr/share/postgresql/18/
    shared_preload_libraries:
    - "vchord"

We can check the extension is already installed successfully.

$ kubectl port-forward services/vchord-rw 5432:5432
$ psql -h
$ psql -h 0.0.0.0 -U tensorchord -d tensorchord
Password for user tensorchord: 
psql (13.3 (Ubuntu 13.3-1.pgdg16.04+1), server 18.0 (Debian 18.0-1.pgdg12+3))
WARNING: psql major version 13, server major version 18.
         Some psql features might not work.
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.

tensorchord=> \dx
                                                 List of installed extensions
  Name   | Version |   Schema   |                                         Description                                         
---------+---------+------------+---------------------------------------------------------------------------------------------
 plpgsql | 1.0     | pg_catalog | PL/pgSQL procedural language
 vchord  | 1.1.1   | public     | vchord: Vector database plugin for Postgres, written in Rust, specifically designed for LLM
 vector  | 0.8.1   | public     | vector data type and ivfflat and hnsw access methods
(3 rows)
Warning

The image ghcr.io/cloudnative-pg/postgresql:18-system-bookworm already contains pgvector, which is required for vchord. However, this image may not include pgvector in future versions, but we have not seen this trend in the short term.

6 - Customer Stories

Real-world production deployments of VectorChord at multi-billion vector scale.

6.1 - Leveraging 3 Billion PostgreSQL Vectors: Earth Genome's Earth Index & VectorChord for Planetary Protection

Monitoring the health of our planet is one of the most critical challenges of our time. Our planet faces immense environmental pressures, and understanding thes

Monitoring the health of our planet is one of the most critical challenges of our time. Our planet faces immense environmental pressures, and understanding these changes requires sifting through an unimaginable amount of satellite data – petabytes of pixels captured constantly. From tracking deforestation in the Amazon to identifying illegal mining operations in remote regions or monitoring agricultural impacts on sensitive ecosystems, turning this data into actionable intelligence is paramount.

Earth Genome’s Earth Index platform is tackling this challenge head-on. It now offers global coverage, making the entire land surface of our planet searchable using the power of AI. This isn’t just a technical achievement; it’s a new tool designed to empower environmental stewardship by providing accessible, actionable intelligence from satellite data.

We at VectorChord are incredibly proud that our PostgreSQL vector search extension is playing a crucial role in powering this ambitious and vital mission.

What is Earth Index Doing? Turning Pixels into Planetary Insights

Imagine finding a specific type of small-scale environmental impact, like an unreported quarry or a new patch of deforestation, somewhere within satellite imagery covering millions of square kilometers. Traditionally, analyzing this data required significant expertise and resources, making tracking specific threats across vast, remote areas difficult, especially for conservation groups, journalists, and regulators with limited budgets. The animation below illustrates how efficiently Earth Index pinpoints the illegal narcotrafficking airstrips in the Amazon, akin to finding a toothpick on a soccer field.

Earth Index Animation

But how does Earth Index make finding environmental features of interest possible on a global scale? The magic lies in a cutting-edge approach using AI foundation models trained via contrastive learning and the resulting vector embeddings.

Here’s a glimpse into their innovative process:

  1. Tiling the Globe: Earth Index divides the Earth’s entire land surface into over 3.2 billion overlapping tiles, each roughly 10 hectares.

  2. Learning the “Planetary DNA” with Contrastive Self-Supervision: Earth Index leverages breakthroughs in AI, specifically foundation models trained for Earth Observation (EO) using self-supervised, contrastive learning methods (like the DINO objective). The foundation model (e.g., an SSL4EO Vision Transformer) learns the fundamental visual language of satellite imagery by distinguishing between pairs of images from the same location (positive pairs) and pairs from different locations (negative pairs). This happens across diverse global data (like Harmonized Landsat Sentinel-2, Sentinel-1 radar) without needing explicit human labels for features like “forest” or “city.”

  3. Generating Vector Embeddings: Once pre-trained, this model acts as an encoder. Imagery for each tile is fed into it, producing a vector embedding – a rich numerical representation or fingerprint capturing the tile’s essential visual and structural characteristics in a highly compressed format (>10,000x compression). This high-dimensional vector acts like a unique “Planetary DNA” for that piece of land.

  4. Creating a Searchable Planet: These 3.2 billion vector embeddings form the core of Earth Index. They need to be stored in a specialized system optimized for rapidly finding vectors that are numerically similar to each other. Users can provide an example of what they’re looking for (e.g., by clicking on a known illegal mine or a specific type of agricultural infrastructure on the map), and Earth Index searches this massive dataset to find other locations across the globe (or within a specific region) with similar vector fingerprints.

Real-World Impact: Earth Index in Action for Environmental Protection

This technological foundation translates directly into powerful tools for understanding and protecting our environment. It empowers journalists (like those at Mongabay and Radio Free Europe), indigenous communities, conservation organizations (like the World Bank), and researchers to uncover hidden environmental changes, monitor industrial footprints, and gather evidence for conservation efforts with unprecedented ease and speed. The figure shows the shrimp farms in the Gulf of Fonseca.

Shrimp farms in the Gulf of Fonseca

Here are some examples of how Earth Index is being used:

  • Combating Deforestation and Illegal Activities: In collaboration with Mongabay, Earth Index was used to pierce through dense canopy cover and identify hidden, illegal narcotrafficking airstrips in the Amazon. Similarly, the platform can rapidly locate illegal gold mining scars, like those devastating parts of the Yanomami Indigenous Territory, helping to quantify the scale of the problem and guide enforcement efforts.

  • Monitoring Resource Extraction: Radio Free Europe utilized Earth Index to identify over 1300 quarries across the Balkans, revealing that roughly half lacked proper permits, highlighting potential environmental damage and unregulated extraction. This approach can monitor fracking wellpads, rubber processing plants, or other industrial facilities.

  • Protecting Water Quality and Ecosystems: Earth Index can locate Concentrated Animal Feeding Operations near sensitive waterways to assess runoff risks or identify expansions of shrimp farming into vital mangrove ecosystems.

  • Tracking Agricultural Expansion: The tool helps monitor the conversion of natural habitats into agricultural land by searching for patterns associated with new clearings or specific crop types.

The Infrastructure Challenge: Searching Billions of Vectors Affordably

Generating 3.2 billion high-dimensional vectors is one challenge; hosting and searching them efficiently presents another significant hurdle. Earth Index needed a solution that could:

  • Handle Massive Scales: Searching billions of vectors requires specialized indexing and querying capabilities.

  • Integrate Seamlessly with Geospatial Data: Earth Index heavily relies on geospatial operations (like filtering results within specific administrative boundaries or proximity to rivers). This made PostgreSQL with PostGIS a natural and essential choice for their core database. Standalone vector databases often lack the rich geospatial features required.

  • Maintain Performance: Users need results relatively quickly for the platform to be effective for investigation and monitoring.

  • Control Costs: Storing and searching billions of vectors, especially if requiring vast amounts of RAM for purely in-memory indexes, can become prohibitively expensive for many organizations, particularly non-profits.

Earth Index needed a vector search solution that lived within their chosen PostgreSQL environment, could scale massively, perform well, and wouldn’t break the bank on hardware costs.

Why PostgreSQL Rocks for Planetary-Scale Vectors

While specialized vector databases exist, Earth Index faced a critical need: seamlessly integrating vector similarity search with rich geospatial data and operations. This is where PostgreSQL, combined with extensions, truly shines:

  • Unified Data Platform: PostgreSQL, especially with the powerful PostGIS extension, allowed Earth Index to keep their vector embeddings alongside their crucial geospatial metadata (tile locations, administrative boundaries, proximity to features like rivers). This avoids the complexity and potential synchronization issues of managing separate databases for vector search and relational/geospatial data.

  • Leveraging the Power of SQL & PostGIS: Earth Index could combine vector similarity searches with standard SQL filters and complex PostGIS spatial queries (e.g., “find vectors similar to this example within this specific national park boundary” or “near this river system”) all within a single query interface. Standalone vector databases often lack this mature geospatial query capability.

  • Maturity and Ecosystem: Building on PostgreSQL means inheriting its decades of development, renowned stability, robust tooling, and wide community support – essential for a mission-critical platform like Earth Index.

However, searching 3.2 billion vectors efficiently requires more than just standard PostgreSQL capabilities.

Why Earth Index Chose VectorChord for Performance and Affordability

While the PostgreSQL ecosystem provided the ideal foundation, achieving the required performance and affordability at the scale of 3.2 billion vectors presented a significant challenge. Earth Index needed a solution that could deliver speed without breaking the bank.

Finding the right balance of performance, cost, and integration was challenging. Hutch Ingold, CTO of Earth Genome, explains their considerations:

When we evaluated solutions for our 3.2 billion vectors – recognizing the rapidly evolving tech landscape – standard pgvector didn’t meet our performance requirements at that scale. Dedicated options like Qdrant posed difficulties: hosted versions were too expensive, and self-hosting clustered deployments on our Kubernetes infrastructure seemed insufficiently mature at the time. We also examined cloud services, such as Google’s Vertex AI Vector Search, but the list price of $237,000 per month for our dataset was simply prohibitive.

This is where VectorChord became the enabling technology:

  • Performance Beyond Vanilla PGVector: VectorChord delivers the necessary speed enhancements – up to 5x faster queries, 16x higher insert throughput, and 16x quicker index building – addressing the performance limitations encountered with standard pgvector at this massive scale.

  • Affordable Scale via Disk-Based Indexing: Critically, VectorChord’s optimized disk-based indexing (leveraging cost-effective SSDs) directly counters the prohibitive costs highlighted by the CTO. It avoids the massive RAM requirements of purely in-memory approaches and the exorbitant monthly fees of managed cloud vector services, making the 3.2 billion vector index financially feasible.

  • Native Integration & Scalability: As a PostgreSQL extension, VectorChord provided these benefits within their chosen database, requiring no infrastructure changes. It’s built to scale efficiently, handling the billions of vectors needed for Earth Index’s global monitoring platform while maintaining interactive performance.

VectorChord provided the crucial combination of high performance, massive scalability, and cost-effectiveness directly within the familiar and powerful PostgreSQL environment that Earth Index needed.

Earth Index: Technical Architecture and Setup

To manage its planetary dataset effectively and affordably, Earth Index built its backend on a cluster of three powerful AWS EC2 i8g.16xlarge instances, powered by the latest Graviton4 ARM processor and Nitro SSD. Each provides 512 GB RAM, 64 vCPUs, and 15 TB of NVMe storage, offering a solid foundation for both computation and disk I/O. This setup, costing around $12,000 per month, delivers massive cost savings compared to the estimated $237,000 monthly fee for a comparable managed cloud vector service, making the project economically viable. These instances run PostgreSQL enhanced with VectorChord and PostGIS, efficiently hosting both the vector data and other application components.

Earth Index Architecture

The core data resides in a partitioned table structure across the three nodes. The primary table, globe24, stores each tile’s unique ID, its geographic point geometry (geometry(Point, 4326)), a partition key (partkey), and the crucial 384-dimensional AI-generated embedding stored efficiently as halfvec(384). This sharding distributes the 3.2 billion rows across the cluster, allowing for parallel processing.

A key advantage of this architecture is the ability to perform complex, integrated queries. Earth Index commonly executes SQL statements that seamlessly combine VectorChord’s fast vector search (embedding <=> '{vector}'::vector) with PostGIS geospatial filtering (st_within(geom, '{geom}')) and partition pruning (partkey IN (...)). This allows users to find visually similar locations within specific geographic boundaries efficiently. Even with billions of vectors, this integrated approach yields practical performance, achieving a median query latency (p50) of approximately 761 ms for typical searches returning the top 500 results.

Enabling Environmental Action at Scale

By providing a cost-effective, scalable, and tightly integrated vector search solution within PostgreSQL, VectorChord helps enable Earth Index’s groundbreaking work. It allows them to manage their enormous “Planetary DNA” database affordably, ensuring that this powerful tool for environmental monitoring can reach the organizations and communities who need it most.

Join the Earth Index waitlist today to explore the environmental challenges in your neighborhood, understand your ecological footprint, and be part of a global effort to protect the planet.

We are thrilled to see Earth Index leverage VectorChord to turn satellite data into actionable environmental intelligence. Their success demonstrates the power of combining cutting-edge AI, robust database technology, and efficient vector search to address critical global challenges.

Interested in bringing large-scale vector search to your PostgreSQL database? Learn more about VectorChord or get started today!