Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/VecSim/algorithms/hnsw/hnsw_multi.h
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ class HNSWIndex_Multi : public HNSWIndex<DataType, DistType> {
int addVector(const void *vector_data, labelType label) override;
vecsim_stl::vector<idType> markDelete(labelType label) override;
double getDistanceFrom_Unsafe(labelType label, const void *vector_data) const override {
if (this->isQuantized) {
auto processed_query = this->preprocessQuery(vector_data);
return getDistanceFromInternal(label, processed_query.get());
}
return getDistanceFromInternal(label, vector_data);
}
int removeLabel(labelType label) override { return labelLookup.erase(label); }
Expand Down
1 change: 1 addition & 0 deletions src/VecSim/algorithms/hnsw/hnsw_serializer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ HNSWSerializer::EncodingVersion HNSWSerializer::ReadVersion(std::ifstream &input
}

void HNSWSerializer::saveIndex(const std::string &location) {
validateSave();
EncodingVersion version = EncodingVersion::V4;
std::ofstream output(location, std::ios::binary);
writeBinaryPOD(output, version);
Expand Down
1 change: 1 addition & 0 deletions src/VecSim/algorithms/hnsw/hnsw_serializer.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,6 @@ class HNSWSerializer : public Serializer {
EncodingVersion m_version;

private:
virtual void validateSave() const = 0;
void saveIndexFields(std::ofstream &output) const = 0;
};
1 change: 1 addition & 0 deletions src/VecSim/algorithms/hnsw/hnsw_serializer_declarations.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ void restoreGraph(std::ifstream &input, HNSWSerializer::EncodingVersion version)

private:
// Functions for index saving.
void validateSave() const override;
void saveIndexFields(std::ofstream &output) const override;

void saveGraph(std::ofstream &output) const;
Expand Down
10 changes: 10 additions & 0 deletions src/VecSim/algorithms/hnsw/hnsw_serializer_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ HNSWIndex<DataType, DistType>::HNSWIndex(std::ifstream &input, const HNSWParams
graphDataBlocks.reserve(initial_vector_size);
}

template <typename DataType, typename DistType>
void HNSWIndex<DataType, DistType>::validateSave() const {
// V4 does not store quantization settings, and its loader always creates unquantized
// components. Reject the save rather than write a file the loader would misread.
if (this->isQuantized) {
throw std::runtime_error(
"Cannot save index: serialization of quantized indexes is not supported");
}
Comment thread
cursor[bot] marked this conversation as resolved.
}

template <typename DataType, typename DistType>
void HNSWIndex<DataType, DistType>::saveIndexIMP(std::ofstream &output) {
this->saveIndexFields(output);
Expand Down
4 changes: 4 additions & 0 deletions src/VecSim/algorithms/hnsw/hnsw_single.h
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ class HNSWIndex_Single : public HNSWIndex<DataType, DistType> {
int addVector(const void *vector_data, labelType label) override;
vecsim_stl::vector<idType> markDelete(labelType label) override;
double getDistanceFrom_Unsafe(labelType label, const void *vector_data) const override {
if (this->isQuantized) {
auto processed_query = this->preprocessQuery(vector_data);
return getDistanceFromInternal(label, processed_query.get());
}
return getDistanceFromInternal(label, vector_data);
}
int removeLabel(labelType label) override { return labelLookup.erase(label); }
Expand Down
168 changes: 164 additions & 4 deletions src/VecSim/index_factories/hnsw_factory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

using bfloat16 = vecsim_types::bfloat16;
using float16 = vecsim_types::float16;
using sq8 = vecsim_types::sq8;

namespace HNSWFactory {

Expand All @@ -34,11 +35,137 @@ NewIndex_ChooseMultiOrSingle(const HNSWParams *params,
HNSWIndex_Single<DataType, DistType>(params, abstractInitParams, components);
}

template <VecSimMetric Metric>
[[nodiscard]] constexpr size_t GetSQ8StoredDataSize(size_t dim, bool with_norm) {
static_assert(Metric == VecSimMetric_L2 || Metric == VecSimMetric_IP);

return with_norm ? sq8::storage_bytes_count<Metric, true>(dim)
: sq8::storage_bytes_count<Metric, false>(dim);
}

// Asymmetric dispatch reports alignment for the stored operand only. Ask the query type's
// dispatcher for the query allocation alignment.
template <typename DataType>
[[nodiscard]] unsigned char GetQueryAlignment(VecSimMetric metric, size_t dim) {
unsigned char alignment = 0;
spaces::GetDistFunc<DataType, float>(metric, dim, &alignment);
return alignment;
}

// Cosine over pre-normalized vectors is computed as inner product.
[[nodiscard]] constexpr VecSimMetric ResolveSQ8Metric(VecSimMetric metric, bool is_normalized) {
return (is_normalized && metric == VecSimMetric_Cosine) ? VecSimMetric_IP : metric;
}

// Keep construction and initial-size validation in sync.
[[nodiscard]] constexpr bool SQ8ParamsSupported(VecSimType type, VecSimMetric resolved_metric,
bool with_mean) {
// SQ8 kernels accept only FLOAT32 and FLOAT16 input.
if (type != VecSimType_FLOAT32 && type != VecSimType_FLOAT16) {
return false;
}
// Only L2 and inner product have SQ8 kernels.
if (resolved_metric != VecSimMetric_L2 && resolved_metric != VecSimMetric_IP) {
return false;
}
// Mean-centered L2 queries are narrowed back to FLOAT16 while the stored metadata remains
// FLOAT32. Support requires a kernel that keeps the centered query in FLOAT32.
if (type == VecSimType_FLOAT16 && with_mean && resolved_metric == VecSimMetric_L2) {
return false;
}
return true;
Comment thread
cursor[bot] marked this conversation as resolved.
}

template <typename DataType, VecSimMetric Metric>
VecSimIndex *NewIndex_SQ8(const HNSWParams *hnswParams, AbstractIndexInitParams abstractInitParams,
const float *mean_ptr) {
auto &allocator = abstractInitParams.allocator;
const size_t dim = abstractInitParams.dim;
const bool with_norm = mean_ptr != nullptr;
unsigned char storage_alignment = 0, asym_storage_alignment = 0;

abstractInitParams.storedDataSize = GetSQ8StoredDataSize<Metric>(dim, with_norm);
Comment thread
dor-forer marked this conversation as resolved.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: the SQ8 serialization guard is reached only after saveIndex has opened the destination with truncation and written the encoding version. A rejected SQ8 save can therefore destroy an existing snapshot. Please validate before opening the destination or save atomically, with a regression asserting that an existing destination remains unchanged.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. SQ8 serialization validation now runs before the destination is opened. The regression creates an existing destination and verifies its contents remain unchanged after the rejected save.

abstractInitParams.isQuantized = true;
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
dor-forer marked this conversation as resolved.

// Graph construction compares two stored SQ8 blobs; search compares a stored blob with a
// DataType query. Both dispatchers report alignment for the stored operand.
auto sym_func = spaces::GetDistFunc<sq8, float>(Metric, dim, &storage_alignment);
Comment thread
dor-forer marked this conversation as resolved.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking. Code flow: sym_func is selected here for stored-to-stored SQ8 comparisons and passed into DistanceCalculatorCommon or DistanceCalculatorWithNorm. During HNSW insertion and pruning, calcDistance invokes that function, which dispatches to SQ8_SQ8_InnerProductImp; it calls UINT8_InnerProductImp, accumulates in 32-bit SIMD lanes, and returns _mm512_reduce_add_epi32(sum) as a signed int. The SQ8 wrapper then converts that already-overflowed result to float and uses it in the dequantization formula, so HNSW can retain the wrong neighbors.

A valid self-dot can overflow at dimension 33,027: 33,026 * 255 * 255 = 2,147,515,650, above INT32_MAX. Please widen or safely chunk the accumulator, select a safe fallback, or reject SQ8 dimensions above a proven bound before wiring this kernel into graph construction.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already handled on the current head by a9911a9e (#1014), which is in this branch’s ancestry. IP_SQ8_SQ8_GetDistFunc, Cosine_SQ8_SQ8_GetDistFunc, and L2_SQ8_SQ8_GetDistFunc return their scalar kernels when dim > UINT8_MAX_EXACT_SIMD_DIM (33,025). Thus dimension 33,026 and above never wire the signed-32-bit SIMD reduction into sym_func. UINT8_DispatcherCapFallback explicitly asserts all three SQ8 dispatcher fallbacks above the bound.

auto asym_func =
spaces::GetDistFunc<sq8, float, DataType>(Metric, dim, &asym_storage_alignment);
Comment thread
dor-forer marked this conversation as resolved.
Comment thread
dor-forer marked this conversation as resolved.
storage_alignment = spaces::combineAlignments(storage_alignment, asym_storage_alignment);
const unsigned char query_alignment = GetQueryAlignment<DataType>(Metric, dim);

PreprocessorInterface *pp = nullptr;
IndexCalculatorInterface<float> *calc = nullptr;

if (with_norm) {
vecsim_stl::vector<float> mean_vec(allocator);
mean_vec.assign(mean_ptr, mean_ptr + dim);

float mean_sum_squares = 0.0f;
for (float v : mean_vec) {
mean_sum_squares += v * v;
}

pp = new (allocator) QuantPreprocessor<DataType, Metric, true>(allocator, dim, mean_vec);
Comment thread
dor-forer marked this conversation as resolved.
calc = new (allocator) DistanceCalculatorWithNorm<DataType, float, Metric>(
allocator, asym_func, sym_func, mean_sum_squares);
} else {
pp = new (allocator) QuantPreprocessor<DataType, Metric>(allocator, dim);
Comment thread
dor-forer marked this conversation as resolved.
calc = new (allocator) DistanceCalculatorCommon<float>(allocator, sym_func, asym_func);
}

auto *container = new (allocator)
MultiPreprocessorsContainer<DataType, 1>(allocator, query_alignment, storage_alignment);
[[maybe_unused]] const int ret = container->addPreprocessor(pp);
assert(ret != -1 && "SQ8 preprocessor was not added correctly");

IndexComponents<DataType, float> components{calc, container};
return NewIndex_ChooseMultiOrSingle<DataType, float>(hnswParams, abstractInitParams,
components);
}

VecSimIndex *NewIndex(const VecSimParams *params, bool is_normalized) {
const HNSWParams *hnswParams = &params->algoParams.hnswParams;
AbstractIndexInitParams abstractInitParams =
VecSimFactory::NewAbstractInitParams(hnswParams, params->logCtx, is_normalized);

if (hnswParams->quantType != VecSimQuant_NONE) {
// Reject unknown quantizers instead of silently creating an unquantized index.
if (hnswParams->quantType != VecSimQuant_SQ8) {
return NULL;
}

const VecSimMetric metric = ResolveSQ8Metric(hnswParams->metric, is_normalized);
const float *mean_ptr = static_cast<const float *>(hnswParams->quantParams);

if (!SQ8ParamsSupported(hnswParams->type, metric, mean_ptr != nullptr)) {
return NULL;
}

if (hnswParams->type == VecSimType_FLOAT32) {
if (metric == VecSimMetric_L2) {
return NewIndex_SQ8<float, VecSimMetric_L2>(hnswParams, abstractInitParams,
mean_ptr);
} else if (metric == VecSimMetric_IP) {
return NewIndex_SQ8<float, VecSimMetric_IP>(hnswParams, abstractInitParams,
mean_ptr);
Comment thread
dor-forer marked this conversation as resolved.
}
} else if (hnswParams->type == VecSimType_FLOAT16) {
if (metric == VecSimMetric_L2) {
return NewIndex_SQ8<float16, VecSimMetric_L2>(hnswParams, abstractInitParams,
mean_ptr);
} else if (metric == VecSimMetric_IP) {
return NewIndex_SQ8<float16, VecSimMetric_IP>(hnswParams, abstractInitParams,
mean_ptr);
}
}

// Keep the return for release builds so future dispatch changes cannot fall through.
assert(false && "unhandled SQ8 data type and metric combination");
return NULL;
Comment thread
dor-forer marked this conversation as resolved.
}
Comment thread
cursor[bot] marked this conversation as resolved.

Comment thread
cursor[bot] marked this conversation as resolved.
if (hnswParams->type == VecSimType_FLOAT32) {
IndexComponents<float, float> indexComponents = CreateIndexComponents<float, float>(
abstractInitParams.allocator, hnswParams->metric, hnswParams->dim, is_normalized);
Expand Down Expand Up @@ -94,7 +221,28 @@ size_t EstimateInitialSize(const HNSWParams *params, bool is_normalized) {
size_t allocations_overhead = VecSimAllocator::getAllocationOverheadSize();

size_t est = sizeof(VecSimAllocator) + allocations_overhead;
if (params->type == VecSimType_FLOAT32) {

if (params->quantType != VecSimQuant_NONE) {
// Keep construction and initial-size validation in sync.
if (params->quantType != VecSimQuant_SQ8 ||
!SQ8ParamsSupported(params->type, ResolveSQ8Metric(params->metric, is_normalized),
params->quantParams != nullptr)) {
throw std::invalid_argument("Unsupported quantization params for HNSW index");
Comment thread
dor-forer marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: this throw escapes through VecSimIndex_EstimateInitialSize, an extern C API with no catch or status channel. FP32 plus SQ8 plus Cosine therefore terminates a C caller instead of reporting failure. Please catch at the boundary and return a documented failure value, or add a status plus out-parameter API, with a C-facing regression.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I added a catch at the VecSimIndex_EstimateInitialSize C boundary so exceptions cannot escape to C callers. It now returns SIZE_MAX, matching the estimator’s existing failure convention, and the contract is documented in vec_sim.h. I also updated the unsupported SQ8 tests, including a direct public C API regression for FP32 + SQ8 + Cosine.

}
// Template arguments do not affect these component sizes.
if (params->quantParams) {
est += allocations_overhead +
sizeof(DistanceCalculatorWithNorm<float, float, VecSimMetric_L2>);
est += allocations_overhead + sizeof(MultiPreprocessorsContainer<float, 1>);
est += allocations_overhead + sizeof(QuantPreprocessor<float, VecSimMetric_L2, true>);
est += allocations_overhead + params->dim * sizeof(float);
} else {
est += allocations_overhead + sizeof(DistanceCalculatorCommon<float>);
est += allocations_overhead + sizeof(MultiPreprocessorsContainer<float, 1>);
est += allocations_overhead + sizeof(QuantPreprocessor<float, VecSimMetric_L2>);
}
est += EstimateInitialSize_ChooseMultiOrSingle<float>(params->multi);
Comment thread
cursor[bot] marked this conversation as resolved.
} else if (params->type == VecSimType_FLOAT32) {
est += EstimateComponentsMemory<float, float>(params->metric, is_normalized);
est += EstimateInitialSize_ChooseMultiOrSingle<float>(params->multi);
} else if (params->type == VecSimType_FLOAT64) {
Expand Down Expand Up @@ -125,9 +273,21 @@ size_t EstimateElementSize(const HNSWParams *params) {
size_t M = (params->M) ? params->M : HNSW_DEFAULT_M;
size_t elementGraphDataSize = sizeof(ElementGraphData) + sizeof(idType) * M * 2;

size_t size_total_data_per_element =
elementGraphDataSize +
VecSimParams_GetStoredDataSize(params->type, params->dim, params->metric);
size_t stored_data_size;
// Preserve the element estimator's existing contract: return a size and validate in NewIndex.
if (params->quantType == VecSimQuant_SQ8) {
bool with_norm = params->quantParams != nullptr;
if (params->metric == VecSimMetric_L2) {
stored_data_size = GetSQ8StoredDataSize<VecSimMetric_L2>(params->dim, with_norm);
} else {
stored_data_size = GetSQ8StoredDataSize<VecSimMetric_IP>(params->dim, with_norm);
}
Comment thread
cursor[bot] marked this conversation as resolved.
} else {
stored_data_size =
VecSimParams_GetStoredDataSize(params->type, params->dim, params->metric);
}

size_t size_total_data_per_element = elementGraphDataSize + stored_data_size;

// when reserving space for new labels in the lookup hash table, each entry is a pointer to a
// label node (bucket).
Expand Down
12 changes: 12 additions & 0 deletions src/VecSim/index_factories/tiered_factory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ inline VecSimIndex *NewIndex(const TieredIndexParams *params) {
inline size_t EstimateInitialSize(const TieredIndexParams *params) {
HNSWParams hnsw_params = params->primaryIndexParams->algoParams.hnswParams;

// Keep size estimation consistent with NewIndex, which rejects quantized tiered indexes.
if (hnsw_params.quantType != VecSimQuant_NONE) {
throw std::invalid_argument("Quantization is not supported for tiered HNSW indexes");
}

// Add size estimation of VecSimTieredIndex sub indexes.
// Normalization is done by the frontend index.
size_t est = HNSWFactory::EstimateInitialSize(&hnsw_params, true);
Expand Down Expand Up @@ -95,6 +100,12 @@ inline size_t EstimateInitialSize(const TieredIndexParams *params) {
}

VecSimIndex *NewIndex(const TieredIndexParams *params) {
// The brute-force frontend is not quantized, so an SQ8 primary index would use an incompatible
// stored-vector layout.
if (params->primaryIndexParams->algoParams.hnswParams.quantType != VecSimQuant_NONE) {
return nullptr;
}

// Tiered index that contains HNSW index as primary index
VecSimType type = params->primaryIndexParams->algoParams.hnswParams.type;
if (type == VecSimType_FLOAT32) {
Expand Down Expand Up @@ -233,6 +244,7 @@ size_t EstimateInitialSize(const TieredIndexParams *params) {
}

size_t EstimateElementSize(const TieredIndexParams *params) {
// Match HNSW's element estimator, which leaves validation to NewIndex.
size_t est = 0;
if (params->primaryIndexParams->algo == VecSimAlgo_HNSWLIB) {
est = HNSWFactory::EstimateElementSize(&params->primaryIndexParams->algoParams.hnswParams);
Expand Down
7 changes: 2 additions & 5 deletions src/VecSim/spaces/computer/preprocessors.h
Original file line number Diff line number Diff line change
Expand Up @@ -476,8 +476,7 @@ class QuantPreprocessor : public PreprocessorInterface {
QuantPreprocessor(std::shared_ptr<VecSimAllocator> allocator, size_t dim)
requires(!WithNorm)
: PreprocessorInterface(allocator), dim(dim),
storage_bytes_count(dim * sizeof(OUTPUT_TYPE) +
sq8::storage_metadata_count<Metric>() * sizeof(MetadataType)),
storage_bytes_count(sq8::storage_bytes_count<Metric>(dim)),
Comment thread
dor-forer marked this conversation as resolved.
query_bytes_count(dim * sizeof(DataType) +
sq8::query_metadata_count<Metric>() * sizeof(MetadataType)) {}

Expand All @@ -486,9 +485,7 @@ class QuantPreprocessor : public PreprocessorInterface {
const vecsim_stl::vector<float> &mean_vec)
requires(WithNorm)
: PreprocessorInterface(allocator), mean(mean_vec), dim(dim),
storage_bytes_count(dim * sizeof(OUTPUT_TYPE) +
sq8::storage_metadata_count<Metric, WithNorm>() *
sizeof(MetadataType)),
storage_bytes_count(sq8::storage_bytes_count<Metric, WithNorm>(dim)),
query_bytes_count(dim * sizeof(DataType) +
sq8::query_metadata_count<Metric, WithNorm>() * sizeof(MetadataType)) {
assert(this->mean.size() == dim && "mean vector size must equal dim");
Expand Down
7 changes: 7 additions & 0 deletions src/VecSim/types/sq8.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ struct sq8 {
((WithNorm && Metric == VecSimMetric_IP) ? 1 : 0);
}

// Stored layout: one quantized byte per dimension, followed by metric-specific FP32 metadata.
template <VecSimMetric Metric, bool WithNorm = false>
static constexpr size_t storage_bytes_count(size_t dim) {
return dim * sizeof(value_type) +
storage_metadata_count<Metric, WithNorm>() * sizeof(float);
}

// Index of x_mean_ip / y_mean_ip in the last slot in metadata array
template <VecSimMetric Metric>
static constexpr size_t mean_ip_index() {
Expand Down
6 changes: 5 additions & 1 deletion src/VecSim/vec_sim.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,11 @@ extern "C" VecSimIndex *VecSimIndex_New(const VecSimParams *params) {
}

extern "C" size_t VecSimIndex_EstimateInitialSize(const VecSimParams *params) {
return VecSimFactory::EstimateInitialSize(params);
try {
return VecSimFactory::EstimateInitialSize(params);
} catch (...) {
return SIZE_MAX;
}
}

extern "C" int VecSimIndex_AddVector(VecSimIndex *index, const void *blob, size_t label) {
Expand Down
7 changes: 4 additions & 3 deletions src/VecSim/vec_sim.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ VecSimIndex *VecSimIndex_New(const VecSimParams *params);
* @brief Estimates the size of an empty index according to the parameters.
* @param params index configurations (initial size, data type, dimension, metric, algorithm and the
* algorithm-related params).
* @return Estimated index size.
* @return Estimated index size, or SIZE_MAX if the parameters are invalid or unsupported.
*/
size_t VecSimIndex_EstimateInitialSize(const VecSimParams *params);

Expand Down Expand Up @@ -84,8 +84,9 @@ int VecSimIndex_DeleteVector(VecSimIndex *index, size_t label);
* @param label the label of the vector in the index.
* @param blob binary representation of the second vector. Blob size should match the index data
* type and dimension, and pre-normalized if needed.
* @return The distance (according to the index's distance metric) between `blob` and the vector
* with label label`.
* @return The distance between `blob` and the vector with `label`, or INVALID_SCORE if the label is
* absent.
* @note Quantized indexes allocate and preprocess a temporary query blob on each call.
*/
double VecSimIndex_GetDistanceFrom_Unsafe(VecSimIndex *index, size_t label, const void *blob);

Expand Down
10 changes: 10 additions & 0 deletions src/VecSim/vec_sim_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ typedef enum {
VecSimType_INT64
} VecSimType;

// Quantization schemes supported by HNSW.
typedef enum {
VecSimQuant_NONE = 0, // No quantization (default).
// 8-bit scalar quantization with optional mean centering.
VecSimQuant_SQ8 = 1,
} VecSimQuantType;

// Algorithm type/library.
typedef enum { VecSimAlgo_BF, VecSimAlgo_HNSWLIB, VecSimAlgo_TIERED, VecSimAlgo_SVS } VecSimAlgo;

Expand Down Expand Up @@ -156,6 +163,9 @@ typedef struct {
size_t efConstruction;
size_t efRuntime;
double epsilon;
VecSimQuantType quantType; // Defaults to VecSimQuant_NONE.
// SQ8 mean vector (float[dim]); NULL disables mean centering. Copied during construction.
const void *quantParams;
} HNSWParams;

typedef struct {
Expand Down
Loading
Loading