Vector analysis performance testing
This topic walks you through running vector search benchmarks on AnalyticDB for PostgreSQL using the ann-benchmarks framework. It covers environment setup, dataset preparation, recall rate testing, and throughput testing, with reference configurations and result tables for four standard datasets.
Test environment
To eliminate network latency from benchmark results, deploy your AnalyticDB for PostgreSQL instance and the Elastic Compute Service (ECS) test client in the same virtual private cloud (VPC).
AnalyticDB for PostgreSQL specifications
| Engine version | Edition | Node specifications | Compute nodes | Storage per node | Storage type |
|---|---|---|---|---|---|
| V6.6.2.5 | Basic Edition (formerly High-performance Edition) | 8 cores, 32 GB | 2 | 1,000 GB | PL1 Enterprise SSD (ESSD) |
ECS specifications
| CPU | Memory | Disk storage |
|---|---|---|
| 16 cores | 32 GB | 2 TB |
Prerequisites
Before you begin, ensure that you have:
-
An AnalyticDB for PostgreSQL instance (V6.6.2.5, Basic Edition)
-
An ECS instance in the same VPC as the AnalyticDB for PostgreSQL instance
-
Python 3.8 or later installed on the ECS instance
-
Docker version later than 20 installed — see Install Docker Desktop on Linux
Set up the test environment
-
Download the ann-benchmarks test tool: adbpg_ann_benchmark_20250430.tar.gz.
-
Install the dependencies:
pip install -r requirements.txt -
Build the test image:
python install.py --proc 4 --algorithm adbpg
Prepare the test dataset
Download a dataset and place it in the data directory of the ann-benchmarks project.
| Dataset | Dimensions | Samples | Metric | Dataset parameter | Download |
|---|---|---|---|---|---|
| GIST | 960 | 1,000,000 | L2 similarity | gist-960-euclidean | GIST |
| SIFT-10M | 128 | 10,000,000 | L2 similarity | sift-128-euclidean | SIFT-10M |
| SIFT-100M | 128 | 100,000,000 | L2 similarity | sift100m-128-euclidean | SIFT-100M |
| Deep | 96 | 10,000,000 | Cosine similarity | deep-image-96-angular | Deep |
| Cohere | 768 | 1,000,000 | L2 similarity | cohere-768-euclidean | Cohere |
| Dbpedia | 1,536 | 1,000,000 | Cosine similarity | dbpedia-openai-1000k-angular | Dbpedia |
Run the benchmark
Step 1: Configure the connection
Edit ann_benchmarks/algorithms/adbpg/module.py and fill in your instance details:
# Internal endpoint of the AnalyticDB for PostgreSQL instance
self._host = 'gp-bp10ofhzg2z****-master.gpdb.rds.aliyuncs.com'
# Port number
self._port = 5432
# Database name
self._dbname = '<database_name>'
# Database account name
self._user = '<user_name>'
# Database account password
self._password = '<YOUR_PASSWORD>'
Step 2: Configure index and search parameters
Edit ann_benchmarks/algorithms/adbpg/config.yml with your target dataset and index mode.
Index creation parameters (arg_groups) — for details on creating a vector index, see Create a vector index.
| Parameter | Description |
|---|---|
M |
Controls the number of bi-directional links in the Hierarchical Navigable Small World (HNSW) index. Higher values improve recall but increase index build time. |
efConstruction |
Controls search quality during index construction. Higher values improve index quality at the cost of longer build time. |
parallel_build |
Number of parallel workers for index construction. Set this to the number of CPU cores on your compute nodes. |
external_storage |
Index caching policy: 1 for mmap, 0 for shared_buffer. Important
Only AnalyticDB for PostgreSQL V6.0 supports this parameter. |
pq_enable |
Enables product quantization (PQ): 1 to enable, 0 to disable. |
pq_segments |
Number of PQ segments. Set this to number of dimensions / 8. |
Search parameters (query_args):
| Parameter | Description |
|---|---|
ef_search |
Number of nearest neighbors examined during HNSW index search. Higher values improve recall at the cost of higher latency. |
max_scan_points |
Maximum number of candidate vectors scanned per query. Reducing this value increases QPS but may lower recall. |
pq_amp |
PQ amplification factor. Has no effect when PQ is disabled. |
parallel |
Number of concurrent queries. Applies only in batch mode. |
Tuning for 95% recall rate
The test targets a recall rate of ≥95% with top-10 results. When recall falls short, adjust parameters in this order:
-
Increase
ef_searchfirst — this is the most direct lever for recall rate and has a predictable effect. -
If recall is still insufficient after increasing
ef_search, increasemax_scan_points. -
Once recall meets the target, reduce
max_scan_pointsincrementally to maximize QPS while keeping recall at ≥95%.
AnalyticDB for PostgreSQL provides the following reference configurations for each test dataset, tuned to achieve ≥95% recall:
# GIST 960
float:
any:
- base_args: ['@metric']
constructor: ADBPG
disabled: false
docker_tag: ann-benchmarks-adbpg
module: ann_benchmarks.algorithms.adbpg
name: adbpg
run_groups:
nopq_mmap:
arg_groups: [{M: 64, efConstruction: 600, parallel_build: 8, external_storage: 1, pq_enable: 0, pq_segments: 120}]
query_args: [[ {ef_search: 100, max_scan_points: 3200, pq_amp: 10, parallel: 1}, {ef_search: 100, max_scan_points: 3200, pq_amp: 10, parallel: 5}, {ef_search: 400, max_scan_points: 3200, pq_amp: 10, parallel: 10}, {ef_search: 100, max_scan_points: 3200, pq_amp: 10, parallel: 15}, {ef_search: 100, max_scan_points: 3200, pq_amp: 10, parallel: 20}, {ef_search: 100, max_scan_points: 3200, pq_amp: 10, parallel: 25}, {ef_search: 100, max_scan_points: 3200, pq_amp: 10, parallel: 30}, {ef_search: 100, max_scan_points: 3200, pq_amp: 10, parallel: 50}]]
# Deep 96
float:
any:
- base_args: ['@metric']
constructor: ADBPG
disabled: false
docker_tag: ann-benchmarks-adbpg
module: ann_benchmarks.algorithms.adbpg
name: adbpg
run_groups:
nopq_mmap:
arg_groups: [{M: 64, efConstruction: 600, parallel_build: 8, external_storage: 1, pq_enable: 0, pq_segments: 12}]
query_args: [[ {ef_search: 400, max_scan_points: 1500, pq_amp: 10, parallel: 1}, {ef_search: 400, max_scan_points: 1500, pq_amp: 10, parallel: 5}, {ef_search: 400, max_scan_points: 1500, pq_amp: 10, parallel: 10}, {ef_search: 400, max_scan_points: 1500, pq_amp: 10, parallel: 15}, {ef_search: 400, max_scan_points: 1500, pq_amp: 10, parallel: 20}, {ef_search: 400, max_scan_points: 1500, pq_amp: 10, parallel: 25}, {ef_search: 400, max_scan_points: 1500, pq_amp: 10, parallel: 30}, {ef_search: 400, max_scan_points: 1500, pq_amp: 10, parallel: 50}]]
# Cohere 768
float:
any:
- base_args: ['@metric']
constructor: ADBPG
disabled: false
docker_tag: ann-benchmarks-adbpg
module: ann_benchmarks.algorithms.adbpg
name: adbpg
run_groups:
nopq_mmap:
arg_groups: [{M: 64, efConstruction: 600, parallel_build: 8, external_storage: 1, pq_enable: 0, pq_segments: 96}]
query_args: [[ {ef_search: 400, max_scan_points: 600, pq_amp: 10, parallel: 1}, {ef_search: 400, max_scan_points: 600, pq_amp: 10, parallel: 5}, {ef_search: 400, max_scan_points: 600, pq_amp: 10, parallel: 10}, {ef_search: 400, max_scan_points: 600, pq_amp: 10, parallel: 15}, {ef_search: 400, max_scan_points: 600, pq_amp: 10, parallel: 20}, {ef_search: 400, max_scan_points: 600, pq_amp: 10, parallel: 25}, {ef_search: 400, max_scan_points: 600, pq_amp: 10, parallel: 30}, {ef_search: 400, max_scan_points: 600, pq_amp: 10, parallel: 50}]]
# Dbpedia 1536
float:
any:
- base_args: ['@metric']
constructor: ADBPG
disabled: false
docker_tag: ann-benchmarks-adbpg
module: ann_benchmarks.algorithms.adbpg
name: adbpg
run_groups:
nopq_mmap:
arg_groups: [{M: 64, efConstruction: 600, parallel_build: 8, external_storage: 1, pq_enable: 0, pq_segments: 192}]
query_args: [[ {ef_search: 400, max_scan_points: 425, pq_amp: 10, parallel: 1}, {ef_search: 400, max_scan_points: 425, pq_amp: 10, parallel: 5}, {ef_search: 400, max_scan_points: 425, pq_amp: 10, parallel: 10}, {ef_search: 400, max_scan_points: 425, pq_amp: 10, parallel: 15}, {ef_search: 400, max_scan_points: 425, pq_amp: 10, parallel: 20}, {ef_search: 400, max_scan_points: 425, pq_amp: 10, parallel: 25}, {ef_search: 400, max_scan_points: 425, pq_amp: 10, parallel: 30}, {ef_search: 400, max_scan_points: 425, pq_amp: 10, parallel: 50}]]
Step 3: Test the recall rate
Run the following command to start the recall rate test:
nohup python run.py --algorithm adbpg --dataset <Dataset> --runs 1 --timeout 990000 \
> annbenchmark_deep.log 2>&1 &
Replace <Dataset> with the dataset parameter from the table above (for example, gist-960-euclidean).
After the test completes, query the results:
python plot.py --dataset <Dataset> --recompute
Sample output:
0: ADBPG(m=64, ef_construction=600, ef_search=400, max_scan_point=500, pq_amp=10) recall: 0.963 qps: 126.200
1: ADBPG(m=64, ef_construction=600, ef_search=400, max_scan_point=1000, pq_amp=10) recall: 0.992 qps: 122.665
If recall does not reach 95%, adjust ef_search and max_scan_points as described above and re-run the test.
Step 4: Test the search performance
Vector search benchmarks measure two distinct characteristics depending on your use case:
-
Latency (
parallel: 1): Measures how fast the system responds to a single query. Use this to evaluate real-time search scenarios where low response time is critical. -
Throughput (
parallel: 5toparallel: 50): Measures how many queries the system handles per second under concurrent load. Use this to evaluate batch or multi-user search scenarios.
After confirming that the recall rate meets your target, run the throughput test with the --batch flag:
nohup python run.py --algorithm adbpg --dataset <Dataset> --runs 1 --timeout 990000 \
--batch > annbenchmark_deep.log 2>&1 &
After the test completes, open annbenchmark_deep.log to review queries per second (QPS), average response time (RT), and P99 RT at each concurrency level:
2023-12-20 17:31:39,297 - INFO - query using 25 parallel
worker 0 cost 9.50 s, qps 315.92, mean rt 0.00317, p99 rt 0.00951
2023-12-20 17:31:49,097 - INFO - QPS: 7653.155
2023-12-20 17:31:49,113 - INFO - query using 30 parallel
worker 0 cost 13.87 s, qps 216.36, mean rt 0.00462, p99 rt 0.04298
2023-12-20 17:32:03,260 - INFO - QPS: 6361.819
2023-12-20 17:32:03,281 - INFO - query using 50 parallel
worker 0 cost 20.78 s, qps 144.36, mean rt 0.00693, p99 rt 0.02735
2023-12-20 17:32:24,385 - INFO - QPS: 7107.920
Test results
The following tables show benchmark results for four datasets on a 2-node instance (8 cores, 32 GB per node). All results are measured at ≥95% recall rate with top-10 results.
Choose an index mode
Select the index mode that matches your workload before reviewing per-dataset results:
| Index mode | Use when |
|---|---|
| noPQ + mmap | Memory is sufficient to cache all vectors and indexes. Delivers the best query performance. Suited for workloads with few update or delete operations. |
| PQ + mmap | Vector count exceeds 1,000,000 and memory is insufficient to cache all vectors. Accepts moderate performance for better memory efficiency. Suited for workloads with few update or delete operations. |
| PQ + shared_buffer | Vector count exceeds 1,000,000, memory is limited, and the workload has frequent update or delete operations. |
| noPQ + shared_buffer | Vector count is below 1,000,000, memory is sufficient to cache all vectors, and the workload has frequent update or delete operations. |
Index creation time reflects the one-time cost to build the index before the instance serves queries. It does not affect query performance after the index is built, but plan for it when sizing initial data load time, especially for large datasets.
Instance specifications: 8 cores and 32 GB × 2 compute nodes
Dataset: GIST L2 (960 dimensions × 1,000,000 samples)
Index mode: noPQ + mmap
Index creation parameters: M: 64, efConstruction: 600, parallel_build: 8, external_storage: 1, pq_enable: 0
Search parameters: ef_search: 100, max_scan_points: 3200
| Index creation time (s) | Query concurrency | QPS | Average RT (ms) | P99 RT (ms) |
|---|---|---|---|---|
| 485 | 1 | 396 | 1 | 2 |
| 5 | 1,744 | 2 | 3 | |
| 10 | 3,073 | 2 | 4 | |
| 15 | 3,358 | 3 | 10 | |
| 20 | 3,511 | 5 | 15 | |
| 25 | 3,601 | 6 | 21 | |
| 30 | 3,689 | 7 | 25 | |
| 50 | 3,823 | 12 | 36 |
Dataset: Deep IP (96 dimensions × 10,000,000 samples)
Index mode: noPQ + mmap
Index creation parameters: M: 64, efConstruction: 600, parallel_build: 8, external_storage: 1, pq_enable: 0
Search parameters: ef_search: 400, max_scan_points: 1500
| Index creation time (s) | Query concurrency | QPS | Average RT (ms) | P99 RT (ms) |
|---|---|---|---|---|
| 1,778 | 1 | 878 | 1 | 2 |
| 5 | 4,344 | 1 | 2 | |
| 10 | 7,950 | 1 | 3 | |
| 15 | 10,114 | 1 | 4 | |
| 20 | 10,629 | 1 | 5 | |
| 25 | 10,858 | 2 | 7 | |
| 30 | 11,093 | 2 | 9 | |
| 50 | 11,354 | 4 | 16 |
Dataset: Cohere L2 (768 dimensions × 1,000,000 samples)
Index mode: noPQ + mmap
Index creation parameters: M: 64, efConstruction: 600, parallel_build: 8, external_storage: 1, pq_enable: 0
Search parameters: ef_search: 400, max_scan_points: 600
| Index creation time (s) | Query concurrency | QPS | Average RT (ms) | P99 RT (ms) |
|---|---|---|---|---|
| 465 | 1 | 561 | 1 | 2 |
| 5 | 2,893 | 1 | 2 | |
| 10 | 5,108 | 1 | 3 | |
| 15 | 5,488 | 2 | 5 | |
| 20 | 5,969 | 2 | 8 | |
| 25 | 6,195 | 3 | 12 | |
| 30 | 6,098 | 4 | 19 | |
| 50 | 6,138 | 7 | 39 |
Dataset: Dbpedia IP (1,536 dimensions × 1,000,000 samples)
Index mode: noPQ + mmap
Index creation parameters: M: 64, efConstruction: 600, parallel_build: 8, external_storage: 1, pq_enable: 0
Search parameters: ef_search: 400, max_scan_points: 425
| Index creation time (s) | Query concurrency | QPS | Average RT (ms) | P99 RT (ms) |
|---|---|---|---|---|
| 807 | 1 | 453 | 1 | 2 |
| 5 | 1,948 | 1 | 3 | |
| 10 | 2,820 | 2 | 4 | |
| 15 | 2,903 | 4 | 11 | |
| 20 | 2,860 | 6 | 19 | |
| 25 | 2,897 | 7 | 27 | |
| 30 | 2,880 | 9 | 34 | |
| 50 | 2,877 | 16 | 63 |