Skip to content

Commit 90f3b24

Browse files
committed
add: WIP Phase 3 first pass; Python API
1 parent ceecc65 commit 90f3b24

4 files changed

Lines changed: 182 additions & 34 deletions

File tree

apis/python/src/tiledb/vector_search/type_erased_module.cc

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -436,13 +436,16 @@ void init_type_erased_module(py::module_& m) {
436436
[](IndexVamana& index,
437437
const FeatureVectorArray& vectors,
438438
size_t k,
439-
uint32_t l_search) {
440-
auto r = index.query(vectors, k, l_search);
439+
uint32_t l_search,
440+
std::optional<std::unordered_set<uint32_t>> query_filter =
441+
std::nullopt) {
442+
auto r = index.query(vectors, k, l_search, query_filter);
441443
return make_python_pair(std::move(r));
442444
},
443445
py::arg("vectors"),
444446
py::arg("k"),
445-
py::arg("l_search"))
447+
py::arg("l_search"),
448+
py::arg("query_filter") = std::nullopt)
446449
.def(
447450
"write_index",
448451
[](IndexVamana& index,

apis/python/src/tiledb/vector_search/vamana_index.py

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
88
Singh, Aditi, et al. FreshDiskANN: A Fast and Accurate Graph-Based ANN Index for Streaming Similarity Search. arXiv:2105.09613, arXiv, 20 May 2021, http://arxiv.org/abs/2105.09613.
99
10-
Gollapudi, Siddharth, et al. Filtered-DiskANN: Graph Algorithms for Approximate Nearest Neighbor Search with Filters. Proceedings of the ACM Web Conference 2023, ACM, 2023, pp. 3406-16, https://doi.org/10.1145/3543507.3583552.
10+
Gollapudi, Siddharth, et al. "Filtered-DiskANN: Graph Algorithms for Approximate Nearest Neighbor Search with Filters." Proceedings of the ACM Web Conference 2023, ACM, 2023, pp. 3406-16, https://doi.org/10.1145/3543507.3583552.
1111
```
1212
"""
13+
import json
14+
import re
1315
import warnings
14-
from typing import Any, Mapping
16+
from typing import Any, Mapping, Optional, Set
1517

1618
import numpy as np
1719

@@ -25,6 +27,55 @@
2527
from tiledb.vector_search.utils import MAX_UINT64
2628
from tiledb.vector_search.utils import to_temporal_policy
2729

30+
31+
def _parse_where_clause(where: str, label_enumeration: dict) -> Set[int]:
32+
"""
33+
Parse a simple where clause and return a set of label IDs.
34+
35+
Supports basic equality conditions like: "label_col == 'value'"
36+
37+
Parameters
38+
----------
39+
where : str
40+
The where clause string to parse
41+
label_enumeration : dict
42+
Mapping from label strings to enumeration IDs
43+
44+
Returns
45+
-------
46+
Set[int]
47+
Set of label IDs matching the where clause
48+
49+
Raises
50+
------
51+
ValueError
52+
If the where clause is invalid or references non-existent labels
53+
"""
54+
# Simple pattern for: column_name == 'value'
55+
# We support single or double quotes
56+
pattern = r"\s*\w+\s*==\s*['\"]([^'\"]+)['\"]\s*"
57+
match = re.match(pattern, where.strip())
58+
59+
if not match:
60+
raise ValueError(
61+
f"Invalid where clause: '{where}'. "
62+
"Expected format: \"label_col == 'value'\""
63+
)
64+
65+
label_value = match.group(1)
66+
67+
# Check if the label exists in the enumeration
68+
if label_value not in label_enumeration:
69+
available_labels = ", ".join(sorted(label_enumeration.keys()))
70+
raise ValueError(
71+
f"Label '{label_value}' not found in index. "
72+
f"Available labels: {available_labels}"
73+
)
74+
75+
# Return the enumeration ID for this label
76+
label_id = label_enumeration[label_value]
77+
return {label_id}
78+
2879
INDEX_TYPE = "VAMANA"
2980

3081
L_BUILD_DEFAULT = 100
@@ -94,6 +145,7 @@ def query_internal(
94145
queries: np.ndarray,
95146
k: int = 10,
96147
l_search: Optional[int] = L_SEARCH_DEFAULT,
148+
where: Optional[str] = None,
97149
**kwargs,
98150
):
99151
"""
@@ -108,6 +160,9 @@ def query_internal(
108160
l_search: int
109161
How deep to search. Larger parameters will result in slower latencies, but higher accuracies.
110162
Should be >= k, and if it's not, we will set it to k.
163+
where: Optional[str]
164+
Optional filter condition for filtered queries.
165+
Example: "label_col == 'dataset_1'"
111166
"""
112167
if self.size == 0:
113168
return np.full((queries.shape[0], k), MAX_FLOAT32), np.full(
@@ -125,7 +180,26 @@ def query_internal(
125180
queries = queries.copy(order="F")
126181
queries_feature_vector_array = vspy.FeatureVectorArray(queries)
127182

128-
distances, ids = self.index.query(queries_feature_vector_array, k, l_search)
183+
# NEW: Handle filtered queries
184+
query_filter = None
185+
if where is not None:
186+
# Get label enumeration from metadata
187+
label_enum_str = self.group.meta.get("label_enumeration", None)
188+
if label_enum_str is None:
189+
raise ValueError(
190+
"Cannot use 'where' parameter: index does not have filter metadata. "
191+
"This index was not created with filter support."
192+
)
193+
194+
# Parse JSON string to get label enumeration
195+
label_enumeration = json.loads(label_enum_str)
196+
197+
# Parse where clause and get filter label IDs
198+
query_filter = _parse_where_clause(where, label_enumeration)
199+
200+
distances, ids = self.index.query(
201+
queries_feature_vector_array, k, l_search, query_filter
202+
)
129203

130204
return np.array(distances, copy=False), np.array(ids, copy=False)
131205

src/include/api/vamana_index.h

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -225,11 +225,12 @@ class IndexVamana {
225225
[[nodiscard]] auto query(
226226
const QueryVectorArray& vectors,
227227
size_t top_k,
228-
std::optional<uint32_t> l_search = std::nullopt) {
228+
std::optional<uint32_t> l_search = std::nullopt,
229+
std::optional<std::unordered_set<uint32_t>> query_filter = std::nullopt) {
229230
if (!index_) {
230231
throw std::runtime_error("Cannot query() because there is no index.");
231232
}
232-
return index_->query(vectors, top_k, l_search);
233+
return index_->query(vectors, top_k, l_search, query_filter);
233234
}
234235

235236
void write_index(
@@ -348,7 +349,8 @@ class IndexVamana {
348349
query(
349350
const QueryVectorArray& vectors,
350351
size_t top_k,
351-
std::optional<uint32_t> l_search) = 0;
352+
std::optional<uint32_t> l_search,
353+
std::optional<std::unordered_set<uint32_t>> query_filter) = 0;
352354

353355
virtual void write_index(
354356
const tiledb::Context& ctx,
@@ -436,7 +438,8 @@ class IndexVamana {
436438
[[nodiscard]] std::tuple<FeatureVectorArray, FeatureVectorArray> query(
437439
const QueryVectorArray& vectors,
438440
size_t top_k,
439-
std::optional<uint32_t> l_search) override {
441+
std::optional<uint32_t> l_search,
442+
std::optional<std::unordered_set<uint32_t>> query_filter) override {
440443
// @todo using index_type = size_t;
441444
auto dtype = vectors.feature_type();
442445

@@ -448,7 +451,7 @@ class IndexVamana {
448451
(float*)vectors.data(),
449452
extents(vectors)[0],
450453
extents(vectors)[1]}; // @todo ??
451-
auto [s, t] = impl_index_.query(qspan, top_k, l_search);
454+
auto [s, t] = impl_index_.query(qspan, top_k, l_search, query_filter);
452455
auto x = FeatureVectorArray{std::move(s)};
453456
auto y = FeatureVectorArray{std::move(t)};
454457
return {std::move(x), std::move(y)};
@@ -458,7 +461,7 @@ class IndexVamana {
458461
(uint8_t*)vectors.data(),
459462
extents(vectors)[0],
460463
extents(vectors)[1]}; // @todo ??
461-
auto [s, t] = impl_index_.query(qspan, top_k, l_search);
464+
auto [s, t] = impl_index_.query(qspan, top_k, l_search, query_filter);
462465
auto x = FeatureVectorArray{std::move(s)};
463466
auto y = FeatureVectorArray{std::move(t)};
464467
return {std::move(x), std::move(y)};

src/include/index/vamana_index.h

Lines changed: 90 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -811,13 +811,15 @@ class vamana_index {
811811
* @param query_set Container of query vectors
812812
* @param k How many nearest neighbors to return
813813
* @param l_search How deep to search
814+
* @param query_filter Optional filter labels for filtered search
814815
* @return Tuple of top k scores and top k ids
815816
*/
816817
template <query_vector_array Q>
817818
auto query(
818819
const Q& query_set,
819820
size_t k,
820821
std::optional<uint32_t> l_search = std::nullopt,
822+
std::optional<std::unordered_set<filter_label_type>> query_filter = std::nullopt,
821823
Distance distance = Distance{}) {
822824
scoped_timer _("vamana_index@query");
823825

@@ -833,18 +835,50 @@ class vamana_index {
833835

834836
stdx::range_for_each(
835837
std::move(par), query_set, [&](auto&& query_vec, auto n, auto i) {
836-
auto&& [tk_scores, tk, V] = greedy_search(
837-
graph_,
838-
feature_vectors_,
839-
medoid_,
840-
query_vec,
841-
k,
842-
L,
843-
distance_function_,
844-
true);
845-
std::copy(
846-
tk_scores.data(), tk_scores.data() + k, top_k_scores[i].data());
847-
std::copy(tk.data(), tk.data() + k, top_k[i].data());
838+
// NEW: Use filtered or unfiltered search based on query_filter
839+
if (filter_enabled_ && query_filter.has_value()) {
840+
// Determine start nodes for ALL labels in query filter (multi-start)
841+
std::vector<id_type> start_nodes_for_query;
842+
for (uint32_t label : *query_filter) {
843+
if (start_nodes_.find(label) != start_nodes_.end()) {
844+
start_nodes_for_query.push_back(start_nodes_.at(label));
845+
}
846+
}
847+
848+
if (start_nodes_for_query.empty()) {
849+
throw std::runtime_error(
850+
"No start nodes found for query filter labels");
851+
}
852+
853+
auto&& [tk_scores, tk, V] = filtered_greedy_search_multi_start(
854+
graph_,
855+
feature_vectors_,
856+
filter_labels_,
857+
start_nodes_for_query,
858+
query_vec,
859+
*query_filter,
860+
k,
861+
L,
862+
distance_function_,
863+
true);
864+
std::copy(
865+
tk_scores.data(), tk_scores.data() + k, top_k_scores[i].data());
866+
std::copy(tk.data(), tk.data() + k, top_k[i].data());
867+
} else {
868+
// Unfiltered search
869+
auto&& [tk_scores, tk, V] = greedy_search(
870+
graph_,
871+
feature_vectors_,
872+
medoid_,
873+
query_vec,
874+
k,
875+
L,
876+
distance_function_,
877+
true);
878+
std::copy(
879+
tk_scores.data(), tk_scores.data() + k, top_k_scores[i].data());
880+
std::copy(tk.data(), tk.data() + k, top_k[i].data());
881+
}
848882
});
849883

850884
return std::make_tuple(std::move(top_k_scores), std::move(top_k));
@@ -857,26 +891,60 @@ class vamana_index {
857891
* @param query_vec The vector to query
858892
* @param k How many nearest neighbors to return
859893
* @param l_search How deep to search
894+
* @param query_filter Optional filter labels for filtered search
860895
* @return Top k scores and top k ids
861896
*/
862897
template <query_vector Q>
863898
auto query(
864899
const Q& query_vec,
865900
size_t k,
866901
std::optional<uint32_t> l_search = std::nullopt,
902+
std::optional<std::unordered_set<filter_label_type>> query_filter = std::nullopt,
867903
Distance distance = Distance{}) {
868904
uint32_t L = l_search ? *l_search : l_build_;
869-
auto&& [top_k_scores, top_k, V] = greedy_search(
870-
graph_,
871-
feature_vectors_,
872-
medoid_,
873-
query_vec,
874-
k,
875-
L,
876-
distance_function_,
877-
true);
878905

879-
return std::make_tuple(std::move(top_k_scores), std::move(top_k));
906+
// NEW: Use filtered or unfiltered search based on query_filter
907+
if (filter_enabled_ && query_filter.has_value()) {
908+
// Determine start nodes for ALL labels in query filter (multi-start)
909+
std::vector<id_type> start_nodes_for_query;
910+
for (uint32_t label : *query_filter) {
911+
if (start_nodes_.find(label) != start_nodes_.end()) {
912+
start_nodes_for_query.push_back(start_nodes_.at(label));
913+
}
914+
}
915+
916+
if (start_nodes_for_query.empty()) {
917+
throw std::runtime_error(
918+
"No start nodes found for query filter labels");
919+
}
920+
921+
auto&& [top_k_scores, top_k, V] = filtered_greedy_search_multi_start(
922+
graph_,
923+
feature_vectors_,
924+
filter_labels_,
925+
start_nodes_for_query,
926+
query_vec,
927+
*query_filter,
928+
k,
929+
L,
930+
distance_function_,
931+
true);
932+
933+
return std::make_tuple(std::move(top_k_scores), std::move(top_k));
934+
} else {
935+
// Unfiltered search
936+
auto&& [top_k_scores, top_k, V] = greedy_search(
937+
graph_,
938+
feature_vectors_,
939+
medoid_,
940+
query_vec,
941+
k,
942+
L,
943+
distance_function_,
944+
true);
945+
946+
return std::make_tuple(std::move(top_k_scores), std::move(top_k));
947+
}
880948
}
881949

882950
constexpr uint64_t dimensions() const {

0 commit comments

Comments
 (0)