Vector search
SelectDB (powered by Apache Doris) supports approximate nearest neighbor (ANN) search on vector data, enabling efficient similarity retrieval for RAG (Retrieval-Augmented Generation) and other AI applications. Vectors are stored as fixed-length arrays and accelerated by an HNSW-based ANN index, supporting top-N search, range search, filtered search, and multiple quantization methods.
Approximate nearest neighbor search
Apache Doris has officially supported ANN search since version 4.0. The system does not introduce a new data type for vectors; they are stored as fixed-length arrays. To accelerate vector distance retrieval, Doris implements a new ANN index type based on Faiss. The following example uses the common SIFT dataset to demonstrate how to create a table with an ANN index:
CREATE TABLE sift_1M (
id int NOT NULL,
embedding array<float> NOT NULL COMMENT "",
INDEX ann_index (embedding) USING ANN PROPERTIES(
"index_type"="hnsw",
"metric_type"="l2_distance",
"dim"="128",
"quantizer"="flat"
)
) ENGINE=OLAP
DUPLICATE KEY(id) COMMENT "OLAP"
DISTRIBUTED BY HASH(id) BUCKETS 1
PROPERTIES (
"replication_num" = "1"
);
-
index_type:hnswspecifies the use of the Hierarchical Navigable Small World algorithm. -
metric_type:l2_distancespecifies L2 distance as the distance function. -
dim:128indicates that the vector dimension is 128. -
quantizer:flatmeans that each dimension is stored in its original float32 format.
|
Parameter |
Required |
Value |
Default |
Description |
|
|
Yes |
Only |
(None) |
Specifies the ANN index algorithm to use. Currently, only HNSW is supported. |
|
|
Yes |
|
(None) |
Specifies the vector similarity or distance metric. |
|
|
Yes |
Positive integer (> 0) |
(None) |
Specifies the vector dimension. All imported vectors must have a dimension that matches this value; otherwise, an error is reported. |
|
|
No |
Positive integer |
|
The maximum number of neighbors (M) for a single node in the HNSW graph. This affects index memory usage and search performance. |
|
|
No |
Positive integer |
|
The size of the candidate queue (efConstruction) during the HNSW build phase. A larger value results in a higher-quality graph but slower build times. |
|
|
No |
|
|
Specifies the vector encoding or quantization method: |
|
|
Required when |
Positive integer |
(None) |
Specifies the number of sub-vectors to split the original high-dimensional vector into. The vector dimension ( |
|
|
Required when |
Positive integer |
(None) |
Specifies the number of bits to quantize each sub-vector. This determines the size of the codebook for each subspace (k = 2 ^ pq_nbits). In Faiss, the value of |
Import data using S3 TVF:
INSERT INTO sift_1M
SELECT *
FROM S3(
"uri" = "https://selectdb-customers-tools-bj.oss-cn-beijing.aliyuncs.com/sift_database.tsv",
"format" = "csv");
select count(*) from sift_1M
--------------
+----------+
| count(*) |
+----------+
| 1000000 |
+----------+
Using l2_distance_approximate or inner_product_approximate triggers the ANN index path. The function name must exactly match the index's metric_type (for example, metric_type='l2_distance' requires using l2_distance_approximate; metric_type='inner_product' requires using inner_product_approximate). For sorting, use ascending order for L2 distance (smaller is closer) and descending order for inner product (larger is closer).
SELECT id,
l2_distance_approximate(
embedding,
[0,11,77,24,3,0,0,0,28,70,125,8,0,0,0,0,44,35,50,45,9,0,0,0,4,0,4,56,18,0,3,9,16,17,59,10,10,8,57,57,100,105,125,41,1,0,6,92,8,14,73,125,29,7,0,5,0,0,8,124,66,6,3,1,63,5,0,1,49,32,17,35,125,21,0,3,2,12,6,109,21,0,0,35,74,125,14,23,0,0,6,50,25,70,64,7,59,18,7,16,22,5,0,1,125,23,1,0,7,30,14,32,4,0,2,2,59,125,19,4,0,0,2,1,6,53,33,2]
) AS distance
FROM sift_1M
ORDER BY distance
LIMIT 10;
--------------
+--------+----------+
| id | distance |
+--------+----------+
| 178811 | 210.1595 |
| 177646 | 217.0161 |
| 181997 | 218.5406 |
| 181605 | 219.2989 |
| 821938 | 221.7228 |
| 807785 | 226.7135 |
| 716433 | 227.3148 |
| 358802 | 230.7314 |
| 803100 | 230.9112 |
| 866737 | 231.6441 |
+--------+----------+
10 rows in set (0.02 sec)
To compare with the exact results from a brute-force calculation, use l2_distance or inner_product (without the _approximate suffix). In this example, the exact search takes about 290 ms:
10 rows in set (0.29 sec)
Using the ANN index reduces query latency from about 290 ms to about 20 ms.
The ANN index is built at the segment level. In a distributed table, each segment returns its local top-N results. The top-N operator then merges these results across tablets and segments to produce the global top-N.
Sorting rules:
-
For
metric_type = l2_distance, a smaller distance means the vectors are closer. UseORDER BY dist ASC. -
For
metric_type = inner_product, a larger value means the vectors are closer. UseORDER BY dist DESCto get the top-N results using the index.
Approximate range search
Unlike top-N search, a range search returns all data points whose distance from the target vector meets a specified condition (greater or less than a threshold), rather than a fixed number of results. This is useful when you need candidates that are "similar enough" or "dissimilar enough"—for example, retrieving diverse content in recommendation systems or locating outliers in anomaly detection.
A typical SQL query looks like this:
SELECT count(*)
FROM sift_1M
WHERE l2_distance_approximate(
embedding,
[0,11,77,24,3,0,0,0,28,70,125,8,0,0,0,0,44,35,50,45,9,0,0,0,4,0,4,56,18,0,3,9,16,17,59,10,10,8,57,57,100,105,125,41,1,0,6,92,8,14,73,125,29,7,0,5,0,0,8,124,66,6,3,1,63,5,0,1,49,32,17,35,125,21,0,3,2,12,6,109,21,0,0,35,74,125,14,23,0,0,6,50,25,70,64,7,59,18,7,16,22,5,0,1,125,23,1,0,7,30,14,32,4,0,2,2,59,125,19,4,0,0,2,1,6,53,33,2])
> 300
--------------
+----------+
| count(*) |
+----------+
| 999271 |
+----------+
1 row in set (0.19 sec)
Range-based vector search in Doris is also accelerated by the ANN index. The index quickly filters candidate vectors before calculating approximate distances, reducing computational overhead. The supported range conditions include >, >=, <, <=.
Compound search
A compound search performs an ANN top-N search and a range condition filter in the same SQL query, returning the top-N results that also satisfy the range constraint.
SELECT id,
l2_distance_approximate(
embedding, [0,11,77,24,3,0,0,0,28,70,125,8,0,0,0,0,44,35,50,45,9,0,0,0,4,0,4,56,18,0,3,9,16,17,59,10,10,8,57,57,100,105,125,41,1,0,6,92,8,14,73,125,29,7,0,5,0,0,8,124,66,6,3,1,63,5,0,1,49,32,17,35,125,21,0,3,2,12,6,109,21,0,0,35,74,125,14,23,0,0,6,50,25,70,64,7,59,18,7,16,22,5,0,1,125,23,1,0,7,30,14,32,4,0,2,2,59,125,19,4,0,0,2,1,6,53,33,2]) as dist
FROM sift_1M
WHERE l2_distance_approximate(
embedding, [0,11,77,24,3,0,0,0,28,70,125,8,0,0,0,0,44,35,50,45,9,0,0,0,4,0,4,56,18,0,3,9,16,17,59,10,10,8,57,57,100,105,125,41,1,0,6,92,8,14,73,125,29,7,0,5,0,0,8,124,66,6,3,1,63,5,0,1,49,32,17,35,125,21,0,3,2,12,6,109,21,0,0,35,74,125,14,23,0,0,6,50,25,70,64,7,59,18,7,16,22,5,0,1,125,23,1,0,7,30,14,32,4,0,2,2,59,125,19,4,0,0,2,1,6,53,33,2])
> 300
ORDER BY dist limit 10
--------------
+--------+----------+
| id | dist |
+--------+----------+
| 243590 | 300.005 |
| 549298 | 300.0317 |
| 429685 | 300.0533 |
| 690172 | 300.0916 |
| 123410 | 300.1333 |
| 232540 | 300.1649 |
| 547696 | 300.2066 |
| 855437 | 300.2782 |
| 589017 | 300.3048 |
| 930696 | 300.3381 |
+--------+----------+
10 rows in set (0.12 sec)
A key factor in compound search is the execution order of the predicate filter and the top-N search. Pre-filtering applies the filter first and then finds the top-N results; post-filtering does the reverse. Post-filtering is typically faster but can reduce recall, so Doris adopts pre-filtering. The index can accelerate both stages, but using it for both may lower recall when the initial range filter is highly selective. Doris adaptively decides whether to use the index for both stages based on the predicate filtering rate and the index type.
Filtered ANN search
A filtered ANN search applies other filter predicates before performing the ANN top-N search, returning the top-N results that satisfy the specified conditions. The following example with 8-dimensional vectors illustrates this process.
CREATE TABLE ann_with_fulltext (
id int NOT NULL,
embedding array<float> NOT NULL,
comment String NOT NULL,
value int NULL,
INDEX idx_comment(`comment`) USING INVERTED PROPERTIES("parser" = "english") COMMENT 'inverted index for comment',
INDEX ann_embedding(`embedding`) USING ANN PROPERTIES("index_type"="hnsw","metric_type"="l2_distance","dim"="8")
) DUPLICATE KEY (`id`)
DISTRIBUTED BY HASH(`id`) BUCKETS 1
PROPERTIES("replication_num"="1");
INSERT INTO ann_with_fulltext VALUES
(1, [0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8], 'this is about music', 10),
(2, [0.2,0.1,0.5,0.3,0.9,0.4,0.7,0.1], 'sports news today', 20),
(3, [0.9,0.8,0.7,0.6,0.5,0.4,0.3,0.2], 'latest music trend', 30),
(4, [0.05,0.06,0.07,0.08,0.09,0.1,0.2,0.3], 'politics update',40)
Suppose a user provides the query vector [0.1,0.1,0.2,0.2,0.3,0.3,0.4,0.4] and wants to retrieve the top 2 most similar documents only from those whose comment field contains "music":
SELECT id, comment,
l2_distance_approximate(embedding, [0.1,0.1,0.2,0.2,0.3,0.3,0.4,0.4]) AS dist
FROM ann_with_fulltext
WHERE comment MATCH_ANY 'music' -- First, filter using the inverted index
ORDER BY dist ASC -- Then, perform an ANN TopN search on the filtered result set
LIMIT 2;
+------+---------------------+----------+
| id | comment | dist |
+------+---------------------+----------+
| 1 | this is about music | 0.663325 |
| 3 | latest music trend | 1.280625 |
+------+---------------------+----------+
2 rows in set (0.04 sec)
To accelerate a filtered ANN search with a vector index, ensure that the columns used in the filter predicates have a secondary index, such as an inverted index.
Query parameters
You can also adjust HNSW index behavior at query time through session variables.
-
hnsw_ef_search: Theef_searchparameter for the HNSW index. It controls the maximum length of the candidate queue during the search phase. A largeref_searchvalue improves search accuracy at the cost of higher latency. The default value is 32. -
hnsw_check_relative_distance: Whether to enable the relative distance check mechanism to improve HNSW search accuracy. The default istrue. -
hnsw_bounded_queue: Whether to use a bounded priority queue to optimize HNSW search performance. The default istrue.
Vector quantization
With FLAT encoding, the HNSW index (original vectors plus graph structure) can consume a large amount of memory. HNSW must be fully loaded into memory to function, which can become a bottleneck on very large datasets. Scalar quantization (SQ) reduces memory by compressing FLOAT32 values, while product quantization (PQ) decomposes high-dimensional vectors into sub-vectors and quantizes them separately. Doris supports two scalar quantization levels: INT8 and INT4 (SQ8 and SQ4). Here is an example using SQ8:
CREATE TABLE sift_1M (
id int NOT NULL,
embedding array<float> NOT NULL COMMENT "",
INDEX ann_index (embedding) USING ANN PROPERTIES(
"index_type"="hnsw",
"metric_type"="l2_distance",
"dim"="128",
"quantizer"="sq8" -- Use INT8 for quantization
)
) ENGINE=OLAP
DUPLICATE KEY(id) COMMENT "OLAP"
DISTRIBUTED BY HASH(id) BUCKETS 1
PROPERTIES (
"replication_num" = "1"
);
In tests with the 768-dimensional Cohere-MEDIUM-1M and Cohere-LARGE-10M datasets, SQ8 can compress the index size to about 1/3 of the size of a FLAT index.
|
Dataset |
Vector dimension |
Scheme |
Total disk usage |
Data part |
Index part |
Remarks |
|
Cohere-MEDIUM-1M |
768D |
Doris (FLAT) |
5.647 GB (2.533 + 3.114) |
2.533 GB |
3.114 GB |
1M vectors, original + HNSW FLAT index |
|
Cohere-MEDIUM-1M |
768D |
Doris SQ INT8 |
3.501 GB (2.533 + 0.992) |
2.533 GB |
0.992 GB |
INT8 symmetric quantization |
|
Cohere-MEDIUM-1M |
768D |
Doris PQ(pq_m=384,pq_nbits=8) |
3.149 GB (2.535 + 0.614) |
2.535 GB |
0.614 GB |
Product quantization |
|
Cohere-LARGE-10M |
768D |
Doris (FLAT) |
56.472 GB (25.328 + 31.145) |
25.328 GB |
31.145 GB |
10M vectors |
|
Cohere-LARGE-10M |
768D |
Doris SQ INT8 |
35.016 GB (25.329 + 9.687) |
25.329 GB |
9.687 GB |
INT8 quantization, index significantly smaller |
Quantization adds extra overhead to the build process because the build phase requires many distance calculations, each of which must decode the quantized values. For a 128-dimensional vector, the build time increases with the number of rows. SQ can introduce approximately 10 times the build cost compared to FLAT.
Doris also supports product quantization, which requires additional parameters:
-
pq_m: Specifies the number of sub-vectors to split the original high-dimensional vector into. The vector dimension (dim) must be divisible bypq_m. -
pq_nbits: Specifies the number of bits to quantize each sub-vector. This determines the size of the codebook for each subspace (k = 2 ^ pq_nbits). In Faiss, the value ofpq_nbitsshould generally be no more than 24.
Product quantization requires a minimum amount of training data. The number of training points must be greater than or equal to the number of cluster centers (n >= 2 ^ pq_nbits).
CREATE TABLE sift_1M (
id int NOT NULL,
embedding array<float> NOT NULL COMMENT "",
INDEX ann_index (embedding) USING ANN PROPERTIES(
"index_type"="hnsw",
"metric_type"="l2_distance",
"dim"="128",
"quantizer"="pq", -- Use PQ for quantization
"pq_m"="2", -- Required for PQ, splits the high-dim vector into pq_m low-dim sub-vectors
"pq_nbits"="2" -- Required for PQ, specifies the number of bits for each subspace codebook
)
) ENGINE=OLAP
DUPLICATE KEY(id) COMMENT "OLAP"
DISTRIBUTED BY HASH(id) BUCKETS 1
PROPERTIES (
"replication_num" = "1"
);

Performance tuning
Vector search is a typical point query scenario that uses a secondary index. If your application requires high QPS and low latency, follow these recommendations. After tuning, on a machine with a 32-core 64 GB FE and a 32-core 64 GB BE, Doris can achieve over 3,000 QPS on the Cohere-MEDIUM-1M dataset.
Query performance
|
Concurrency |
Scheme |
QPS |
Average latency (s) |
P99 latency (s) |
CPU usage |
Recall rate |
|
240 |
Doris |
3340.4399 |
0.071368168 |
0.163399825 |
40% |
91.00% |
|
240 |
Doris SQ INT8 |
3188.6359 |
0.074728852 |
0.160370195 |
40% |
88.26% |
|
240 |
Doris SQ INT4 |
2818.2291 |
0.084663868 |
0.174826815 |
43% |
80.38% |
|
240 |
Doris brute-force calculation |
3.6787 |
25.554878826 |
29.363227973 |
100% |
100.00% |
|
480 |
Doris |
4155.7220 |
0.113387271 |
0.261086075 |
60% |
91.00% |
|
480 |
Doris SQ INT8 |
3833.1130 |
0.123040214 |
0.276912867 |
50% |
88.26% |
|
480 |
Doris SQ INT4 |
3431.0538 |
0.137636995 |
0.281631249 |
57% |
80.38% |
|
480 |
Doris brute-force calculation |
3.6787 |
25.554878826 |
29.363227973 |
100% |
100.00% |
Use prepared statements
Embedding models often output vectors with 768 or more dimensions. Embedding such a large vector literal directly in SQL can cause parsing time to exceed execution time, so use a prepared statement instead. Doris does not support executing these commands through the MySQL client; you must use JDBC.
1. Enable server-side prepared statements in the JDBC URL.
url = jdbc:mysql://127.0.0.1:9030/demo?useServerPrepStmts=true
2. Use a prepared statement.
// Use '?' as a placeholder. The readStatement should be reused.
PreparedStatement readStatement = conn.prepareStatement("SELECT id, l2_distance_approximate(embedding, cast (? as ARRAY<FLOAT>)) AS distance
FROM l2_distance_approximate
ORDER BY distance
LIMIT 10");
...
readStatement.setString("[0,11,77,24,3,0,0,0,28,70,125,8,0,0,0,0,44,35,50,45,9,0,0,0,4,0,4,56,18,0,3,9,16,17,59,10,10,8,57,57,100,105,125,41,1,0,6,92,8,14,73,125,29,7,0,5,0,0,8,124,66,6,3,1,63,5,0,1,49,32,17,35,125,21,0,3,2,12,6,109,21,0,0,35,74,125,14,23,0,0,6,50,25,70,64,7,59,18,7,16,22,5,0,1,125,23,1,0,7,30,14,32,4,0,2,2,59,125,19,4,0,0,2,1,6,53,33,2]");
ResultSet resultSet = readStatement.executeQuery();
Reduce segment count
The Doris ANN index is built on segments. Too many segments can introduce overhead. Ideally, a table with an ANN index should have no more than five segments per tablet. You can increase the size of individual segments to reduce their number by adjusting write_buffer_size and vertical_compaction_max_segment_size in be.conf. We recommend setting both to 10737418240 (10 GB).
Reduce rowset count
Reducing the number of rowsets, like reducing segments, lowers scheduling overhead. Each import generates a new rowset, so we recommend using stream load or INSERT INTO SELECT for bulk imports.
Keep the ANN index in memory
The current ANN index algorithm is memory-based. If the index of a queried segment is not in memory, it will trigger disk I/O. For optimal performance, we recommend keeping the index in memory. You can do this by setting enable_segment_cache_prune=false in be.conf.
parallel_pipeine_task_num = 1
ANN top-N queries return a small number of rows and do not require high parallelism. We recommend setting SET parallel_pipeline_task_num = 1.
enable_profile = false
For extremely latency-sensitive applications, disable the query profile by setting enable_profile=false.
Python SDK
Community members have contributed a Python SDK that makes it easy to use Doris vector search capabilities in Python.
-
https://github.com/uchenily/doris_vector_search: This SDK is optimized for vector distance retrieval and is currently the best-performing Python SDK for Doris vector search available.
Limitations
-
Doris requires the column corresponding to the ANN index to be a
NOT NULLABLEcolumn of typeArray<Float>. During data import, you must ensure that the length of every vector in this column equals the dimension (dim) specified in the index properties; otherwise, Doris reports an error. -
The ANN index can only be used on tables with the DuplicateKey model.
-
Doris uses pre-filtering semantics, evaluating predicates before the ANN top-N calculation. To ensure correct results, if a predicate in the SQL query involves a column that does not have a secondary index, Doris falls back to brute-force calculation. For example:
SELECT id, l2_distance_approximate(embedding, [xxx]) AS distance
FROM sift_1M
WHERE round(id) > 100
ORDER BY distance limit 10;
Although id is a primary key, this query will fall back to a brute-force calculation. This is because the round(id) > 100 predicate applies to a column that lacks a secondary index (like an inverted index) capable of precisely locating rows. To maintain the pre-filtering semantics for the ANN top-N search, the system defaults to a full scan.
-
If the distance function specified in the SQL query does not match the
metric_typeof the index in the DDL, Doris cannot use the ANN index to accelerate the top-N calculation, even if you usel2_distance_approximateorinner_product_approximate. -
If the
metric_typeisinner_product, only a query withORDER BY inner_product_approximate() DESC LIMIT N(theDESCkeyword is mandatory) can be accelerated by the ANN index. -
The
xxx_approximate()function triggers an index scan only when its first argument is a column array and its second argument is aCASTor an array literal. If you swap their positions, the query falls back to a brute-force search.