VectorChord Vector Search on PostgreSQL 18 on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers the deployment and use of VectorChord on PostgreSQL 18 on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images.
VectorChord (the vchord extension) adds the vchordrq vector index to PostgreSQL. Instead of walking a graph, vchordrq splits your embeddings into lists (partitions), compresses them with RaBitQ quantisation, scans only the lists closest to the query, and reranks the best candidates with exact distances. It uses pgvector's vector type and distance operators (<->, <=>, <#>), so existing pgvector tables and queries work unchanged, and pgvector's own HNSW and IVFFlat indexes remain available next to it.
pgvector is included and is required. VectorChord declares requires = 'vector', so pgvector is installed and enabled alongside it.
What this image is, and what it is not. It is one PostgreSQL 18 server per virtual machine with VectorChord preloaded. It is not a managed service, not a cluster and has no web interface: you work with it in SQL through psql or any PostgreSQL driver.
What is included:
-
PostgreSQL 18.6 from the official PGDG repository, running under systemd as
postgresql@18-main.service -
VectorChord 1.1.1 preloaded (
shared_preload_libraries = 'vchord') and pgvector 0.8.7, both enabled in the defaultvectordbdatabase -
A demo
itemstable of twenty thousand 128 dimensional vectors with a realvchordrqindex, so you can run a similarity search the moment the VM boots -
vectorchord-benchmark, which builds a pgvector HNSW index and a VectorChordvchordrqindex on the same data on your VM and reports build time, size, recall and latency for each -
A per VM database cluster, a per VM TLS certificate and two per VM passwords (the
postgressuperuser and thevectorappapplication role) generated on first boot into a root only file -
Loopback only networking by default: the only port reachable from the network is TCP 22
-
Unattended security upgrades left enabled
Licensing. VectorChord is offered upstream under a choice of the GNU Affero General Public License v3 (AGPL-3.0-only) or the Elastic License 2.0. cloudimg distributes it in this image under the AGPL v3 only. The image ships the AGPL text, a notice recording that election, and the complete corresponding source of the installed build (the upstream release commit) in /usr/share/doc/postgresql-18-vchord/. cloudimg does not modify VectorChord. PostgreSQL and pgvector are under the PostgreSQL License.
VectorChord is a trademark of its owner. PostgreSQL is a registered trademark of the PostgreSQL Community Association. cloudimg is not affiliated with, sponsored by or endorsed by either; this image packages the open source software for use on Azure.
Prerequisites
-
Active Azure subscription, an SSH public key, and a VNet plus subnet in the target region
-
Subscription to this listing on Azure Marketplace
-
A Network Security Group allowing TCP 22 from your administration network. Nothing else needs to be opened: PostgreSQL listens on loopback in the shipped image.
Recommended virtual machine size: Standard_B2ms (2 vCPU, 8 GB RAM). PostgreSQL memory settings (shared_buffers, effective_cache_size, maintenance_work_mem) are sized from the VM's RAM on first boot, so a larger size is used automatically. For production workloads with millions of vectors choose a memory rich size such as Standard_E4s_v5 and a Premium SSD.
Deploy the virtual machine
Create the VM from the image, opening only SSH to your own network:
# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn cloudimg1647283583153:vectorchord-ubuntu-24-04:default:latest
az vm create \
--resource-group my-rg \
--name vectorchord-1 \
--image cloudimg1647283583153:vectorchord-ubuntu-24-04:default:latest \
--size Standard_B2ms \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard
First boot creates the database cluster, generates the passwords and builds the demo index. It takes under a minute.
Confirm first boot finished
SSH in as azureuser and confirm the first boot unit and PostgreSQL are both active, and that the ready marker exists:
systemctl is-active vectorchord-firstboot.service postgresql@18-main.service
ls -l /var/lib/cloudimg/vectorchord-ready
Both units report active. The ready marker is written only after the passwords have been set, and PostgreSQL refuses to start with any network rule until it exists.

