Getting started with C++
The open-source SVS library supports all documented features except our proprietary vector compression (LVQ and Leanvec), which are exclusive to Intel hardware and available via our shared library and PyPI package. This tutorial shows how to use the shared library to enable our proprietary vector compression and unlock significant performance and memory gains!
We also provide an example using the open-source SVS library only.
Building
Building the SVS example should be relatively straight-forward. We test on Ubuntu 22.04 LTS, but any Linux distribution should work.
Prerequisites
A C++20 capable compiler:
GCC >= 11.0
Clang >= 18.0 (note that the shared library requires a higher Clang version than the open-source library)
CMake build
To build and run the SVS example using our shared library run the following commands:
git clone https://github.com/intel/ScalableVectorSearch
cd ScalableVectorSearch/examples/cpp/shared
mkdir build && cd build
cmake ..
make -j
./example_vamana_with_compression
See CMakeLists.txt for details on how the shared library is used and remember to update the link in CMakeLists.txt to download the latest shared library release.
Step by step example using vector compression
Here is a step by step explanation of the example that showcases the most important features of SVS.
We will use the random dataset included in SVS for testing in data/test_dataset
.
Compress the data
To boost performance and reduce memory usage, we first compress the data using our vector compression technique LVQ. See How to Choose Compression Parameters for details.
const size_t num_threads = 4;
size_t padding = 32;
size_t leanvec_dim = 64;
auto threadpool = svs::threads::as_threadpool(num_threads);
auto loaded = svs::VectorDataLoader<float>(std::filesystem::path(SVS_DATA_DIR) / "data_f32.svs").load();
auto data = svs::leanvec::LeanDataset<svs::leanvec::UsingLVQ<4>, svs::leanvec::UsingLVQ<8>, svs::Dynamic, svs::Dynamic>::reduce(
loaded, std::nullopt, threadpool, padding, svs::lib::MaybeStatic<svs::Dynamic>(leanvec_dim)
Building the index
To search effectively, first build a graph-based index linking related data vectors. We’ll keep defaults for hyperparameters, exact values can be tuned later based on the dataset.
auto parameters = svs::index::vamana::VamanaBuildParameters{};
svs::Vamana index = svs::Vamana::build<float>(parameters, data, svs::distance::DistanceL2(), num_threads);
Searching the index
The graph is built; we can now query it. Load queries from disk and set search_window_size
–larger values boost accuracy but reduce speed (see How to Set the Search Window Size).
const size_t search_window_size = 50;
const size_t n_neighbors = 10;
index.set_search_window_size(search_window_size);
auto queries = svs::load_data<float>(std::filesystem::path(SVS_DATA_DIR) / "queries_f32.fvecs");
auto results = index.search(queries, n_neighbors);
After searching, we compare the results with the ground-truth and print the obtained recall.
auto groundtruth = svs::load_data<int>(std::filesystem::path(SVS_DATA_DIR) / "groundtruth_euclidean.ivecs");
double recall = svs::k_recall_at_n(groundtruth, results, n_neighbors, n_neighbors);
fmt::print("Recall@{} = {:.4f}\n", n_neighbors, recall);
Saving and loading the index
If you are satisfied with the performance of the generated index, you can save it to disk to avoid rebuilding it in the future.
index.save("config", "graph", "data");
index = svs::Vamana::assemble<float>(
"config", svs::GraphLoader("graph"), svs::lib::load_from_disk<svs::leanvec::LeanDataset<svs::leanvec::UsingLVQ<4>, svs::leanvec::UsingLVQ<8>, svs::Dynamic, svs::Dynamic>>("data", padding), svs::distance::DistanceL2(), num_threads
);
Note
The save index function currently uses three folders for saving. All three are needed to be able to reload the index.
One folder for the graph.
One folder for the data.
One folder for metadata.
This is subject to change in the future.
Entire example
/*
* Copyright 2025 Intel Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// SVS
#include "svs/core/recall.h"
#include "svs/extensions/flat/leanvec.h"
#include "svs/extensions/flat/lvq.h"
#include "svs/extensions/vamana/leanvec.h"
#include "svs/extensions/vamana/lvq.h"
#include "svs/orchestrators/dynamic_vamana.h"
#include "svs/orchestrators/exhaustive.h"
#include "svs/orchestrators/vamana.h"
int main() {
// STEP 1: Compress Data with LeanVec, reducing dimensionality to leanvec_dim dimensions and using
// 4 and 8 bits for primary and secondary levels respectively.
//! [Compress data]
const size_t num_threads = 4;
size_t padding = 32;
size_t leanvec_dim = 64;
auto threadpool = svs::threads::as_threadpool(num_threads);
auto loaded = svs::VectorDataLoader<float>(std::filesystem::path(SVS_DATA_DIR) / "data_f32.svs").load();
auto data = svs::leanvec::LeanDataset<svs::leanvec::UsingLVQ<4>, svs::leanvec::UsingLVQ<8>, svs::Dynamic, svs::Dynamic>::reduce(
loaded, std::nullopt, threadpool, padding, svs::lib::MaybeStatic<svs::Dynamic>(leanvec_dim)
);
//! [Compress data]
// STEP 2: Build Vamana Index
//! [Index Build]
auto parameters = svs::index::vamana::VamanaBuildParameters{};
svs::Vamana index = svs::Vamana::build<float>(parameters, data, svs::distance::DistanceL2(), num_threads);
//! [Index Build]
// STEP 3: Search the Index
//! [Perform Queries]
const size_t search_window_size = 50;
const size_t n_neighbors = 10;
index.set_search_window_size(search_window_size);
auto queries = svs::load_data<float>(std::filesystem::path(SVS_DATA_DIR) / "queries_f32.fvecs");
auto results = index.search(queries, n_neighbors);
//! [Perform Queries]
//! [Recall]
auto groundtruth = svs::load_data<int>(std::filesystem::path(SVS_DATA_DIR) / "groundtruth_euclidean.ivecs");
double recall = svs::k_recall_at_n(groundtruth, results, n_neighbors, n_neighbors);
fmt::print("Recall@{} = {:.4f}\n", n_neighbors, recall);
//! [Recall]
// STEP 4: Saving and reloading the index
//! [Saving Loading]
index.save("config", "graph", "data");
index = svs::Vamana::assemble<float>(
"config", svs::GraphLoader("graph"), svs::lib::load_from_disk<svs::leanvec::LeanDataset<svs::leanvec::UsingLVQ<4>, svs::leanvec::UsingLVQ<8>, svs::Dynamic, svs::Dynamic>>("data", padding), svs::distance::DistanceL2(), num_threads
);
//! [Saving Loading]
index.set_search_window_size(search_window_size);
recall = svs::k_recall_at_n(groundtruth, results, n_neighbors, n_neighbors);
fmt::print("Recall@{} after saving and reloading = {:.4f}\n", n_neighbors, recall);
return 0;
}
Using open-source SVS only
Building and installing
Prerequisites
A C++20 capable compiler:
GCC >= 11.0
Clang >= 13.0
To build SVS and the included examples, use the following:
git clone https://github.com/intel/ScalableVectorSearch
cd ScalableVectorSearch
mkdir build && cd build
cmake .. -DSVS_BUILD_EXAMPLES=YES
cmake --build . -j$(nproc)
Run this command to confirm SVS is installed correctly, it should print some types, like float32
.
examples/cpp/types
Run this command to execute the example
examples/cpp/vamana ../data/test_dataset/data_f32.fvecs ../data/test_dataset/queries_f32.fvecs ../data/test_dataset/groundtruth_euclidean.ivecs
Entire example
/*
* Copyright 2023 Intel Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//! [Example All]
//! [Includes]
// SVS Dependencies
#include "svs/orchestrators/vamana.h" // bulk of the dependencies required.
#include "svs/core/recall.h" // Convenient k-recall@n computation.
#include "svs/extensions/vamana/scalar.h" // SQ vamana extensions.
#include "svs/quantization/scalar/scalar.h" // SQ implementation.
// Alternative main definition
#include "svsmain.h"
// stl
#include <map>
#include <string>
#include <string_view>
#include <vector>
//! [Includes]
//! [Helper Utilities]
double run_recall(
svs::Vamana& index,
const svs::data::SimpleData<float>& queries,
const svs::data::SimpleData<uint32_t>& groundtruth,
size_t search_window_size,
size_t num_neighbors,
std::string_view message = ""
) {
index.set_search_window_size(search_window_size);
auto results = index.search(queries, num_neighbors);
double recall = svs::k_recall_at_n(groundtruth, results, num_neighbors, num_neighbors);
if (!message.empty()) {
fmt::print("[{}] ", message);
}
fmt::print("Windowsize = {}, Recall = {}\n", search_window_size, recall);
return recall;
}
const bool DEBUG = false;
void check(double expected, double got, double eps = 0.005) {
double diff = std::abs(expected - got);
if constexpr (DEBUG) {
fmt::print("Expected {}. Got {}\n", expected, got);
} else {
if (diff > eps) {
throw ANNEXCEPTION("Expected ", expected, ". Got ", got, '!');
}
}
}
//! [Helper Utilities]
// Alternative main definition
int svs_main(std::vector<std::string> args) {
//! [Argument Extraction]
const size_t nargs = args.size();
if (nargs != 4) {
throw ANNEXCEPTION("Expected 3 arguments. Instead, got ", nargs, '!');
}
const std::string& data_vecs = args.at(1);
const std::string& query_vecs = args.at(2);
const std::string& groundtruth_vecs = args.at(3);
//! [Argument Extraction]
// Building the index
//! [Build Parameters]
auto parameters = svs::index::vamana::VamanaBuildParameters{
1.2, // alpha
64, // graph max degree
128, // search window size
1024, // max candidate pool size
60, // prune to degree
true, // full search history
};
//! [Build Parameters]
//! [Index Build]
size_t num_threads = 4;
svs::Vamana index = svs::Vamana::build<float>(
parameters, svs::VectorDataLoader<float>(data_vecs), svs::DistanceL2(), num_threads
);
//! [Index Build]
// Searching the index
//! [Load Aux]
// Load the queries and ground truth.
auto queries = svs::load_data<float>(query_vecs);
auto groundtruth = svs::load_data<uint32_t>(groundtruth_vecs);
//! [Load Aux]
//! [Perform Queries]
index.set_search_window_size(30);
svs::QueryResult<size_t> results = index.search(queries, 10);
double recall = svs::k_recall_at_n(groundtruth, results);
check(0.8215, recall);
//! [Perform Queries]
//! [Search Window Size]
auto expected_recall =
std::map<size_t, double>({{10, 0.5509}, {20, 0.7281}, {30, 0.8215}, {40, 0.8788}});
for (auto windowsize : {10, 20, 30, 40}) {
recall = run_recall(index, queries, groundtruth, windowsize, 10, "Sweep");
check(expected_recall.at(windowsize), recall);
}
//! [Search Window Size]
// Saving the index
//! [Saving]
index.save("example_config", "example_graph", "example_data");
//! [Saving]
// Reloading a saved index
//! [Loading]
// We can reload an index from a previously saved set of files.
index = svs::Vamana::assemble<float>(
"example_config",
svs::GraphLoader("example_graph"),
svs::VectorDataLoader<float>("example_data"),
svs::DistanceType::L2,
4 // num_threads
);
recall = run_recall(index, queries, groundtruth, 30, 10, "Reload");
check(0.8215, recall);
//! [Loading]
//! [Only Loading]
// We can reload an index from a previously saved set of files.
index = svs::Vamana::assemble<float>(
"example_config",
svs::GraphLoader("example_graph"),
svs::VectorDataLoader<float>("example_data"),
svs::DistanceType::L2,
4 // num_threads
);
//! [Only Loading]
//! [Set a new thread pool with n-threads]
index.set_threadpool(svs::threads::DefaultThreadPool(4));
//! [Set a new thread pool with n-threads]
//! [Compressed Loader]
// Quantization
namespace scalar = svs::quantization::scalar;
// Wrap the compressor object in a lazy functor.
// This will defer loading and compression of the SQ dataset until the threadpool
// used in the index has been created.
auto compressor = svs::lib::Lazy([=](svs::threads::ThreadPool auto& threadpool) {
auto data = svs::VectorDataLoader<float, 128>("example_data").load();
return scalar::SQDataset<std::int8_t, 128>::compress(data, threadpool);
});
index = svs::Vamana::assemble<float>(
"example_config",
svs::GraphLoader("example_graph"),
compressor,
svs::DistanceType::L2,
4
);
recall = run_recall(index, queries, groundtruth, 30, 10, "Compressed load");
check(0.8190, recall);
//! [Compressed Loader]
//! [Build Index Compressed]
// Compressed building
index =
svs::Vamana::build<float>(parameters, compressor, svs::DistanceL2(), num_threads);
recall = run_recall(index, queries, groundtruth, 30, 10, "Compressed Build");
check(0.8212, recall);
//! [Build Index Compressed]
return 0;
}
// Special main providing some helpful utilities.
SVS_DEFINE_MAIN();
//! [Example All]