diff --git a/python/tests/test_params.py b/python/tests/test_params.py index 4922910..56492ad 100644 --- a/python/tests/test_params.py +++ b/python/tests/test_params.py @@ -325,13 +325,26 @@ class TestHnswQueryParam: assert param.is_using_refiner == False assert param.radius == 0 assert param.is_linear == False + assert param.prefetch_offset == 8 + assert param.prefetch_lines == 0 def test_custom(self): - param = HnswQueryParam(ef=10, is_using_refiner=True, radius=30, is_linear=True) + param = HnswQueryParam( + ef=10, + is_using_refiner=True, + radius=30, + is_linear=True, + extra_params={ + "prefetch_offset": 16, + "prefetch_lines": 4, + }, + ) assert param.ef == 10 assert param.is_using_refiner == True assert param.radius == 30 assert param.is_linear == True + assert param.prefetch_offset == 16 + assert param.prefetch_lines == 4 def test_readonly_attributes(self): param = HnswQueryParam() diff --git a/python/tests/test_vamana.py b/python/tests/test_vamana.py index 50d6830..d05e0a8 100644 --- a/python/tests/test_vamana.py +++ b/python/tests/test_vamana.py @@ -274,16 +274,27 @@ class TestVamanaQueryParamSurface: assert q.radius == pytest.approx(0.0) assert q.is_linear is False assert q.is_using_refiner is False + assert q.prefetch_offset == 8 + assert q.prefetch_lines == 0 def test_custom_construction(self): q = VamanaQueryParam( - ef_search=300, radius=0.5, is_linear=True, is_using_refiner=True + ef_search=300, + radius=0.5, + is_linear=True, + is_using_refiner=True, + extra_params={ + "prefetch_offset": 8, + "prefetch_lines": 2, + }, ) assert q.type == IndexType.VAMANA assert q.ef_search == 300 assert q.radius == pytest.approx(0.5) assert q.is_linear is True assert q.is_using_refiner is True + assert q.prefetch_offset == 8 + assert q.prefetch_lines == 2 def test_repr_contains_key_fields(self): text = repr(VamanaQueryParam(ef_search=128, radius=0.25)) @@ -302,7 +313,14 @@ class TestVamanaQueryParamSurface: def test_pickle_roundtrip(self): original = VamanaQueryParam( - ef_search=256, radius=0.3, is_linear=False, is_using_refiner=True + ef_search=256, + radius=0.3, + is_linear=False, + is_using_refiner=True, + extra_params={ + "prefetch_offset": 4, + "prefetch_lines": 3, + }, ) restored = pickle.loads(pickle.dumps(original)) assert restored.type == IndexType.VAMANA @@ -310,6 +328,8 @@ class TestVamanaQueryParamSurface: assert restored.radius == pytest.approx(0.3) assert restored.is_linear is False assert restored.is_using_refiner is True + assert restored.prefetch_offset == 4 + assert restored.prefetch_lines == 3 class TestVamanaPublicNamespace: diff --git a/python/zvec/model/param/__init__.pyi b/python/zvec/model/param/__init__.pyi index f4ff22a..759b413 100644 --- a/python/zvec/model/param/__init__.pyi +++ b/python/zvec/model/param/__init__.pyi @@ -267,6 +267,12 @@ class HnswQueryParam(QueryParam): radius (float): Search radius for range queries. Default is 0.0. is_linear (bool): Force linear search. Default is False. is_using_refiner (bool, optional): Whether to use refiner for the query. Default is False. + prefetch_offset (int, optional): Graph prefetch offset (PO) used by the + HNSW fast path. ``0`` disables prefetching. Default is ``8``. + Values are clamped to ``256``. + prefetch_lines (int, optional): Number of 64B cache lines to prefetch + per neighbour vector (PL). ``0`` (default) uses the auto-derived + value ``ceil(vector_size/64)``. Values are clamped to ``256``. Examples: >>> params = HnswQueryParam(ef=300) @@ -282,6 +288,7 @@ class HnswQueryParam(QueryParam): radius: typing.SupportsFloat = 0.0, is_linear: bool = False, is_using_refiner: bool = False, + extra_params: dict[str, int] = ..., ) -> None: """ Constructs an HnswQueryParam instance. @@ -292,6 +299,11 @@ class HnswQueryParam(QueryParam): radius (float, optional): Search radius for range queries. Default is 0.0. is_linear (bool, optional): Force linear search. Default is False. is_using_refiner (bool, optional): Whether to use refiner for the query. Default is False. + extra_params (dict, optional): Additional search parameters. Supported keys: + - ``prefetch_offset`` (int): Graph prefetch offset (PO). + ``0`` disables prefetching. Default is ``8``. + - ``prefetch_lines`` (int): Number of 64B cache lines to prefetch + per neighbour vector (PL). ``0`` (default) means auto-derive from vector size. """ def __repr__(self) -> str: ... def __setstate__(self, arg0: tuple) -> None: ... @@ -301,6 +313,18 @@ class HnswQueryParam(QueryParam): int: Size of the dynamic candidate list during HNSW search. """ + @property + def prefetch_offset(self) -> int: + """ + int: Graph prefetch offset used by the HNSW fast path. + """ + + @property + def prefetch_lines(self) -> int: + """ + int: Override of prefetch cache lines per vector (0=auto). + """ + class HnswRabitqIndexParam(VectorIndexParam): """ @@ -549,6 +573,110 @@ class IVFQueryParam(QueryParam): int: Number of inverted lists to search during IVF query. """ +class VamanaIndexParam(VectorIndexParam): + """ + Parameters for configuring a Vamana (DiskANN) index. + + Attributes: + metric_type (MetricType): Distance metric. Default is ``MetricType.IP``. + max_degree (int): Maximum out-degree (R) of every node. Default is 64. + search_list_size (int): Candidate list size during construction. Default is 100. + alpha (float): RobustPrune alpha factor. Default is 1.2. + saturate_graph (bool): Force every node to reach max_degree. Default is False. + use_contiguous_memory (bool): Allocate contiguous memory arena. Default is False. + use_id_map (bool): Reserved flag for id remapping. Default is False. + quantize_type (QuantizeType): Vector quantization type. Default is ``QuantizeType.UNDEFINED``. + + Examples: + >>> params = VamanaIndexParam(metric_type=MetricType.COSINE, max_degree=64) + """ + def __getstate__(self) -> tuple: ... + def __init__( + self, + metric_type: _zvec.typing.MetricType = ..., + max_degree: typing.SupportsInt = 64, + search_list_size: typing.SupportsInt = 100, + alpha: typing.SupportsFloat = 1.2, + saturate_graph: bool = False, + use_contiguous_memory: bool = False, + use_id_map: bool = False, + quantize_type: _zvec.typing.QuantizeType = ..., + ) -> None: ... + def __repr__(self) -> str: ... + def __setstate__(self, arg0: tuple) -> None: ... + def to_dict(self) -> dict: ... + @property + def max_degree(self) -> int: + """int: Maximum out-degree (R) of every node in the Vamana graph.""" + @property + def search_list_size(self) -> int: + """int: Candidate list size during Vamana graph construction.""" + @property + def alpha(self) -> float: + """float: Vamana RobustPrune alpha factor.""" + @property + def saturate_graph(self) -> bool: + """bool: Whether to saturate every node to max_degree neighbors.""" + @property + def use_contiguous_memory(self) -> bool: + """bool: Whether to allocate a single contiguous memory arena.""" + @property + def use_id_map(self) -> bool: + """bool: Reserved flag for engine-level id remapping.""" + +class VamanaQueryParam(QueryParam): + """ + Query parameters for the Vamana (DiskANN) index. + + Attributes: + type (IndexType): Always ``IndexType.VAMANA``. + ef_search (int): Size of the dynamic candidate list during search. Default is 200. + radius (float): Search radius for range queries. Default is 0.0. + is_linear (bool): Force linear search. Default is False. + is_using_refiner (bool): Whether to use refiner. Default is False. + prefetch_offset (int): Graph prefetch offset (PO). Default is 8. + prefetch_lines (int): Cache lines to prefetch per vector (PL). Default is 0 (auto). + + Examples: + >>> params = VamanaQueryParam(ef_search=200) + >>> print(params.ef_search) + 200 + """ + def __getstate__(self) -> tuple: ... + def __init__( + self, + ef_search: typing.SupportsInt = 200, + radius: typing.SupportsFloat = 0.0, + is_linear: bool = False, + is_using_refiner: bool = False, + extra_params: dict[str, int] = ..., + ) -> None: + """ + Constructs a VamanaQueryParam instance. + + Args: + ef_search (int, optional): Search-time candidate list size. Defaults to 200. + radius (float, optional): Search radius for range queries. Default is 0.0. + is_linear (bool, optional): Force linear search. Default is False. + is_using_refiner (bool, optional): Whether to use refiner. Default is False. + extra_params (dict, optional): Additional search parameters. Supported keys: + - ``prefetch_offset`` (int): Graph prefetch offset (PO). + ``0`` disables prefetching. Default is ``8``. + - ``prefetch_lines`` (int): Cache lines to prefetch per vector (PL). + ``0`` (default) means auto-derive from vector size. + """ + def __repr__(self) -> str: ... + def __setstate__(self, arg0: tuple) -> None: ... + @property + def ef_search(self) -> int: + """int: Size of the dynamic candidate list during Vamana search.""" + @property + def prefetch_offset(self) -> int: + """int: Graph prefetch offset used by the Vamana fast path.""" + @property + def prefetch_lines(self) -> int: + """int: Override of prefetch cache lines per vector (0=auto).""" + class IndexOption: """ diff --git a/src/binding/c/c_api.cc b/src/binding/c/c_api.cc index 2736169..a81cc38 100644 --- a/src/binding/c/c_api.cc +++ b/src/binding/c/c_api.cc @@ -1343,6 +1343,18 @@ zvec_index_params_t *zvec_index_params_create(zvec_index_type_t index_type) { false, // use_soar (default) zvec::QuantizeType::UNDEFINED); break; + case ZVEC_INDEX_TYPE_VAMANA: + cpp_params = + new zvec::VamanaIndexParams( + zvec::MetricType::L2, // metric_type + zvec::core_interface::kDefaultVamanaMaxDegree, + zvec::core_interface::kDefaultVamanaSearchListSize, + zvec::core_interface::kDefaultVamanaAlpha, + zvec::core_interface::kDefaultVamanaSaturateGraph, + false, // use_contiguous_memory + false, // use_id_map + zvec::QuantizeType::UNDEFINED); + break; case ZVEC_INDEX_TYPE_FLAT: default: cpp_params = @@ -1546,6 +1558,55 @@ int zvec_index_params_get_hnsw_ef_construction(const zvec_index_params_t *params return hnsw_params->ef_construction(); } +zvec_error_code_t zvec_index_params_set_vamana_params( + zvec_index_params_t *params, int max_degree, int search_list_size, + float alpha, bool saturate_graph, bool use_contiguous_memory) { + if (!params) { + SET_LAST_ERROR(ZVEC_ERROR_INVALID_ARGUMENT, + "Invalid params or not Vamana index type"); + return ZVEC_ERROR_INVALID_ARGUMENT; + } + auto *cpp_params = reinterpret_cast(params); + auto *vamana_params = dynamic_cast(cpp_params); + if (!vamana_params) { + SET_LAST_ERROR(ZVEC_ERROR_INVALID_ARGUMENT, + "Invalid params or not Vamana index type"); + return ZVEC_ERROR_INVALID_ARGUMENT; + } + vamana_params->set_max_degree(max_degree); + vamana_params->set_search_list_size(search_list_size); + vamana_params->set_alpha(alpha); + vamana_params->set_saturate_graph(saturate_graph); + vamana_params->set_use_contiguous_memory(use_contiguous_memory); + return ZVEC_OK; +} + +zvec_error_code_t zvec_index_params_get_vamana_params( + const zvec_index_params_t *params, int *out_max_degree, + int *out_search_list_size, float *out_alpha, bool *out_saturate_graph, + bool *out_use_contiguous_memory) { + if (!params || !out_max_degree || !out_search_list_size || !out_alpha || + !out_saturate_graph || !out_use_contiguous_memory) { + SET_LAST_ERROR(ZVEC_ERROR_INVALID_ARGUMENT, + "Invalid params or output pointer is null"); + return ZVEC_ERROR_INVALID_ARGUMENT; + } + auto *cpp_params = reinterpret_cast(params); + auto *vamana_params = + dynamic_cast(cpp_params); + if (!vamana_params) { + SET_LAST_ERROR(ZVEC_ERROR_INVALID_ARGUMENT, + "Invalid params or not Vamana index type"); + return ZVEC_ERROR_INVALID_ARGUMENT; + } + *out_max_degree = vamana_params->max_degree(); + *out_search_list_size = vamana_params->search_list_size(); + *out_alpha = vamana_params->alpha(); + *out_saturate_graph = vamana_params->saturate_graph(); + *out_use_contiguous_memory = vamana_params->use_contiguous_memory(); + return ZVEC_OK; +} + /** * @brief Set IVF-specific parameters * @param params Index parameters (must be IVF type) @@ -5146,6 +5207,102 @@ zvec_error_code_t zvec_vector_query_get_output_fields(const zvec_vector_query_t return ZVEC_OK; } +// ============================================================================= +// VamanaQueryParams implementation - wrapper around zvec::VamanaQueryParams +// ============================================================================= + +zvec_vamana_query_params_t *zvec_query_params_vamana_create( + int ef_search, float radius, bool is_linear, bool is_using_refiner) { + ZVEC_TRY_RETURN_NULL( + "Failed to create VamanaQueryParams", + auto *params = new zvec::VamanaQueryParams(ef_search, radius, is_linear, + is_using_refiner); + return reinterpret_cast(params);) + return nullptr; +} + +void zvec_query_params_vamana_destroy(zvec_vamana_query_params_t *params) { + if (params) { + delete reinterpret_cast(params); + } +} + +zvec_error_code_t zvec_query_params_vamana_set_ef_search( + zvec_vamana_query_params_t *params, int ef_search) { + if (!params) { + SET_LAST_ERROR(ZVEC_ERROR_INVALID_ARGUMENT, + "Vamana query params pointer is null"); + return ZVEC_ERROR_INVALID_ARGUMENT; + } + auto *ptr = reinterpret_cast(params); + ptr->set_ef_search(ef_search); + return ZVEC_OK; +} + +int zvec_query_params_vamana_get_ef_search( + const zvec_vamana_query_params_t *params) { + if (!params) return zvec::core_interface::kDefaultVamanaEfSearch; + auto *ptr = reinterpret_cast(params); + return ptr->ef_search(); +} + +zvec_error_code_t zvec_query_params_vamana_set_radius( + zvec_vamana_query_params_t *params, float radius) { + if (!params) { + SET_LAST_ERROR(ZVEC_ERROR_INVALID_ARGUMENT, + "Vamana query params pointer is null"); + return ZVEC_ERROR_INVALID_ARGUMENT; + } + auto *ptr = reinterpret_cast(params); + ptr->set_radius(radius); + return ZVEC_OK; +} + +float zvec_query_params_vamana_get_radius( + const zvec_vamana_query_params_t *params) { + if (!params) return 0.0f; + auto *ptr = reinterpret_cast(params); + return ptr->radius(); +} + +zvec_error_code_t zvec_query_params_vamana_set_is_linear( + zvec_vamana_query_params_t *params, bool is_linear) { + if (!params) { + SET_LAST_ERROR(ZVEC_ERROR_INVALID_ARGUMENT, + "Vamana query params pointer is null"); + return ZVEC_ERROR_INVALID_ARGUMENT; + } + auto *ptr = reinterpret_cast(params); + ptr->set_is_linear(is_linear); + return ZVEC_OK; +} + +bool zvec_query_params_vamana_get_is_linear( + const zvec_vamana_query_params_t *params) { + if (!params) return false; + auto *ptr = reinterpret_cast(params); + return ptr->is_linear(); +} + +zvec_error_code_t zvec_query_params_vamana_set_is_using_refiner( + zvec_vamana_query_params_t *params, bool is_using_refiner) { + if (!params) { + SET_LAST_ERROR(ZVEC_ERROR_INVALID_ARGUMENT, + "Vamana query params pointer is null"); + return ZVEC_ERROR_INVALID_ARGUMENT; + } + auto *ptr = reinterpret_cast(params); + ptr->set_is_using_refiner(is_using_refiner); + return ZVEC_OK; +} + +bool zvec_query_params_vamana_get_is_using_refiner( + const zvec_vamana_query_params_t *params) { + if (!params) return false; + auto *ptr = reinterpret_cast(params); + return ptr->is_using_refiner(); +} + // ============================================================================= // Type-safe query params attachment functions (transfer ownership to // query object) @@ -5237,6 +5394,23 @@ zvec_error_code_t zvec_vector_query_set_fts_params( return ZVEC_OK; } +zvec_error_code_t zvec_vector_query_set_vamana_params( + zvec_vector_query_t *query, zvec_vamana_query_params_t *vamana_params) { + if (!query || !vamana_params) { + SET_LAST_ERROR(ZVEC_ERROR_INVALID_ARGUMENT, + "Query or Vamana params pointer is null"); + return ZVEC_ERROR_INVALID_ARGUMENT; + } + + auto *query_ptr = reinterpret_cast(query); + auto *params_ptr = + reinterpret_cast(vamana_params); + + query_ptr->target_.query_params_.reset(params_ptr); + + return ZVEC_OK; +} + // ============================================================================= // Fts payload implementation - wrapper around zvec::FtsClause (value type) // ============================================================================= @@ -5574,6 +5748,24 @@ zvec_error_code_t zvec_group_by_vector_query_set_flat_params( return ZVEC_OK; } +zvec_error_code_t zvec_group_by_vector_query_set_vamana_params( + zvec_group_by_vector_query_t *query, + zvec_vamana_query_params_t *vamana_params) { + if (!query || !vamana_params) { + SET_LAST_ERROR(ZVEC_ERROR_INVALID_ARGUMENT, + "Query or Vamana params pointer is null"); + return ZVEC_ERROR_INVALID_ARGUMENT; + } + + auto *query_ptr = reinterpret_cast(query); + auto *params_ptr = + reinterpret_cast(vamana_params); + + query_ptr->target_.query_params_.reset(params_ptr); + + return ZVEC_OK; +} + // ============================================================================= // Reranker Implementation // ============================================================================= @@ -5916,6 +6108,20 @@ zvec_error_code_t zvec_sub_query_set_flat_params( return ZVEC_OK; } +zvec_error_code_t zvec_sub_query_set_vamana_params( + zvec_sub_query_t *query, zvec_vamana_query_params_t *vamana_params) { + if (!query || !vamana_params) { + SET_LAST_ERROR(ZVEC_ERROR_INVALID_ARGUMENT, + "Sub-vector query or Vamana params pointer is null"); + return ZVEC_ERROR_INVALID_ARGUMENT; + } + auto *ptr = reinterpret_cast(query); + auto *params_ptr = + reinterpret_cast(vamana_params); + ptr->target_.query_params_.reset(params_ptr); + return ZVEC_OK; +} + // ============================================================================= // Index Interface Implementation // ============================================================================= diff --git a/src/binding/python/model/param/python_param.cc b/src/binding/python/model/param/python_param.cc index 1045e23..268214d 100644 --- a/src/binding/python/model/param/python_param.cc +++ b/src/binding/python/model/param/python_param.cc @@ -1057,10 +1057,24 @@ Examples: {"type":"HNSW", "ef":300} )pbdoc"); hnsw_params - .def(py::init(), + .def(py::init([](int ef, float radius, bool is_linear, + bool is_using_refiner, py::dict extra_params) { + auto obj = std::make_shared(ef, radius, is_linear, + is_using_refiner); + if (extra_params.contains("prefetch_offset")) { + obj->set_prefetch_offset( + extra_params["prefetch_offset"].cast()); + } + if (extra_params.contains("prefetch_lines")) { + obj->set_prefetch_lines( + extra_params["prefetch_lines"].cast()); + } + return obj; + }), py::arg("ef") = core_interface::kDefaultHnswEfSearch, py::arg("radius") = 0.0f, py::arg("is_linear") = false, py::arg("is_using_refiner") = false, + py::arg("extra_params") = py::dict(), R"pbdoc( Constructs an HnswQueryParam instance. @@ -1070,10 +1084,29 @@ Args: radius (float, optional): Search radius for range queries. Default is 0.0. is_linear (bool, optional): Force linear search. Default is False. is_using_refiner (bool, optional): Whether to use refiner for the query. Default is False. + extra_params (dict, optional): Additional search parameters. Supported keys: + - ``prefetch_offset`` (int): Graph prefetch offset (PO). + ``0`` disables prefetching. Default is ``8``. + Values are clamped to ``256``. + - ``prefetch_lines`` (int): Number of 64B cache lines to prefetch + per neighbour vector (PL). ``0`` (default) uses the auto-derived + value ``ceil(vector_size/64)``. Values are clamped to ``256``. )pbdoc") .def_property_readonly( "ef", [](const HnswQueryParams &self) -> int { return self.ef(); }, "int: Size of the dynamic candidate list during HNSW search.") + .def_property_readonly( + "prefetch_offset", + [](const HnswQueryParams &self) -> uint32_t { + return self.prefetch_offset(); + }, + "int: Graph prefetch offset used by the HNSW fast path.") + .def_property_readonly( + "prefetch_lines", + [](const HnswQueryParams &self) -> uint32_t { + return self.prefetch_lines(); + }, + "int: Override of prefetch cache lines per vector (0=auto).") .def("__repr__", [](const HnswQueryParams &self) -> std::string { return "{" @@ -1083,20 +1116,32 @@ Args: ", \"radius\":" + std::to_string(self.radius()) + ", \"is_linear\":" + std::to_string(self.is_linear()) + ", \"is_using_refiner\":" + - std::to_string(self.is_using_refiner()) + "}"; + std::to_string(self.is_using_refiner()) + + ", \"prefetch_offset\":" + + std::to_string(self.prefetch_offset()) + + ", \"prefetch_lines\":" + + std::to_string(self.prefetch_lines()) + "}"; }) .def(py::pickle( [](const HnswQueryParams &self) { return py::make_tuple(self.ef(), self.radius(), self.is_linear(), - self.is_using_refiner()); + self.is_using_refiner(), + self.prefetch_offset(), + self.prefetch_lines()); }, [](py::tuple t) { - if (t.size() != 4) + if (t.size() != 4 && t.size() != 5 && t.size() != 6) throw std::runtime_error("Invalid state for HnswQueryParams"); auto obj = std::make_shared(t[0].cast()); obj->set_radius(t[1].cast()); obj->set_is_linear(t[2].cast()); obj->set_is_using_refiner(t[3].cast()); + if (t.size() >= 5) { + obj->set_prefetch_offset(t[4].cast()); + } + if (t.size() >= 6) { + obj->set_prefetch_lines(t[5].cast()); + } return obj; })); @@ -1298,10 +1343,24 @@ Examples: 200 )pbdoc"); vamana_query_params - .def(py::init(), + .def(py::init([](int ef_search, float radius, bool is_linear, + bool is_using_refiner, py::dict extra_params) { + auto obj = std::make_shared( + ef_search, radius, is_linear, is_using_refiner); + if (extra_params.contains("prefetch_offset")) { + obj->set_prefetch_offset( + extra_params["prefetch_offset"].cast()); + } + if (extra_params.contains("prefetch_lines")) { + obj->set_prefetch_lines( + extra_params["prefetch_lines"].cast()); + } + return obj; + }), py::arg("ef_search") = core_interface::kDefaultVamanaEfSearch, py::arg("radius") = 0.0f, py::arg("is_linear") = false, py::arg("is_using_refiner") = false, + py::arg("extra_params") = py::dict(), R"pbdoc( Constructs a VamanaQueryParam instance. @@ -1312,11 +1371,30 @@ Args: is_linear (bool, optional): Force linear search. Default is False. is_using_refiner (bool, optional): Whether to use refiner for the query. Default is False. + extra_params (dict, optional): Additional search parameters. Supported keys: + - ``prefetch_offset`` (int): Graph prefetch offset (PO). + ``0`` disables prefetching. Default is ``8``. + Values are clamped to ``256``. + - ``prefetch_lines`` (int): Number of 64B cache lines to prefetch + per neighbour vector (PL). ``0`` (default) uses the auto-derived + value ``ceil(dim/64)``. Values are clamped to ``256``. )pbdoc") .def_property_readonly( "ef_search", [](const VamanaQueryParams &self) -> int { return self.ef_search(); }, "int: Size of the dynamic candidate list during Vamana search.") + .def_property_readonly( + "prefetch_offset", + [](const VamanaQueryParams &self) -> uint32_t { + return self.prefetch_offset(); + }, + "int: Graph prefetch offset used by the Vamana fast path.") + .def_property_readonly( + "prefetch_lines", + [](const VamanaQueryParams &self) -> uint32_t { + return self.prefetch_lines(); + }, + "int: Override of prefetch cache lines per vector (0=auto).") .def("__repr__", [](const VamanaQueryParams &self) -> std::string { return "{" @@ -1326,20 +1404,32 @@ Args: ", \"radius\":" + std::to_string(self.radius()) + ", \"is_linear\":" + std::to_string(self.is_linear()) + ", \"is_using_refiner\":" + - std::to_string(self.is_using_refiner()) + "}"; + std::to_string(self.is_using_refiner()) + + ", \"prefetch_offset\":" + + std::to_string(self.prefetch_offset()) + + ", \"prefetch_lines\":" + + std::to_string(self.prefetch_lines()) + "}"; }) .def(py::pickle( [](const VamanaQueryParams &self) { return py::make_tuple(self.ef_search(), self.radius(), - self.is_linear(), self.is_using_refiner()); + self.is_linear(), self.is_using_refiner(), + self.prefetch_offset(), + self.prefetch_lines()); }, [](py::tuple t) { - if (t.size() != 4) + if (t.size() != 4 && t.size() != 5 && t.size() != 6) throw std::runtime_error("Invalid state for VamanaQueryParams"); auto obj = std::make_shared(t[0].cast()); obj->set_radius(t[1].cast()); obj->set_is_linear(t[2].cast()); obj->set_is_using_refiner(t[3].cast()); + if (t.size() >= 5) { + obj->set_prefetch_offset(t[4].cast()); + } + if (t.size() >= 6) { + obj->set_prefetch_lines(t[5].cast()); + } return obj; })); diff --git a/src/core/algorithm/hnsw/hnsw_algorithm.cc b/src/core/algorithm/hnsw/hnsw_algorithm.cc index 0480cc4..8c6fcfe 100644 --- a/src/core/algorithm/hnsw/hnsw_algorithm.cc +++ b/src/core/algorithm/hnsw/hnsw_algorithm.cc @@ -191,7 +191,8 @@ template void fast_search_neighbors(const EntityType &entity, HeapType &pool, VisitFilter &visit, HnswDistCalculator &dc, uint32_t topk, uint32_t ef, node_id_t entry_point, - dist_t entry_dist, uint32_t prefetch_lines) { + dist_t entry_dist, uint32_t prefetch_lines, + uint32_t prefetch_offset) { const uint32_t max_deg = entity.max_degree(0); // level 0 only const uint32_t cap = std::max(topk, ef); pool.reset(static_cast(cap), static_cast(max_deg)); @@ -200,8 +201,6 @@ void fast_search_neighbors(const EntityType &entity, HeapType &pool, visit.set_visited(entry_point); pool.push_block(&entry_dist, &entry_point, 1); - static constexpr uint32_t GRAPH_PO = 8; - uint32_t buf_capacity = max_deg; std::vector neighbor_ids(buf_capacity); std::vector dists(buf_capacity); @@ -221,7 +220,7 @@ void fast_search_neighbors(const EntityType &entity, HeapType &pool, } const uint32_t po = - std::min(static_cast(neighbors.size()), GRAPH_PO); + std::min(static_cast(neighbors.size()), prefetch_offset); uint32_t unvisited_count = 0; uint32_t i = 0; @@ -270,8 +269,9 @@ void dual_heap_search_neighbors(const EntityType &entity, level_t level, node_id_t *entry_point, dist_t *dist, TopkHeap &topk, HnswContext *ctx, HnswDistCalculator &dc, FilterFn &&filter) { - static constexpr uint32_t BATCH_SIZE = 12; - static constexpr uint32_t PREFETCH_STEP = 2; + const uint32_t prefetch_offset = ctx->po(); + const uint32_t prefetch_lines = + ctx->pl() > 0 ? ctx->pl() : (entity.vector_size() + 63) / 64; uint32_t buf_capacity = entity.max_degree(level); std::vector neighbor_ids(buf_capacity); @@ -342,8 +342,12 @@ void dual_heap_search_neighbors(const EntityType &entity, level_t level, } // do prefetch - for (uint32_t i = 0; i < std::min(BATCH_SIZE * PREFETCH_STEP, size); ++i) { - ailego_prefetch(neighbor_vec_blocks[i].data()); + for (uint32_t i = 0; i < std::min(prefetch_offset, size); ++i) { + const char *base = + static_cast(neighbor_vec_blocks[i].data()); + for (uint32_t cl = 0; cl < prefetch_lines; ++cl) { + ailego_prefetch(base + cl * 64); + } } for (uint32_t i = 0; i < size; ++i) { @@ -388,8 +392,6 @@ void HnswAlgorithm::search_neighbors(level_t level, const auto &entity = static_cast(ctx->get_entity()); HnswDistCalculator &dc = ctx->dist_calculator(); - const uint32_t prefetch_lines = (entity.vector_size() + 63) / 64; - if (!use_pool || ctx->filter().is_valid() || level != 0) { // Dual-heap path: add_node, filtered search, or upper-level scan. auto run_with_filter = [&](auto &&filter) { @@ -410,6 +412,9 @@ void HnswAlgorithm::search_neighbors(level_t level, } else { // Pool-based path for level-0 unfiltered search. if constexpr (std::is_same_v) { + const uint32_t prefetch_lines = + ctx->pl() > 0 ? ctx->pl() : (entity.vector_size() + 63) / 64; + // Fast path: direct pointer access via get_vector_ptr. // BlockHeap (AVX2) or LinearPool (scalar) for top-k tracking. const uint32_t topk_v = static_cast(ctx->topk()); @@ -422,12 +427,12 @@ void HnswAlgorithm::search_neighbors(level_t level, if (avx2_ok) { auto &bpool = ctx->block_pool(); fast_search_neighbors(entity, bpool, visit, dc, topk_v, ef_v, - *entry_point, *dist, prefetch_lines); + *entry_point, *dist, prefetch_lines, ctx->po()); copy_pool_to_topk(bpool, topk); } else { auto &lpool = ctx->pool(); fast_search_neighbors(entity, lpool, visit, dc, topk_v, ef_v, - *entry_point, *dist, prefetch_lines); + *entry_point, *dist, prefetch_lines, ctx->po()); copy_pool_to_topk(lpool, topk); } } else { diff --git a/src/core/algorithm/hnsw/hnsw_context.cc b/src/core/algorithm/hnsw/hnsw_context.cc index dd25e53..2310ffc 100644 --- a/src/core/algorithm/hnsw/hnsw_context.cc +++ b/src/core/algorithm/hnsw/hnsw_context.cc @@ -151,6 +151,14 @@ int HnswContext::update(const ailego::Params ¶ms) { topk_heap_.limit(std::max(topk_, ef_)); } + if (params.has(PARAM_HNSW_SEARCHER_PO)) { + params.get(PARAM_HNSW_SEARCHER_PO, &po_); + } + + if (params.has(PARAM_HNSW_SEARCHER_PL)) { + params.get(PARAM_HNSW_SEARCHER_PL, &pl_); + } + if (params.has(PARAM_HNSW_SEARCHER_MAX_SCAN_RATIO)) { params.get(PARAM_HNSW_SEARCHER_MAX_SCAN_RATIO, &max_scan_ratio_); max_scan_num_ = @@ -171,6 +179,8 @@ int HnswContext::update(const ailego::Params ¶ms) { topk_heap_.limit(std::max(topk_, ef_)); } params.get(PARAM_HNSW_STREAMER_EF, &ef_); + params.get(PARAM_HNSW_STREAMER_PO, &po_); + params.get(PARAM_HNSW_STREAMER_PL, &pl_); params.get(PARAM_HNSW_STREAMER_MAX_SCAN_RATIO, &max_scan_ratio_); params.get(PARAM_HNSW_STREAMER_MAX_SCAN_LIMIT, &max_scan_limit_); params.get(PARAM_HNSW_STREAMER_MIN_SCAN_LIMIT, &min_scan_limit_); diff --git a/src/core/algorithm/hnsw/hnsw_context.h b/src/core/algorithm/hnsw/hnsw_context.h index 4a0e868..bc97bbf 100644 --- a/src/core/algorithm/hnsw/hnsw_context.h +++ b/src/core/algorithm/hnsw/hnsw_context.h @@ -314,6 +314,22 @@ class HnswContext : public IndexContext { return ef_; } + inline void set_po(uint32_t v) { + po_ = v; + } + + inline uint32_t po(void) const { + return po_; + } + + inline void set_pl(uint32_t v) { + pl_ = v; + } + + inline uint32_t pl(void) const { + return pl_; + } + inline void set_filter_mode(uint32_t v) { filter_mode_ = v; } @@ -524,6 +540,8 @@ class HnswContext : public IndexContext { uint32_t filter_mode_{VisitFilter::ByteMap}; float negative_probability_{HnswEntity::kDefaultBFNegativeProbability}; uint32_t ef_{HnswEntity::kDefaultEf}; + uint32_t po_{8}; + uint32_t pl_{0}; float max_scan_ratio_{HnswEntity::kDefaultScanRatio}; uint32_t magic_{0U}; std::vector results_{}; diff --git a/src/core/algorithm/hnsw/hnsw_params.h b/src/core/algorithm/hnsw/hnsw_params.h index 4caa148..6cb9e0c 100644 --- a/src/core/algorithm/hnsw/hnsw_params.h +++ b/src/core/algorithm/hnsw/hnsw_params.h @@ -38,6 +38,8 @@ static const std::string PARAM_HNSW_BUILDER_L0_MAX_NEIGHBOR_COUNT_MULTIPLIER( "proxima.hnsw.builder.l0_max_neighbor_count_multiplier"); static const std::string PARAM_HNSW_SEARCHER_EF("proxima.hnsw.searcher.ef"); +static const std::string PARAM_HNSW_SEARCHER_PO("proxima.hnsw.searcher.po"); +static const std::string PARAM_HNSW_SEARCHER_PL("proxima.hnsw.searcher.pl"); static const std::string PARAM_HNSW_SEARCHER_BRUTE_FORCE_THRESHOLD( "proxima.hnsw.searcher.brute_force_threshold"); static const std::string PARAM_HNSW_SEARCHER_NEIGHBORS_IN_MEMORY_ENABLE( @@ -60,6 +62,8 @@ static const std::string PARAM_HNSW_STREAMER_MIN_SCAN_LIMIT( static const std::string PARAM_HNSW_STREAMER_MAX_SCAN_LIMIT( "proxima.hnsw.streamer.max_scan_limit"); static const std::string PARAM_HNSW_STREAMER_EF("proxima.hnsw.streamer.ef"); +static const std::string PARAM_HNSW_STREAMER_PO("proxima.hnsw.streamer.po"); +static const std::string PARAM_HNSW_STREAMER_PL("proxima.hnsw.streamer.pl"); static const std::string PARAM_HNSW_STREAMER_EFCONSTRUCTION( "proxima.hnsw.streamer.efconstruction"); static const std::string PARAM_HNSW_STREAMER_MAX_NEIGHBOR_COUNT( diff --git a/src/core/algorithm/hnsw/hnsw_streamer.cc b/src/core/algorithm/hnsw/hnsw_streamer.cc index c5e78f4..92b2cf7 100644 --- a/src/core/algorithm/hnsw/hnsw_streamer.cc +++ b/src/core/algorithm/hnsw/hnsw_streamer.cc @@ -52,6 +52,8 @@ int HnswStreamer::init(const IndexMeta &imeta, const ailego::Params ¶ms) { params.get(PARAM_HNSW_STREAMER_DOCS_HARD_LIMIT, &docs_hard_limit_); params.get(PARAM_HNSW_STREAMER_EF, &ef_); + params.get(PARAM_HNSW_STREAMER_PO, &po_); + params.get(PARAM_HNSW_STREAMER_PL, &pl_); params.get(PARAM_HNSW_STREAMER_EFCONSTRUCTION, &ef_construction_); params.get(PARAM_HNSW_STREAMER_VISIT_BLOOMFILTER_ENABLE, &bf_enabled_); params.get(PARAM_HNSW_STREAMER_VISIT_BLOOMFILTER_NEGATIVE_PROB, @@ -417,6 +419,8 @@ IndexStreamer::Context::Pointer HnswStreamer::create_context(void) const { return Context::Pointer(); } ctx->set_ef(ef_); + ctx->set_po(po_); + ctx->set_pl(pl_); ctx->set_max_scan_limit(max_scan_limit_); ctx->set_min_scan_limit(min_scan_limit_); ctx->set_max_scan_ratio(max_scan_ratio_); diff --git a/src/core/algorithm/hnsw/hnsw_streamer.h b/src/core/algorithm/hnsw/hnsw_streamer.h index 2568369..047cd6b 100644 --- a/src/core/algorithm/hnsw/hnsw_streamer.h +++ b/src/core/algorithm/hnsw/hnsw_streamer.h @@ -215,6 +215,8 @@ class HnswStreamer : public IndexStreamer { uint32_t upper_max_neighbor_cnt_{HnswEntity::kDefaultUpperMaxNeighborCnt}; uint32_t l0_max_neighbor_cnt_{HnswEntity::kDefaultL0MaxNeighborCnt}; uint32_t ef_{HnswEntity::kDefaultEf}; + uint32_t po_{8}; + uint32_t pl_{0}; uint32_t ef_construction_{HnswEntity::kDefaultEfConstruction}; uint32_t scaling_factor_{HnswEntity::kDefaultScalingFactor}; size_t bruteforce_threshold_{HnswEntity::kDefaultBruteForceThreshold}; diff --git a/src/core/algorithm/vamana/vamana_algorithm.cc b/src/core/algorithm/vamana/vamana_algorithm.cc index dfd4cc6..bf0fe5e 100644 --- a/src/core/algorithm/vamana/vamana_algorithm.cc +++ b/src/core/algorithm/vamana/vamana_algorithm.cc @@ -126,7 +126,7 @@ template void fast_greedy_search(const EntityType &entity, HeapType &pool, VisitFilter &visit, VamanaDistCalculator &dc, uint32_t topk, uint32_t ef, node_id_t entry_point, - uint32_t prefetch_lines) { + uint32_t prefetch_lines, uint32_t prefetch_offset) { const uint32_t max_deg = entity.max_degree(); const uint32_t cap = std::max(topk, ef); pool.reset(static_cast(cap), static_cast(max_deg)); @@ -136,8 +136,6 @@ void fast_greedy_search(const EntityType &entity, HeapType &pool, visit.set_visited(entry_point); pool.push_block(&ep_dist, &entry_point, 1); - static constexpr uint32_t GRAPH_PO = 8; - uint32_t buf_capacity = max_deg; std::vector neighbor_ids(buf_capacity); std::vector dists(buf_capacity); @@ -157,7 +155,7 @@ void fast_greedy_search(const EntityType &entity, HeapType &pool, } const uint32_t po = - std::min(static_cast(neighbors.size()), GRAPH_PO); + std::min(static_cast(neighbors.size()), prefetch_offset); uint32_t unvisited_count = 0; uint32_t i = 0; @@ -200,8 +198,9 @@ template void dual_heap_greedy_search(const EntityType &entity, VamanaContext *ctx, VamanaDistCalculator &dc, node_id_t entry_point, FilterFn &&filter) { - static constexpr uint32_t PREFETCH_BATCH = 2; - static constexpr uint32_t PREFETCH_STEP = 2; + const uint32_t prefetch_offset = ctx->po(); + const uint32_t prefetch_lines = + ctx->pl() > 0 ? ctx->pl() : (entity.vector_size() + 63) / 64; uint32_t buf_capacity = entity.max_degree(); std::vector neighbor_ids(buf_capacity); @@ -273,9 +272,12 @@ void dual_heap_greedy_search(const EntityType &entity, VamanaContext *ctx, neighbor_vec_blocks); if (ailego_unlikely(ret != 0)) break; - for (uint32_t i = 0; - i < std::min(PREFETCH_BATCH * PREFETCH_STEP, unvisited_count); ++i) { - ailego_prefetch(neighbor_vec_blocks[i].data()); + for (uint32_t i = 0; i < std::min(prefetch_offset, unvisited_count); ++i) { + const char *base = + static_cast(neighbor_vec_blocks[i].data()); + for (uint32_t cl = 0; cl < prefetch_lines; ++cl) { + ailego_prefetch(base + cl * 64); + } } // Batch distance computation (reuse pre-allocated buffers). @@ -316,17 +318,8 @@ void VamanaAlgorithm::greedy_search(node_id_t entry_point, const IndexFilter &index_filter = static_cast(ctx)->filter(); - // Number of cache lines per vector (e.g. 2 for dim=128). - // Used by both the fallback candidates/filter path and the fast helpers. - uint32_t prefetch_lines = (dc.dimension() + 63) / 64; - if constexpr (std::is_same_v) { - // Contiguous flat array stride is already 64B-aligned. Use it so that - // prefetch does not overshoot into the next vector. - size_t stride = entity.vector_stride(); - if (stride > 0) { - prefetch_lines = static_cast(stride / 64); - } - } + const uint32_t prefetch_lines = + ctx->pl() > 0 ? ctx->pl() : (entity.vector_size() + 63) / 64; if (!use_pool || index_filter.is_valid()) { // Fallback path used by add_node (use_pool=false) and filtered search. @@ -362,12 +355,12 @@ void VamanaAlgorithm::greedy_search(node_id_t entry_point, if (avx2_ok) { auto &bpool = ctx->block_pool(); fast_greedy_search(entity, bpool, visit, dc, topk_v, ef_v, entry_point, - prefetch_lines); + prefetch_lines, ctx->po()); copy_pool_to_topk(bpool, topk_heap); } else { auto &lpool = ctx->pool(); fast_greedy_search(entity, lpool, visit, dc, topk_v, ef_v, entry_point, - prefetch_lines); + prefetch_lines, ctx->po()); copy_pool_to_topk(lpool, topk_heap); } } else { diff --git a/src/core/algorithm/vamana/vamana_context.cc b/src/core/algorithm/vamana/vamana_context.cc index a1483ec..ee52c51 100644 --- a/src/core/algorithm/vamana/vamana_context.cc +++ b/src/core/algorithm/vamana/vamana_context.cc @@ -119,6 +119,12 @@ int VamanaContext::update(const ailego::Params ¶ms) { params.get(PARAM_VAMANA_STREAMER_EF, &ef); ef_ = ef; topk_heap_.limit(std::max(topk_, ef_)); + uint32_t po = po_; + params.get(PARAM_VAMANA_STREAMER_PO, &po); + po_ = po; + uint32_t pl = pl_; + params.get(PARAM_VAMANA_STREAMER_PL, &pl); + pl_ = pl; return 0; } diff --git a/src/core/algorithm/vamana/vamana_context.h b/src/core/algorithm/vamana/vamana_context.h index b3cacae..a576353 100644 --- a/src/core/algorithm/vamana/vamana_context.h +++ b/src/core/algorithm/vamana/vamana_context.h @@ -177,6 +177,21 @@ class VamanaContext : public IndexContext { inline uint32_t ef() const { return ef_; } + inline void set_po(uint32_t v) { + po_ = v; + } + + inline uint32_t po() const { + return po_; + } + + inline void set_pl(uint32_t v) { + pl_ = v; + } + + inline uint32_t pl() const { + return pl_; + } inline void set_max_scan_ratio(float v) { max_scan_ratio_ = v; } @@ -296,6 +311,8 @@ class VamanaContext : public IndexContext { uint32_t reserve_max_doc_cnt_{kMinReserveDocCnt}; uint32_t topk_{0}; uint32_t ef_{VamanaEntity::kDefaultEf}; + uint32_t po_{8}; + uint32_t pl_{0}; float max_scan_ratio_{VamanaEntity::kDefaultScanRatio}; size_t max_scan_limit_{VamanaEntity::kDefaultMaxScanLimit}; size_t min_scan_limit_{VamanaEntity::kDefaultMinScanLimit}; diff --git a/src/core/algorithm/vamana/vamana_params.h b/src/core/algorithm/vamana/vamana_params.h index 68ffaf8..4d7df96 100644 --- a/src/core/algorithm/vamana/vamana_params.h +++ b/src/core/algorithm/vamana/vamana_params.h @@ -48,6 +48,8 @@ static const std::string PARAM_VAMANA_STREAMER_ALPHA( static const std::string PARAM_VAMANA_STREAMER_MAX_OCCLUSION_SIZE( "proxima.vamana.streamer.max_occlusion_size"); static const std::string PARAM_VAMANA_STREAMER_EF("proxima.vamana.streamer.ef"); +static const std::string PARAM_VAMANA_STREAMER_PO("proxima.vamana.streamer.po"); +static const std::string PARAM_VAMANA_STREAMER_PL("proxima.vamana.streamer.pl"); static const std::string PARAM_VAMANA_STREAMER_BRUTE_FORCE_THRESHOLD( "proxima.vamana.streamer.brute_force_threshold"); static const std::string PARAM_VAMANA_STREAMER_MAX_SCAN_RATIO( diff --git a/src/core/interface/index_factory.cc b/src/core/interface/index_factory.cc index bffceee..5d9dea1 100644 --- a/src/core/interface/index_factory.cc +++ b/src/core/interface/index_factory.cc @@ -166,6 +166,12 @@ std::string IndexFactory::QueryParamSerializeToJson(const QueryParamType ¶m, if (!omit_empty_value || param.ef_search != 0) { json_obj.set("ef_search", ailego::JsonValue(param.ef_search)); } + if (!omit_empty_value || param.prefetch_offset != 0) { + json_obj.set("prefetch_offset", ailego::JsonValue(param.prefetch_offset)); + } + if (!omit_empty_value || param.prefetch_lines != 0) { + json_obj.set("prefetch_lines", ailego::JsonValue(param.prefetch_lines)); + } index_type = IndexType::kHNSW; } else if constexpr (std::is_same_v) { if (!omit_empty_value || param.nprobe != 0) { @@ -185,6 +191,12 @@ std::string IndexFactory::QueryParamSerializeToJson(const QueryParamType ¶m, if (!omit_empty_value || param.ef_search != 0) { json_obj.set("ef_search", ailego::JsonValue(param.ef_search)); } + if (!omit_empty_value || param.prefetch_offset != 0) { + json_obj.set("prefetch_offset", ailego::JsonValue(param.prefetch_offset)); + } + if (!omit_empty_value || param.prefetch_lines != 0) { + json_obj.set("prefetch_lines", ailego::JsonValue(param.prefetch_lines)); + } index_type = IndexType::kVamana; } @@ -268,6 +280,16 @@ typename QueryParamType::Pointer IndexFactory::QueryParamDeserializeFromJson( LOG_ERROR("Failed to deserialize ef_search"); return nullptr; } + if (!extract_value_from_json(json_obj, "prefetch_offset", + param->prefetch_offset, tmp_json_value)) { + LOG_ERROR("Failed to deserialize prefetch_offset"); + return nullptr; + } + if (!extract_value_from_json(json_obj, "prefetch_lines", + param->prefetch_lines, tmp_json_value)) { + LOG_ERROR("Failed to deserialize prefetch_lines"); + return nullptr; + } return param; } else if (index_type == IndexType::kIVF) { auto param = std::make_shared(); @@ -301,6 +323,16 @@ typename QueryParamType::Pointer IndexFactory::QueryParamDeserializeFromJson( LOG_ERROR("Failed to deserialize ef_search"); return nullptr; } + if (!extract_value_from_json(json_obj, "prefetch_offset", + param->prefetch_offset, tmp_json_value)) { + LOG_ERROR("Failed to deserialize prefetch_offset"); + return nullptr; + } + if (!extract_value_from_json(json_obj, "prefetch_lines", + param->prefetch_lines, tmp_json_value)) { + LOG_ERROR("Failed to deserialize prefetch_lines"); + return nullptr; + } return param; } else { LOG_ERROR("Unsupported index type: %s", @@ -319,6 +351,16 @@ typename QueryParamType::Pointer IndexFactory::QueryParamDeserializeFromJson( LOG_ERROR("Failed to deserialize ef_search"); return nullptr; } + if (!extract_value_from_json(json_obj, "prefetch_offset", + param->prefetch_offset, tmp_json_value)) { + LOG_ERROR("Failed to deserialize prefetch_offset"); + return nullptr; + } + if (!extract_value_from_json(json_obj, "prefetch_lines", + param->prefetch_lines, tmp_json_value)) { + LOG_ERROR("Failed to deserialize prefetch_lines"); + return nullptr; + } } else if constexpr (std::is_same_v) { if (!extract_value_from_json(json_obj, "nprobe", param->nprobe, tmp_json_value)) { @@ -337,6 +379,16 @@ typename QueryParamType::Pointer IndexFactory::QueryParamDeserializeFromJson( LOG_ERROR("Failed to deserialize ef_search"); return nullptr; } + if (!extract_value_from_json(json_obj, "prefetch_offset", + param->prefetch_offset, tmp_json_value)) { + LOG_ERROR("Failed to deserialize prefetch_offset"); + return nullptr; + } + if (!extract_value_from_json(json_obj, "prefetch_lines", + param->prefetch_lines, tmp_json_value)) { + LOG_ERROR("Failed to deserialize prefetch_lines"); + return nullptr; + } } else { LOG_ERROR("Unsupported index type: %s", magic_enum::enum_name(index_type).data()); diff --git a/src/core/interface/indexes/hnsw_index.cc b/src/core/interface/indexes/hnsw_index.cc index 816665b..0744da3 100644 --- a/src/core/interface/indexes/hnsw_index.cc +++ b/src/core/interface/indexes/hnsw_index.cc @@ -128,6 +128,12 @@ int HNSWIndex::_prepare_for_search( const int real_search_ef = std::max(1u, std::min(2048u, hnsw_search_param->ef_search)); params.set(core::PARAM_HNSW_STREAMER_EF, real_search_ef); + const uint32_t real_search_po = + std::min(256u, hnsw_search_param->prefetch_offset); + params.set(core::PARAM_HNSW_STREAMER_PO, real_search_po); + const uint32_t real_search_pl = + std::min(256u, hnsw_search_param->prefetch_lines); + params.set(core::PARAM_HNSW_STREAMER_PL, real_search_pl); context->update(params); return 0; } diff --git a/src/core/interface/indexes/vamana_index.cc b/src/core/interface/indexes/vamana_index.cc index 297cf37..8591ff9 100644 --- a/src/core/interface/indexes/vamana_index.cc +++ b/src/core/interface/indexes/vamana_index.cc @@ -91,6 +91,12 @@ int VamanaIndex::_prepare_for_search( const uint32_t real_search_ef = std::max(1u, std::min(2048u, vamana_search_param->ef_search)); params.set(core::PARAM_VAMANA_STREAMER_EF, real_search_ef); + const uint32_t real_search_po = + std::min(256u, vamana_search_param->prefetch_offset); + params.set(core::PARAM_VAMANA_STREAMER_PO, real_search_po); + const uint32_t real_search_pl = + std::min(256u, vamana_search_param->prefetch_lines); + params.set(core::PARAM_VAMANA_STREAMER_PL, real_search_pl); context->update(params); return 0; } diff --git a/src/db/index/column/vector_column/engine_helper.hpp b/src/db/index/column/vector_column/engine_helper.hpp index 27dd0f1..f7b727b 100644 --- a/src/db/index/column/vector_column/engine_helper.hpp +++ b/src/db/index/column/vector_column/engine_helper.hpp @@ -162,6 +162,10 @@ class ProximaEngineHelper { auto db_hnsw_query_params = dynamic_cast( query_params.query_params.get()); hnsw_query_param->ef_search = db_hnsw_query_params->ef(); + hnsw_query_param->prefetch_offset = + db_hnsw_query_params->prefetch_offset(); + hnsw_query_param->prefetch_lines = + db_hnsw_query_params->prefetch_lines(); } return std::move(hnsw_query_param); } @@ -238,6 +242,10 @@ class ProximaEngineHelper { query_params.query_params.get()); vamana_query_param->ef_search = static_cast(db_vamana_query_params->ef_search()); + vamana_query_param->prefetch_offset = + db_vamana_query_params->prefetch_offset(); + vamana_query_param->prefetch_lines = + db_vamana_query_params->prefetch_lines(); } return std::move(vamana_query_param); } diff --git a/src/include/zvec/c_api.h b/src/include/zvec/c_api.h index 500265f..d02335c 100644 --- a/src/include/zvec/c_api.h +++ b/src/include/zvec/c_api.h @@ -826,6 +826,7 @@ typedef uint32_t zvec_index_type_t; #define ZVEC_INDEX_TYPE_HNSW 1 #define ZVEC_INDEX_TYPE_IVF 2 #define ZVEC_INDEX_TYPE_FLAT 3 +#define ZVEC_INDEX_TYPE_VAMANA 6 #define ZVEC_INDEX_TYPE_INVERT 10 #define ZVEC_INDEX_TYPE_FTS 11 @@ -986,6 +987,37 @@ zvec_index_params_get_hnsw_m(const zvec_index_params_t *params); ZVEC_EXPORT int ZVEC_CALL zvec_index_params_get_hnsw_ef_construction(const zvec_index_params_t *params); +/** + * @brief Set Vamana specific parameters + * @param params Index parameters (must be VAMANA type) + * @param max_degree Maximum out-degree (R) of every node (default: 64) + * @param search_list_size Candidate list size during construction (default: + * 100) + * @param alpha RobustPrune alpha factor (default: 1.2) + * @param saturate_graph Force every node to reach max_degree (default: false) + * @param use_contiguous_memory Allocate contiguous memory arena (default: + * false) + * @return ZVEC_OK on success, error code on failure + */ +ZVEC_EXPORT zvec_error_code_t ZVEC_CALL zvec_index_params_set_vamana_params( + zvec_index_params_t *params, int max_degree, int search_list_size, + float alpha, bool saturate_graph, bool use_contiguous_memory); + +/** + * @brief Get Vamana parameters (all at once) + * @param params Index parameters (must be VAMANA type) + * @param[out] out_max_degree Maximum out-degree + * @param[out] out_search_list_size Construction candidate list size + * @param[out] out_alpha RobustPrune alpha factor + * @param[out] out_saturate_graph Whether saturate graph is enabled + * @param[out] out_use_contiguous_memory Whether contiguous memory is enabled + * @return ZVEC_OK on success, error code on failure + */ +ZVEC_EXPORT zvec_error_code_t ZVEC_CALL zvec_index_params_get_vamana_params( + const zvec_index_params_t *params, int *out_max_degree, + int *out_search_list_size, float *out_alpha, bool *out_saturate_graph, + bool *out_use_contiguous_memory); + /** * @brief Set IVF specific parameters * @param params Index parameters (must be IVF type) @@ -1102,6 +1134,16 @@ typedef struct zvec_flat_query_params_t zvec_flat_query_params_t; */ typedef struct zvec_fts_query_params_t zvec_fts_query_params_t; +/** + * @brief Vamana query parameters handle (opaque pointer) + * + * Internally maps to zvec::VamanaQueryParams* (raw pointer). + * Created by zvec_query_params_vamana_create() and destroyed by + * zvec_query_params_vamana_destroy(). Caller owns the pointer and must + * explicitly destroy it. + */ +typedef struct zvec_vamana_query_params_t zvec_vamana_query_params_t; + // ============================================================================= // Query Structures (Opaque Pointer Pattern) @@ -1490,6 +1532,99 @@ zvec_query_params_fts_set_default_operator(zvec_fts_query_params_t *params, ZVEC_EXPORT const char *ZVEC_CALL zvec_query_params_fts_get_default_operator( const zvec_fts_query_params_t *params); +// ----------------------------------------------------------------------------- +// zvec_vamana_query_params_t (Vamana Query Parameters) +// ----------------------------------------------------------------------------- + +/** + * @brief Create Vamana query parameters + * @param ef_search Search-time candidate list size (default: 200) + * @param radius Search radius (default: 0.0) + * @param is_linear Whether linear search (default: false) + * @param is_using_refiner Whether using refiner (default: false) + * @return zvec_vamana_query_params_t* Pointer to the newly created Vamana + * query parameters + */ +ZVEC_EXPORT zvec_vamana_query_params_t *ZVEC_CALL +zvec_query_params_vamana_create(int ef_search, float radius, bool is_linear, + bool is_using_refiner); + +/** + * @brief Destroy Vamana query parameters + * @param params Vamana query parameters pointer + */ +ZVEC_EXPORT void ZVEC_CALL +zvec_query_params_vamana_destroy(zvec_vamana_query_params_t *params); + +/** + * @brief Set search-time candidate list size + * @param params Vamana query parameters pointer + * @param ef_search Candidate list size + * @return zvec_error_code_t Error code + */ +ZVEC_EXPORT zvec_error_code_t ZVEC_CALL zvec_query_params_vamana_set_ef_search( + zvec_vamana_query_params_t *params, int ef_search); + +/** + * @brief Get search-time candidate list size + * @param params Vamana query parameters pointer + * @return int Candidate list size + */ +ZVEC_EXPORT int ZVEC_CALL zvec_query_params_vamana_get_ef_search( + const zvec_vamana_query_params_t *params); + +/** + * @brief Set search radius + * @param params Vamana query parameters pointer + * @param radius Search radius + * @return zvec_error_code_t Error code + */ +ZVEC_EXPORT zvec_error_code_t ZVEC_CALL zvec_query_params_vamana_set_radius( + zvec_vamana_query_params_t *params, float radius); + +/** + * @brief Get search radius + * @param params Vamana query parameters pointer + * @return float Search radius + */ +ZVEC_EXPORT float ZVEC_CALL +zvec_query_params_vamana_get_radius(const zvec_vamana_query_params_t *params); + +/** + * @brief Set linear search mode + * @param params Vamana query parameters pointer + * @param is_linear Whether linear search + * @return zvec_error_code_t Error code + */ +ZVEC_EXPORT zvec_error_code_t ZVEC_CALL zvec_query_params_vamana_set_is_linear( + zvec_vamana_query_params_t *params, bool is_linear); + +/** + * @brief Get linear search mode + * @param params Vamana query parameters pointer + * @return bool Whether linear search + */ +ZVEC_EXPORT bool ZVEC_CALL zvec_query_params_vamana_get_is_linear( + const zvec_vamana_query_params_t *params); + +/** + * @brief Set whether to use refiner + * @param params Vamana query parameters pointer + * @param is_using_refiner Whether to use refiner + * @return zvec_error_code_t Error code + */ +ZVEC_EXPORT zvec_error_code_t ZVEC_CALL +zvec_query_params_vamana_set_is_using_refiner( + zvec_vamana_query_params_t *params, bool is_using_refiner); + +/** + * @brief Get whether to use refiner + * @param params Vamana query parameters pointer + * @return bool Whether to use refiner + */ +ZVEC_EXPORT bool ZVEC_CALL zvec_query_params_vamana_get_is_using_refiner( + const zvec_vamana_query_params_t *params); + // ----------------------------------------------------------------------------- // zvec_vector_query_t (Vector Query) // ----------------------------------------------------------------------------- @@ -1672,6 +1807,15 @@ ZVEC_EXPORT zvec_error_code_t ZVEC_CALL zvec_vector_query_set_flat_params( ZVEC_EXPORT zvec_error_code_t ZVEC_CALL zvec_vector_query_set_fts_params( zvec_vector_query_t *query, zvec_fts_query_params_t *fts_params); +/** + * @brief Set Vamana query parameters for vector query + * @param query Vector query pointer + * @param vamana_params Vamana query parameters pointer + * @return zvec_error_code_t Error code + */ +ZVEC_EXPORT zvec_error_code_t ZVEC_CALL zvec_vector_query_set_vamana_params( + zvec_vector_query_t *query, zvec_vamana_query_params_t *vamana_params); + // ----------------------------------------------------------------------------- // zvec_fts_t (FTS query payload) // ----------------------------------------------------------------------------- @@ -1944,6 +2088,17 @@ ZVEC_EXPORT zvec_error_code_t ZVEC_CALL zvec_group_by_vector_query_set_flat_params( zvec_group_by_vector_query_t *query, zvec_flat_query_params_t *flat_params); +/** + * @brief Set Vamana query parameters (takes ownership) + * @param query Group by vector query pointer + * @param vamana_params Vamana query parameters pointer + * @return zvec_error_code_t Error code + */ +ZVEC_EXPORT zvec_error_code_t ZVEC_CALL +zvec_group_by_vector_query_set_vamana_params( + zvec_group_by_vector_query_t *query, + zvec_vamana_query_params_t *vamana_params); + // ----------------------------------------------------------------------------- // Rerank Strategy (set on MultiQuery) // ----------------------------------------------------------------------------- @@ -2194,6 +2349,16 @@ ZVEC_EXPORT zvec_error_code_t ZVEC_CALL zvec_sub_query_set_ivf_params( */ ZVEC_EXPORT zvec_error_code_t ZVEC_CALL zvec_sub_query_set_flat_params( zvec_sub_query_t *query, zvec_flat_query_params_t *flat_params); + +/** + * @brief Set Vamana query parameters (takes ownership) + * @param query Sub-query pointer + * @param vamana_params Vamana query parameters pointer + * @return zvec_error_code_t Error code + */ +ZVEC_EXPORT zvec_error_code_t ZVEC_CALL zvec_sub_query_set_vamana_params( + zvec_sub_query_t *query, zvec_vamana_query_params_t *vamana_params); + // ============================================================================= // Collection Options and Statistics (Opaque Pointer Pattern) // ============================================================================= diff --git a/src/include/zvec/core/interface/constants.h b/src/include/zvec/core/interface/constants.h index 91bf087..1f39daa 100644 --- a/src/include/zvec/core/interface/constants.h +++ b/src/include/zvec/core/interface/constants.h @@ -22,6 +22,8 @@ constexpr static uint32_t kDefaultHnswEfConstruction = 500; constexpr static uint32_t kDefaultHnswNeighborCnt = 50; constexpr static uint32_t kDefaultHnswEfSearch = 300; +constexpr static uint32_t kDefaultPrefetchOffset = 8; +constexpr static uint32_t kDefaultPrefetchLines = 0; constexpr static uint32_t kDefaultVamanaMaxDegree = 64; constexpr static uint32_t kDefaultVamanaSearchListSize = 100; diff --git a/src/include/zvec/core/interface/index_param.h b/src/include/zvec/core/interface/index_param.h index f14ebef..49051bc 100644 --- a/src/include/zvec/core/interface/index_param.h +++ b/src/include/zvec/core/interface/index_param.h @@ -185,6 +185,8 @@ struct HNSWQueryParam : public BaseIndexQueryParam { using Pointer = std::shared_ptr; uint32_t ef_search = kDefaultHnswEfSearch; + uint32_t prefetch_offset = kDefaultPrefetchOffset; + uint32_t prefetch_lines = kDefaultPrefetchLines; BaseIndexQueryParam::Pointer Clone() const override { return std::make_shared(*this); @@ -377,6 +379,8 @@ struct VamanaQueryParam : public BaseIndexQueryParam { using Pointer = std::shared_ptr; uint32_t ef_search = kDefaultVamanaEfSearch; + uint32_t prefetch_offset = kDefaultPrefetchOffset; + uint32_t prefetch_lines = kDefaultPrefetchLines; BaseIndexQueryParam::Pointer Clone() const override { return std::make_shared(*this); diff --git a/src/include/zvec/core/interface/index_param_builders.h b/src/include/zvec/core/interface/index_param_builders.h index 75fc67b..236e236 100644 --- a/src/include/zvec/core/interface/index_param_builders.h +++ b/src/include/zvec/core/interface/index_param_builders.h @@ -354,6 +354,16 @@ class HNSWQueryParamBuilder return *this; } + HNSWQueryParamBuilder &with_prefetch_offset(uint32_t prefetch_offset) { + m_param.prefetch_offset = prefetch_offset; + return *this; + } + + HNSWQueryParamBuilder &with_prefetch_lines(uint32_t prefetch_lines) { + m_param.prefetch_lines = prefetch_lines; + return *this; + } + HNSWQueryParam::Pointer build() { return std::make_shared(std::move(m_param)); } @@ -419,6 +429,16 @@ class VamanaQueryParamBuilder return *this; } + VamanaQueryParamBuilder &with_prefetch_offset(uint32_t prefetch_offset) { + m_param.prefetch_offset = prefetch_offset; + return *this; + } + + VamanaQueryParamBuilder &with_prefetch_lines(uint32_t prefetch_lines) { + m_param.prefetch_lines = prefetch_lines; + return *this; + } + VamanaQueryParam::Pointer build() { return std::make_shared(std::move(m_param)); } diff --git a/src/include/zvec/db/query_params.h b/src/include/zvec/db/query_params.h index ca5cebb..302f9a8 100644 --- a/src/include/zvec/db/query_params.h +++ b/src/include/zvec/db/query_params.h @@ -71,10 +71,15 @@ class QueryParams { class HnswQueryParams : public QueryParams { public: - HnswQueryParams(int ef = core_interface::kDefaultHnswEfSearch, - float radius = 0.0f, bool is_linear = false, - bool is_using_refiner = false) - : QueryParams(IndexType::HNSW), ef_(ef) { + HnswQueryParams( + int ef = core_interface::kDefaultHnswEfSearch, float radius = 0.0f, + bool is_linear = false, bool is_using_refiner = false, + uint32_t prefetch_offset = core_interface::kDefaultPrefetchOffset, + uint32_t prefetch_lines = core_interface::kDefaultPrefetchLines) + : QueryParams(IndexType::HNSW), + ef_(ef), + prefetch_offset_(prefetch_offset), + prefetch_lines_(prefetch_lines) { set_radius(radius); set_is_linear(is_linear); set_is_using_refiner(is_using_refiner); @@ -90,8 +95,26 @@ class HnswQueryParams : public QueryParams { ef_ = ef; } + uint32_t prefetch_offset() const { + return prefetch_offset_; + } + + void set_prefetch_offset(uint32_t prefetch_offset) { + prefetch_offset_ = prefetch_offset; + } + + uint32_t prefetch_lines() const { + return prefetch_lines_; + } + + void set_prefetch_lines(uint32_t prefetch_lines) { + prefetch_lines_ = prefetch_lines; + } + private: int ef_; + uint32_t prefetch_offset_{core_interface::kDefaultPrefetchOffset}; + uint32_t prefetch_lines_{core_interface::kDefaultPrefetchLines}; }; class IVFQueryParams : public QueryParams { @@ -197,10 +220,16 @@ class DiskAnnQueryParams : public QueryParams { class VamanaQueryParams : public QueryParams { public: - VamanaQueryParams(int ef_search = core_interface::kDefaultVamanaEfSearch, - float radius = 0.0f, bool is_linear = false, - bool is_using_refiner = false) - : QueryParams(IndexType::VAMANA), ef_search_(ef_search) { + VamanaQueryParams( + int ef_search = core_interface::kDefaultVamanaEfSearch, + float radius = 0.0f, bool is_linear = false, + bool is_using_refiner = false, + uint32_t prefetch_offset = core_interface::kDefaultPrefetchOffset, + uint32_t prefetch_lines = core_interface::kDefaultPrefetchLines) + : QueryParams(IndexType::VAMANA), + ef_search_(ef_search), + prefetch_offset_(prefetch_offset), + prefetch_lines_(prefetch_lines) { set_radius(radius); set_is_linear(is_linear); set_is_using_refiner(is_using_refiner); @@ -216,8 +245,26 @@ class VamanaQueryParams : public QueryParams { ef_search_ = ef_search; } + uint32_t prefetch_offset() const { + return prefetch_offset_; + } + + void set_prefetch_offset(uint32_t prefetch_offset) { + prefetch_offset_ = prefetch_offset; + } + + uint32_t prefetch_lines() const { + return prefetch_lines_; + } + + void set_prefetch_lines(uint32_t prefetch_lines) { + prefetch_lines_ = prefetch_lines; + } + private: int ef_search_; + uint32_t prefetch_offset_{core_interface::kDefaultPrefetchOffset}; + uint32_t prefetch_lines_{core_interface::kDefaultPrefetchLines}; }; class FtsQueryParams : public QueryParams { diff --git a/tests/c/c_api_test.c b/tests/c/c_api_test.c index 5a0c3a1..8670ff8 100644 --- a/tests/c/c_api_test.c +++ b/tests/c/c_api_test.c @@ -3695,16 +3695,46 @@ void test_query_params_functions(void) { is_using_refiner = zvec_query_params_flat_get_is_using_refiner(flat_params); TEST_ASSERT(is_using_refiner == true); + // Test Vamana query parameters + zvec_vamana_query_params_t *vamana_params = + zvec_query_params_vamana_create(256, 0.3f, false, true); + TEST_ASSERT(vamana_params != NULL); + + TEST_ASSERT(zvec_query_params_vamana_get_ef_search(vamana_params) == 256); + TEST_ASSERT(zvec_query_params_vamana_get_radius(vamana_params) == 0.3f); + TEST_ASSERT(zvec_query_params_vamana_get_is_linear(vamana_params) == false); + TEST_ASSERT(zvec_query_params_vamana_get_is_using_refiner(vamana_params) == + true); + + // Vamana set/get all parameters + err = zvec_query_params_vamana_set_ef_search(vamana_params, 512); + TEST_ASSERT(err == ZVEC_OK); + TEST_ASSERT(zvec_query_params_vamana_get_ef_search(vamana_params) == 512); + + err = zvec_query_params_vamana_set_radius(vamana_params, 0.5f); + TEST_ASSERT(err == ZVEC_OK); + TEST_ASSERT(zvec_query_params_vamana_get_radius(vamana_params) == 0.5f); + + err = zvec_query_params_vamana_set_is_linear(vamana_params, true); + TEST_ASSERT(err == ZVEC_OK); + TEST_ASSERT(zvec_query_params_vamana_get_is_linear(vamana_params) == true); + + err = zvec_query_params_vamana_set_is_using_refiner(vamana_params, false); + TEST_ASSERT(err == ZVEC_OK); + TEST_ASSERT(zvec_query_params_vamana_get_is_using_refiner(vamana_params) == + false); + // Test destruction of valid parameters zvec_query_params_hnsw_destroy(hnsw_params); zvec_query_params_ivf_destroy(ivf_params); zvec_query_params_flat_destroy(flat_params); - + zvec_query_params_vamana_destroy(vamana_params); // Test boundary cases - null pointer handling zvec_query_params_hnsw_destroy(NULL); zvec_query_params_ivf_destroy(NULL); zvec_query_params_flat_destroy(NULL); + zvec_query_params_vamana_destroy(NULL); // Test null pointer handling for setters err = zvec_query_params_hnsw_set_radius(NULL, 0.5f); @@ -3713,6 +3743,8 @@ void test_query_params_functions(void) { TEST_ASSERT(err == ZVEC_ERROR_INVALID_ARGUMENT); err = zvec_query_params_flat_set_radius(NULL, 0.5f); TEST_ASSERT(err == ZVEC_ERROR_INVALID_ARGUMENT); + err = zvec_query_params_vamana_set_ef_search(NULL, 100); + TEST_ASSERT(err == ZVEC_ERROR_INVALID_ARGUMENT); // Test default values for getters with NULL TEST_ASSERT(zvec_query_params_hnsw_get_radius(NULL) == 0.0f); @@ -3724,6 +3756,10 @@ void test_query_params_functions(void) { TEST_ASSERT(zvec_query_params_hnsw_get_is_using_refiner(NULL) == false); TEST_ASSERT(zvec_query_params_ivf_get_is_using_refiner(NULL) == false); TEST_ASSERT(zvec_query_params_flat_get_is_using_refiner(NULL) == false); + TEST_ASSERT(zvec_query_params_vamana_get_ef_search(NULL) == 200); + TEST_ASSERT(zvec_query_params_vamana_get_radius(NULL) == 0.0f); + TEST_ASSERT(zvec_query_params_vamana_get_is_linear(NULL) == false); + TEST_ASSERT(zvec_query_params_vamana_get_is_using_refiner(NULL) == false); TEST_END(); } @@ -4972,11 +5008,50 @@ void test_index_params_creation_functions(void) { TEST_ASSERT(enable_range_opt == true); TEST_ASSERT(enable_wildcard == false); + // Test Vamana parameters using new API + zvec_index_params_t *vamana_params = + zvec_index_params_create(ZVEC_INDEX_TYPE_VAMANA); + TEST_ASSERT(vamana_params != NULL); + TEST_ASSERT(zvec_index_params_get_type(vamana_params) == + ZVEC_INDEX_TYPE_VAMANA); + TEST_ASSERT(zvec_index_params_get_metric_type(vamana_params) == + ZVEC_METRIC_TYPE_L2); + + int max_degree, search_list_size; + float alpha; + bool saturate_graph, use_contiguous_memory; + zvec_error_code_t verr; + + // Set and get Vamana params + verr = zvec_index_params_set_vamana_params(vamana_params, 128, 200, 1.5f, + true, true); + TEST_ASSERT(verr == ZVEC_OK); + verr = zvec_index_params_get_vamana_params( + vamana_params, &max_degree, &search_list_size, &alpha, &saturate_graph, + &use_contiguous_memory); + TEST_ASSERT(verr == ZVEC_OK); + TEST_ASSERT(max_degree == 128); + TEST_ASSERT(search_list_size == 200); + TEST_ASSERT(alpha == 1.5f); + TEST_ASSERT(saturate_graph == true); + TEST_ASSERT(use_contiguous_memory == true); + + // Set metric and quantize type + zvec_index_params_set_metric_type(vamana_params, ZVEC_METRIC_TYPE_COSINE); + TEST_ASSERT(zvec_index_params_get_metric_type(vamana_params) == + ZVEC_METRIC_TYPE_COSINE); + + // Test type mismatch: set_vamana_params on non-Vamana params should fail + verr = zvec_index_params_set_vamana_params(hnsw_params, 64, 100, 1.2f, false, + false); + TEST_ASSERT(verr == ZVEC_ERROR_INVALID_ARGUMENT); + // Cleanup zvec_index_params_destroy(hnsw_params); zvec_index_params_destroy(ivf_params); zvec_index_params_destroy(flat_params); zvec_index_params_destroy(invert_params); + zvec_index_params_destroy(vamana_params); TEST_END(); }