Retrieve your per VM credentials
Read the root only credentials file written on first boot:
sudo cat /root/vectorchord-credentials.txt
It is 0600 root:root and holds the postgres superuser password, the vectorapp application role password and the database name (vectordb). These values are unique to this VM. The passwords were sent to PostgreSQL only as SCRAM-SHA-256 verifiers, so they do not appear in any log.
Connect to the database
The quickest local connection uses peer authentication and needs no password:
sudo -u postgres psql -d vectordb -c "SELECT version();"
To connect as the application role, store its password in your own ~/.pgpass (so it never appears on a command line or in your shell history), then connect over TLS:
sudo awk -F= '/^vectorapp\.password=/{print "127.0.0.1:5432:vectordb:vectorapp:"$2}' /root/vectorchord-credentials.txt > ~/.pgpass && chmod 600 ~/.pgpass
psql "host=127.0.0.1 dbname=vectordb user=vectorapp sslmode=require" -w -c "SELECT current_user, s.ssl, s.version FROM pg_stat_ssl s WHERE s.pid = pg_backend_pid();"
The result shows vectorapp, t and TLSv1.3: the session is encrypted and authenticated with the per VM password.
Verify the extensions
sudo -u postgres psql -d vectordb -c "SELECT extname, extversion FROM pg_extension WHERE extname IN ('vchord','vector') ORDER BY extname;"
sudo -u postgres psql -d vectordb -c "SHOW shared_preload_libraries;"
vchord is VectorChord, vector is pgvector, and VectorChord is loaded at server start.

Run a similarity search on the demo table
The vectordb database ships an items table with twenty thousand 128 dimensional vectors and a vchordrq index named items_embedding_idx:
psql "host=127.0.0.1 dbname=vectordb user=vectorapp sslmode=require" -w -c "SELECT count(*) AS rows, pg_size_pretty(pg_relation_size('items_embedding_idx')) AS index_size, (SELECT amname FROM pg_am a JOIN pg_class c ON c.relam = a.oid WHERE c.relname = 'items_embedding_idx') AS access_method FROM items;"
Find the five nearest neighbours of item 42 by Euclidean distance:
psql "host=127.0.0.1 dbname=vectordb user=vectorapp sslmode=require" -w -c "SELECT id, category, round((embedding <-> (SELECT embedding FROM items WHERE id = 42))::numeric, 4) AS distance FROM items ORDER BY embedding <-> (SELECT embedding FROM items WHERE id = 42) LIMIT 5;"
Item 42 comes back first at distance zero, followed by its nearest neighbours from the same cluster. Confirm the search really uses the vchordrq index:
psql "host=127.0.0.1 dbname=vectordb user=vectorapp sslmode=require" -w -c "EXPLAIN (COSTS OFF) SELECT id FROM items ORDER BY embedding <-> (SELECT embedding FROM items WHERE id = 42) LIMIT 5;"
The plan shows Index Scan using items_embedding_idx.

