FAQ
This page collects common questions that VSAG users hit while choosing indexes, tuning performance, and integrating APIs. Follow the linked pages for details.
Which index should I choose?
Common VSAG indexes target different workloads. Choose by data type, scale, and recall / latency target.
hgraph is the default choice for dense vectors. It fits text, image, and
multimodal embeddings in online search systems that need high recall and low
latency. It supports multiple quantizers, incremental insertion, deletion,
reranking, and automatic tuning.
ivf fits large-scale or high-throughput workloads where memory is tight and
queries can tolerate bucket-based recall tradeoffs. It reduces scanning by
partitioning vectors into buckets.
sindi is for sparse vector retrieval, such as BM25, SPLADE, or BGE-M3 sparse
outputs. It only accepts dtype: "sparse" and primarily uses
metric_type: "ip".
pyramid fits multi-tenant, partitioned, or tag-path workloads. It keeps
multiple subgraphs inside one index and supports tag / path based retrieval.
brute_force is for small datasets, functional validation, and exact-recall
baselines. It is exact, but latency and throughput usually do not scale to large
datasets.
Practical guidance:
- If you are unsure and your vectors are dense, start with
hgraph. - Use
sindifor sparse vectors. - Use
brute_forcefor small datasets or recall baselines. - Compare
ivfwhen throughput and memory matter more than single-query latency. - Consider
pyramidwhen your data has clear partition, tenant, or path structure.
Related pages: Index Overview, Best Practices.
Why does the same parameter set perform very differently on different datasets?
This is common in vector search. Even if the vector count, dimensionality, and index parameters are identical, different datasets can have very different search difficulty.
The root cause is data distribution:
- Some datasets have clear neighbor structure, so graph search reaches the right region quickly.
- Some datasets have ambiguous neighbor boundaries, so search must expand more candidates for the same recall.
- Embedding normalization, clustering, per-dimension distribution, and noise all affect search difficulty.
For HGraph-like graph indexes, ef_search is a key search-time parameter for
recall and latency. It controls how many candidates the search keeps and
expands:
- Larger
ef_searchusually improves recall. - Larger
ef_searchusually increases per-query latency. - When other factors are similar, query latency is often approximately linear in
ef_search.
Therefore, do not compare datasets only by QPS at the same ef_search. A more
meaningful process is:
- Tune
ef_searchseparately on each dataset. - Make each dataset reach the same target recall, such as 95% or 98% recall.
- Compare P50 / P95 / P99 latency and QPS at that target recall.
If dataset A reaches 95% recall with ef_search = 80, while dataset B needs
ef_search = 300, B being much slower is expected. It means B is harder to
search; it does not necessarily mean the index degraded.
When reporting performance, record:
- Dataset name and scale.
- Dimensionality.
- Index parameters.
- Target recall and actual recall.
ef_search.- QPS.
- P50 / P95 / P99 latency.
Related pages: HGraph, Evaluation Tool.
Why is sq8_uniform usually faster than sq8? When should I enable use_reorder?
Both sq8 and sq8_uniform are 8-bit scalar quantizers, but they use different
scaling strategies.
sq8 is per-dimension quantization:
- Each dimension has its own
min_i/max_i/scale_i. - This adapts better to each dimension’s value range.
- Distance computation has to handle per-dimension scales, so the hot path is more complex.
sq8_uniform is global uniform quantization:
- All dimensions share one
min/max/scale. - Query and base codes can more easily be computed directly in the integer domain.
- SIMD, AVX-512, AMX, and NEON paths are more efficient.
- Distance computation can avoid per-element dequantization and per-dimension scale handling.
When the data distribution fits this assumption, sq8_uniform is often faster
than sq8.
Good use cases for sq8_uniform:
- Normalized vectors, especially
cosineworkloads. - Dimensions have similar value ranges.
- Distance computation is the query bottleneck.
- Throughput and latency matter more than the last bit of recall.
- You can use
use_reorderto fix coarse-ranking errors.
Less suitable cases:
- Different dimensions have very different value ranges.
- Vectors concatenate heterogeneous feature blocks.
- Some dimensions have heavy tails or strong outliers.
- You do not plan to enable reorder and are very sensitive to recall.
use_reorder first uses the compressed base quantizer for coarse ranking, then
reranks candidates with a higher-precision precise quantizer.
Common configuration:
{
"index_param": {
"base_quantization_type": "sq8_uniform",
"use_reorder": true,
"precise_quantization_type": "fp32"
}
}
Enable use_reorder when:
- You use lossy quantizers such as
sq4,sq4_uniform,pq,pqfs, orrabitq. - Recall is not stable enough with
sq8orsq8_uniform. topkis small but final ranking quality is important.- You can afford an additional higher-precision representation.
- Production recall stability matters more than maximum compression.
You can skip use_reorder when:
fp32orfp16already meets recall.sq8_uniformreaches recall targets without reorder.- Memory budget is very tight.
- Latency is extremely sensitive and reranking overhead is unacceptable.
Simple guidance:
- Throughput first: try
sq8_uniformwithout reorder and measure recall. - Safer default:
sq8_uniform+use_reorder: true+precise_quantization_type: "fp32". - Strong compression:
sq4_uniform/pq/rabitqusually need reorder.
Related pages: Scalar Uniform Quantization, Scalar Quantization.
What are the distance semantics of l2, ip, and cosine?
VSAG search results are always sorted by smaller distance first. Even when the underlying metric is inner product or cosine similarity, VSAG converts the score into distance semantics.
Specific semantics:
l2returnsL2Sqr, the squared L2 distance.ipreturns1 - inner_product.cosinereturns1 - cosine_similarity.
Why does l2 return squared distance? Squared L2 distance has the same ordering
as L2 distance, and avoiding the square root improves performance. VSAG
therefore commonly uses L2Sqr internally and in returned distances.
This affects RangeSearch radius settings:
- If you want L2 distance smaller than
2.0, pass radius4.0. - For
ip, radius means1 - inner_product. - For
cosine, radius means1 - cosine_similarity.
For example, if you want cosine similarity at least 0.8:
distance = 1 - cosine_similarity
radius = 1 - 0.8 = 0.2
Notes:
- Different systems may return similarity or distance.
- Before comparing with another library or ground truth, confirm the scoring semantics.
- After an index is created,
metric_typecannot be changed at search time.
Related pages: Metric Semantics, Range Search.
What is the difference between base_quantization_type and precise_quantization_type?
These two parameters control coarse storage and rerank storage.
base_quantization_type is the main storage quantizer:
- It stores the main vectors inside the index.
- It is used for coarse distance computation during graph search or inverted-list scanning.
- It directly affects memory usage, search speed, and coarse-ranking recall.
- Common values include
fp32,fp16,bf16,sq8,sq8_uniform, andpq.
precise_quantization_type is the higher-precision quantizer for reranking:
- It only takes effect when
use_reorder: true. - It reranks coarse candidates.
- It corrects distance errors introduced by lossy quantization.
- The common choice is
fp32; depending on memory budget,fp16,bf16, orsq8may also be used.
A useful mental model:
base_quantization_type = format used to quickly find candidates
precise_quantization_type = format used to recompute candidate distances
High-recall baseline:
{
"index_param": {
"base_quantization_type": "fp32"
}
}
Memory / recall tradeoff:
{
"index_param": {
"base_quantization_type": "sq8_uniform",
"use_reorder": true,
"precise_quantization_type": "fp32"
}
}
Lower memory:
{
"index_param": {
"base_quantization_type": "sq4_uniform",
"use_reorder": true,
"precise_quantization_type": "fp32"
}
}
More aggressive compression:
{
"index_param": {
"base_quantization_type": "pq",
"use_reorder": true,
"precise_quantization_type": "fp32"
}
}
Setting guidance:
- Recall first, enough memory:
base_quantization_type: "fp32". - General production choice:
base_quantization_type: "sq8_uniform". - Data distribution does not fit uniform scaling: try
sq8. - Tight memory: try
sq4_uniform,pq, orrabitq, and enable reorder. - If
use_reorderis enabled, start withprecise_quantization_type: "fp32".
Note that dtype is the input data type, while base_quantization_type is the
internal storage / computation format. They are not the same. For example, input
can be dtype: "float32" while the index stores vectors with
base_quantization_type: "sq8_uniform".
Related pages: Quantization Overview, HGraph, IVF.
Should I use Bitset, lambda, Filter, attribute filtering, or extra_info?
VSAG provides multiple filtering APIs for different workloads.
Bitset filtering fits a known set of ids to exclude, such as tombstones,
blacklists, or permission-denied ids. Bitset::Test(id) == true means the id is
filtered out.
Lambda or std::function<bool(int64_t)> fits simple filtering logic. Returning
true means the id is filtered out.
A Filter object fits more complex filtering logic, or cases where you can
provide hints such as ValidRatio(). Filter::CheckValid(id) == true means the
id is kept.
Attribute filtering fits structured predicates, such as
category = "book" AND price <= 100. It is used through SearchRequest and fits
vector + structured field hybrid search.
extra_info filtering fits fixed-size byte payloads stored beside each vector.
HGraph can filter on those bytes during graph traversal.
Filter::CheckValid(const char*) == true means the vector is kept.
How to choose:
- Use Bitset if you only need to exclude a known id set.
- Use lambda for simple ad hoc logic.
- Use a
Filterobject for complex logic and when you can estimate pass ratio. - Use attribute filtering for named, typed structured fields.
- Use
extra_infowhen metadata is a fixed-size byte payload stored with vectors.
The most confusing part is true / false semantics:
Bitset::Test(id)returnstrueto filter out this id.- A lambda returns
trueto filter out this id. Filter::CheckValid(id)returnstrueto keep this id.Filter::CheckValid(const char*)returnstrueto keep the vector.
When using bitset filtering, keep ids in [0, 2^32) when possible to avoid
low-32-bit collisions. If the predicate is very selective, graph search may need
to expand more candidates to collect enough valid results. For HGraph, consider
brute_force_threshold so highly selective filters can automatically fall back
to brute-force scanning.
Related pages: Filtered Search, Attribute Filter, Extra Info.