ApproximateRadiusSearch provides HNSW-based vector similarity search for finding all vectors within a specified distance threshold. This document serves as the complete reference for implementation, API usage, testing, and deployment.
Status: Production-Ready Beta (5/5 APIs functional)
Version: 1.5.0-beta
Last Updated: April 2026
All core methods are fully implemented and tested:
| API Method | Status | Description |
|---|---|---|
search() |
✅ COMPLETE | Core HNSW-based radius search |
searchById() |
✅ COMPLETE | ID-based vector lookup with caching |
batchSearch() |
✅ COMPLETE | Multi-query batch processing |
searchWithTargetCount() |
✅ COMPLETE | Adaptive radius targeting with binary search |
estimateResultCount() |
✅ COMPLETE | Sample-based result estimation |
Uses HNSW (Hierarchical Navigable Small World) graphs via VectorIndexManager:
- Performs approximate KNN search with large k
- Filters results by distance threshold
- Returns all vectors within specified radius
Performance: O(log N) average case, 93-99% recall
- L2 (Euclidean):
Metric::L2- Computer vision, spatial data - Cosine Similarity:
Metric::COSINE- Text embeddings, semantic search - Dot Product:
Metric::DOT_PRODUCT- Learned embeddings
#include "index/approximate_radius_search.h"
#include "index/vector_index.h"
#include "storage/rocksdb_wrapper.h"
// Initialize
themis::RocksDBWrapper db;
db.init("/data/vectors");
themis::VectorIndexManager vim(db);
vim.init("embeddings", 128, VectorIndexManager::Metric::COSINE);
// Add vectors
themis::BaseEntity doc("doc1");
std::vector<float> embedding(128, 0.5f);
doc.setField("embedding", embedding);
vim.addEntity(doc);
// Search
themis::vector::ApproximateRadiusSearch searcher(vim);
ApproximateRadiusSearch::SearchConfig config;
config.radius = 0.5f;
config.max_results = 100;
auto result = searcher.search(query_vector, config);Finds all vectors within distance threshold.
Result<SearchResult> search(
const std::vector<float>& query_vector,
const SearchConfig& config
);Returns: SearchResult with matches, timing, metadata
Search using stored vector's ID (with cache optimization).
Result<SearchResult> searchById(
std::string_view query_id,
const SearchConfig& config
);Performance: Adds ~0.5-2ms for ID lookup
Process multiple queries efficiently.
Result<std::vector<SearchResult>> batchSearch(
const std::vector<std::vector<float>>& query_vectors,
const SearchConfig& config
);Performance: 15-25% faster than sequential
Automatically adjusts radius for target result count.
Result<SearchResult> searchWithTargetCount(
const std::vector<float>& query_vector,
int target_count,
const SearchConfig& config
);Algorithm: Binary search (converges in 5-10 iterations)
Estimate results without full search.
Result<size_t> estimateResultCount(
const std::vector<float>& query_vector,
float radius,
Metric metric = Metric::COSINE
);Performance: 10-100x faster than actual search
struct SearchConfig {
Metric metric = Metric::COSINE; // Distance metric
float radius = 0.5f; // Distance threshold
int max_results = 1000; // Result limit
float min_recall = 0.95f; // Quality target
bool sort_results = true; // Sort by distance
int ef_search = 64; // HNSW parameter
};Small Dataset (< 10K vectors):
- M=8, efConstruction=100, efSearch=32
- max_results=500
Medium Dataset (10K-100K):
- M=16, efConstruction=200, efSearch=64
- max_results=1000
Large Dataset (> 100K):
- M=24, efConstruction=300, efSearch=128
- max_results=2000
| Dataset Size | Avg Latency | Throughput (QPS) |
|---|---|---|
| 1K vectors | ~0.5ms | ~2000 |
| 10K vectors | ~2-3ms | ~400-500 |
| 100K vectors | ~10-15ms | ~80-120 |
| 1M vectors | ~40-60ms | ~20-30 |
Configuration: M=16, efConstruction=200, efSearch=64
Memory (GB) = N × D × 4 bytes × (1 + M/8) / 1e9
Examples:
- 100K × 128-dim × M=16: ~110 MB
- 1M × 768-dim × M=16: ~4.2 GB
- Small datasets (< 10K): 98-99%
- Medium datasets (10K-100K): 95-97%
- Large datasets (> 100K): 93-96%
File: tests/test_vector_advanced_features.cpp
Covers basic functionality, error handling, statistics.
File: tests/test_approximate_radius_search_integration.cpp
Comprehensive testing:
- All distance metrics (L2, COSINE, DOT)
- Large dataset scaling (1000+ vectors)
- Batch vs sequential performance
- Adaptive target count accuracy
- Result estimation validation
- Error handling edge cases
- Statistics tracking
File: benchmarks/bench_approximate_radius_search.cpp
Measures:
- Dataset scalability (1K to 100K vectors)
- Radius parameter impact
- Batch throughput
- Adaptive search convergence
- Estimation speed
- ID-based lookup performance
- Metric comparison
auto result = searcher.search(query, config);
if (!result.has_value()) {
switch (result.error().code) {
case ErrorCode::INVALID_INPUT:
// Bad query vector or config
return http_response(400, "Invalid input");
case ErrorCode::NOT_FOUND:
// searchById with non-existent ID
return http_response(404, "Vector not found");
case ErrorCode::INTERNAL_ERROR:
// Retry once
result = searcher.search(query, config);
break;
}
}Track key metrics:
auto stats = searcher.getStatistics();
logger.gauge("radius_search.total_searches", stats.total_searches);
logger.gauge("radius_search.avg_results", stats.avg_results_per_search);
logger.gauge("radius_search.avg_latency_ms", stats.avg_time_ms);
// Alerts
if (stats.avg_time_ms > 100.0) {
alert("High latency detected");
}- Use batch processing for multiple queries (15-25% faster)
- Right-size max_results - don't set higher than needed
- Choose appropriate radius - smaller = faster
- Use estimateResultCount() for query planning
- Increase ef_search for better recall (trade: slower)
- Consider sharding for > 10M vectors
Fully integrated with existing infrastructure:
- Shares HNSW index, cache, and storage
- Consistent error handling
- Unified configuration
- No code duplication
// Core HNSW search
searchKnnRadius(query, epsilon, max_results, whitelistPks)
// Vector lookup (for searchById)
getVectorByPk(pk) // Cache-first, storage fallback
// Configuration
getDimension(), getMetric(), getVectorCount()// Find similar documents within threshold
config.radius = 0.3f;
config.metric = Metric::COSINE;
auto results = searcher.search(doc_embedding, config);// Find items similar to user's history
config.radius = 0.5f;
auto results = searcher.searchById(user_last_item, config);// Find near-duplicates
config.radius = 0.1f;
config.max_results = 10;
auto results = searcher.search(item_embedding, config);// Estimate cluster sizes
auto estimate = searcher.estimateResultCount(centroid, radius);
if (estimate.value() > threshold) {
// Cluster too large, split
}Solutions:
- Reduce ef_search (64 → 32)
- Lower max_results
- Use smaller radius
- Enable batch processing
Solutions:
- Increase ef_search (64 → 128 → 256)
- Rebuild index with higher M
- Verify metric matches data
Solutions:
- Reduce M parameter
- Shard dataset across nodes
- Use product quantization (advanced)
include/index/approximate_radius_search.h- API headersrc/index/approximate_radius_search.cpp- Implementationinclude/index/vector_index.h- VectorIndexManager (modified)src/index/vector_index.cpp- getVectorByPk() implementation
tests/test_vector_advanced_features.cpp- Unit teststests/test_approximate_radius_search_integration.cpp- Integration tests
benchmarks/bench_approximate_radius_search.cpp- Performance validation
examples/example_approximate_radius_search.cpp- Usage examples
- HNSW Paper: Malkov & Yashunin (2018), IEEE TPAMI
- hnswlib: https://github.com/nmslib/hnswlib
- License: Apache 2.0
- GitHub Issues: https://github.com/makr-code/ThemisDB/issues
- Example Code:
examples/example_approximate_radius_search.cpp - Unit Tests:
tests/test_vector_advanced_features.cpp