The probes setting
A vchordrq index built with lists answers a query by scanning the vchordrq.probes lists closest to it. More probes means higher recall and more work per query. A query against a partitioned index fails with need 1 probes, but 0 probes provided if no probes value is set, so this image sets a default of 10 for the vectordb database:
psql "host=127.0.0.1 dbname=vectordb user=vectorapp sslmode=require" -w -c "SHOW vchordrq.probes;"
Set it for a new database with ALTER DATABASE yourdb SET vchordrq.probes = '10';, or per session with SET vchordrq.probes = '30';. A good starting point is three to ten percent of the number of lists, then measure.
Index your own embeddings
Create a table with a vector column sized to your embedding model, load your data, then build the index. For cosine similarity, which most text embedding models use, upstream recommends residual quantisation and spherical centroids:
CREATE TABLE documents (
id bigserial PRIMARY KEY,
content text,
embedding vector(768)
);
-- load your rows, then:
CREATE INDEX ON documents USING vchordrq (embedding vector_cosine_ops) WITH (options = $$
residual_quantization = true
[build.internal]
lists = [1000]
spherical_centroids = true
build_threads = 2
$$);
SET vchordrq.probes = '30';
SELECT id, content FROM documents ORDER BY embedding <=> '[...]' LIMIT 10;
Choose lists from the number of rows: upstream suggests roughly four to eight times the square root of the row count. Below about one hundred thousand rows you can leave lists out entirely. Build the index after loading the data, because the partitions are learned from the rows present at build time.
To measure recall for a real query, VectorChord compares the index result with an exact search:
SELECT vchordrq_evaluate_query_recall(query => $$
SELECT ctid FROM documents ORDER BY embedding <=> '[...]' LIMIT 10
$$, exact_search => true);
Benchmark: vchordrq against pgvector HNSW
vectorchord-benchmark generates a deterministic dataset of embedding like vectors (768 dimensions, L2 normalised, cosine distance), computes the exact top ten neighbours of each query by sequential scan, then builds a pgvector HNSW index and a VectorChord vchordrq index on the same table with the same memory and parallel worker settings, and measures each at several search settings. Latency is measured inside the server, so it excludes network and client overhead.
Run a quick version on your VM (about a minute):
sudo vectorchord-benchmark --rows 20000 --dim 768 --queries 50
Measured result, Standard_B2ms (2 vCPU, 8 GB RAM) launched from this image, 100,000 rows of 768 dimensions, 200 queries, top 10:
| Index | Build time | Index size |
|---|---|---|
| pgvector HNSW (m 16, ef_construction 64) | 129.9 s | 391 MB |
| VectorChord vchordrq (1,264 lists) | 23.4 s | 445 MB |
| Index | Setting | Recall@10 | p50 latency | p95 latency | Queries per second |
|---|---|---|---|---|---|
| HNSW | ef_search 40 | 0.861 | 1.46 ms | 2.94 ms | 574 |
| HNSW | ef_search 100 | 0.969 | 1.94 ms | 4.65 ms | 416 |
| HNSW | ef_search 200 | 0.984 | 2.95 ms | 5.53 ms | 300 |
| vchordrq | probes 37 | 1.000 | 1.05 ms | 1.51 ms | 849 |
| vchordrq | probes 126 | 1.000 | 1.92 ms | 2.51 ms | 480 |
| vchordrq | probes 316 | 1.000 | 3.64 ms | 4.16 ms | 265 |
| vchordrq | probes 632 | 1.000 | 6.57 ms | 7.30 ms | 149 |
How to read it. The clear and repeatable difference is index build time: vchordrq built about five and a half times faster than HNSW here (23 seconds against 130), because it learns partitions with k-means instead of inserting every vector into a graph. Its index was about 14 percent larger. On this dataset vchordrq also returned the exact top ten at a lower latency than HNSW reached at its best recall. That part depends on how well your data clusters: on a smaller, less clustered run during the build (20,000 rows on a 4 GB VM) the two were much closer, and on data with little cluster structure HNSW holds its recall better while vchordrq needs more probes. The B-series sizes are burstable, so a long benchmark on a VM that has used its CPU credits runs slower. Synthetic data is only a stand in: run the benchmark on a copy of your own embeddings before you choose.
The full size run takes several minutes:
sudo vectorchord-benchmark --rows 100000 --dim 768 --queries 200

Security posture
The database is not reachable from the network. PostgreSQL listens on loopback only, so the only port listening off box is TCP 22:
sudo ss -tlnH | awk '{print $4}' | sort
sudo -u postgres psql -c "SHOW listen_addresses;"
Every TCP connection requires TLS and a password. Ask PostgreSQL for the authentication rules it actually applies:
sudo -u postgres psql -c "SELECT type, database, user_name, address, auth_method FROM pg_hba_file_rules ORDER BY rule_number;"
There is one local rule for peer authentication over the Unix socket and two hostssl rules for loopback with scram-sha-256. There is no plaintext host rule.
Fail closed before first boot completes. The image contains no database cluster. First boot creates one, starts PostgreSQL with Unix socket access only, sets the passwords, and only then adds the TLS loopback rules and writes the ready marker. A start guard on postgresql@.service refuses to start a cluster that listens beyond loopback or has any TCP rule while the ready marker is missing.
SSH. Root has no login path, and your choice of key or password authentication in the portal is honoured:
sudo sshd -T | grep -E '^(permitrootlogin|clientaliveinterval) '
Kernel module baseline. The image fences the IPsec and RxRPC modules associated with the Dirty Frag vulnerabilities (CVE-2026-43284, CVE-2026-43500). This appliance terminates no IPsec, so the fence costs nothing:
sudo modprobe -n -v esp4
The last line reads install /bin/false.
Connect from your workstation over an SSH tunnel
The recommended way to reach the database from your own machine is an SSH tunnel, which needs no change to the VM:
ssh -N -L 5432:127.0.0.1:5432 azureuser@<vm-public-ip>
Then, on your workstation, connect to localhost as if the database were local:
psql "host=localhost port=5432 dbname=vectordb user=vectorapp sslmode=require"
Optionally expose PostgreSQL to your application subnet
Opening the database is a deliberate change. Scope it to your own subnet, never to the internet. TLS with a per VM certificate is already on.
First allow your subnet in the authentication rules and listen on all interfaces, then restart PostgreSQL:
echo "hostssl all all 10.0.1.0/24 scram-sha-256" | sudo tee -a /etc/postgresql/18/main/pg_hba.conf
echo "listen_addresses = '*'" | sudo tee /etc/postgresql/18/main/conf.d/99-local.conf
sudo chown postgres:postgres /etc/postgresql/18/main/conf.d/99-local.conf
sudo systemctl restart postgresql
Then open TCP 5432 to the same subnet in the VM's Network Security Group:
az network nsg rule create \
--resource-group my-rg \
--nsg-name my-nsg \
--name allow-postgres-from-app-subnet \
--priority 200 \
--source-address-prefixes 10.0.1.0/24 \
--destination-port-ranges 5432 \
--access Allow --protocol Tcp
Clients must now present the per VM password over TLS. The shipped certificate is self signed, so use sslmode=require, or replace /etc/ssl/certs/ssl-cert-snakeoil.pem and its key with a certificate from your own CA and use sslmode=verify-full.
Rotating the credentials
Change a password interactively with \password, which hashes it in psql before sending, so the new password never reaches the server logs:
sudo -u postgres psql -d vectordb
\password vectorapp
Then update /root/vectorchord-credentials.txt and your ~/.pgpass to match.
Licence and source
The licence election, the AGPL text and the source of the installed VectorChord build are in the image:
ls /usr/share/doc/postgresql-18-vchord/
grep -E 'ONLY|commit' /usr/share/doc/postgresql-18-vchord/LICENSE-ELECTION
Maintenance
-
Security updates. Unattended upgrades remain enabled for Ubuntu security updates. PostgreSQL, pgvector and VectorChord come from the PGDG repository and the VectorChord release, so update them deliberately:
sudo apt-get update && sudo apt-get install --only-upgrade postgresql-18 postgresql-18-pgvector, then restart PostgreSQL. -
Upgrading VectorChord. Install the newer
postgresql-18-vchordpackage from the upstream release and runALTER EXTENSION vchord UPDATE;in each database. -
Rebuilding an index.
vchordrqlearns its partitions at build time. After the data has grown or changed a lot,REINDEX INDEX CONCURRENTLY your_index;re-learns them. -
Backups. Indexes are part of the database, so ordinary PostgreSQL backups (
pg_dump,pg_basebackupor an Azure disk snapshot) capture them along with your data.
Support
cloudimg provides 24/7 support for this image. Raise a request at cloudimg.co.uk with the VM size, the region and the output of sudo -u postgres psql -d vectordb -c "SELECT extname, extversion FROM pg_extension;".