Skip to content

Commit 7c6d4e5

Browse files
committed
Fix IDSelector handling in unbounded HNSW search
1 parent 02dbb4e commit 7c6d4e5

2 files changed

Lines changed: 88 additions & 7 deletions

File tree

faiss/impl/HNSW.cpp

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1310,18 +1310,30 @@ void reservePriorityQueue(
13101310
/// Templated body of `search_from_candidate_unbounded`. The choice of
13111311
/// max-heap vs min-heap for both `top_candidates` and `candidates` is
13121312
/// derived from C via `TopCandidatesQueue` / `CandidatesQueue`.
1313-
template <typename VTType, class C>
1313+
template <typename VTType, class C, bool use_selector>
13141314
TopCandidatesQueue<C> search_from_candidate_unbounded_fixVT(
13151315
const HNSW& hnsw,
13161316
const HNSW::Node& node,
13171317
DistanceComputer& qdis,
13181318
int ef,
13191319
VTType& vt,
1320-
HNSWStats& stats) {
1320+
HNSWStats& stats,
1321+
const IDSelector* sel) {
13211322
int ndis = 0;
13221323
TopCandidatesQueue<C> top_candidates;
13231324
reservePriorityQueue(top_candidates, ef);
13241325

1326+
TopCandidatesQueue<C> result_candidates;
1327+
if constexpr (use_selector) {
1328+
FAISS_ASSERT(sel);
1329+
// Keep rejected nodes in top_candidates so they can still be used
1330+
// for graph traversal, but never return them as search results.
1331+
reservePriorityQueue(result_candidates, ef);
1332+
if (sel->is_member(node.second)) {
1333+
result_candidates.push(node);
1334+
}
1335+
}
1336+
13251337
CandidatesQueue<C> candidates;
13261338
reservePriorityQueue(candidates, ef);
13271339

@@ -1361,6 +1373,17 @@ TopCandidatesQueue<C> search_from_candidate_unbounded_fixVT(
13611373
size_t saved_j[4];
13621374

13631375
auto add_to_heap = [&](const size_t idx, const float dis) {
1376+
if constexpr (use_selector) {
1377+
if (sel->is_member(idx) &&
1378+
(result_candidates.size() < static_cast<size_t>(ef) ||
1379+
C::cmp(result_candidates.top().first, dis))) {
1380+
result_candidates.emplace(dis, idx);
1381+
if (result_candidates.size() > static_cast<size_t>(ef)) {
1382+
result_candidates.pop();
1383+
}
1384+
}
1385+
}
1386+
13641387
if (C::cmp(top_candidates.top().first, dis) ||
13651388
top_candidates.size() < static_cast<size_t>(ef)) {
13661389
candidates.emplace(dis, idx);
@@ -1416,7 +1439,11 @@ TopCandidatesQueue<C> search_from_candidate_unbounded_fixVT(
14161439
}
14171440
stats.ndis += ndis;
14181441

1419-
return top_candidates;
1442+
if constexpr (use_selector) {
1443+
return result_candidates;
1444+
} else {
1445+
return top_candidates;
1446+
}
14201447
}
14211448

14221449
} // namespace
@@ -1434,8 +1461,8 @@ std::priority_queue<HNSW::Node> hnsw_detail::search_from_candidate_unbounded(
14341461
HNSWStats& stats) {
14351462
using C = HNSW::C_distance;
14361463
auto call = [&]<typename VTType>(VTType& vt_concrete) {
1437-
return search_from_candidate_unbounded_fixVT<VTType, C>(
1438-
hnsw, node, qdis, ef, vt_concrete, stats);
1464+
return search_from_candidate_unbounded_fixVT<VTType, C, false>(
1465+
hnsw, node, qdis, ef, vt_concrete, stats, nullptr);
14391466
};
14401467
if (VisitedTableVector* vtv = dynamic_cast<VisitedTableVector*>(vt)) {
14411468
return call(*vtv);
@@ -1522,14 +1549,26 @@ HNSWStats search_impl(
15221549
}
15231550
}
15241551
} else {
1552+
const IDSelector* sel = params ? params->sel : nullptr;
15251553
auto call = [&]<typename VTType>(VTType& vt_concrete) {
1526-
return search_from_candidate_unbounded_fixVT<VTType, C>(
1554+
if (sel) {
1555+
return search_from_candidate_unbounded_fixVT<VTType, C, true>(
1556+
hnsw,
1557+
HNSW::Node(d_nearest, nearest),
1558+
qdis,
1559+
ef,
1560+
vt_concrete,
1561+
stats,
1562+
sel);
1563+
}
1564+
return search_from_candidate_unbounded_fixVT<VTType, C, false>(
15271565
hnsw,
15281566
HNSW::Node(d_nearest, nearest),
15291567
qdis,
15301568
ef,
15311569
vt_concrete,
1532-
stats);
1570+
stats,
1571+
nullptr);
15331572
};
15341573
TopCandidatesQueue<C> top_candidates;
15351574
if (VisitedTableVector* vtv = dynamic_cast<VisitedTableVector*>(&vt)) {

tests/test_search_params.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,48 @@ def do_test_id_selector_weak(self, index_key):
302302
def test_HSNW(self):
303303
self.do_test_id_selector_weak("HNSW")
304304

305+
def test_HNSW_unbounded_queue_id_selector(self):
306+
xb = np.array(
307+
[
308+
[0.0, 0.0],
309+
[10.0, 0.0],
310+
[20.0, 0.0],
311+
[30.0, 0.0],
312+
],
313+
dtype=np.float32,
314+
)
315+
cases = (
316+
(faiss.METRIC_L2, [[0.0, 0.0]], 100.0),
317+
(faiss.METRIC_INNER_PRODUCT, [[1.0, 0.0]], 10.0),
318+
)
319+
for metric, query, expected_distance in cases:
320+
index = faiss.IndexHNSWFlat(2, 4, metric)
321+
index.hnsw.efConstruction = 40
322+
index.add(xb)
323+
selector = faiss.IDSelectorRange(1, 2)
324+
xq = np.array(query, dtype=np.float32)
325+
326+
for ef_search in (1, 40):
327+
with self.subTest(metric=metric, ef_search=ef_search):
328+
params = faiss.SearchParametersHNSW(
329+
efSearch=ef_search,
330+
bounded_queue=False,
331+
sel=selector,
332+
)
333+
D, I = index.search(xq, 1, params=params)
334+
335+
np.testing.assert_array_equal(I, [[1]])
336+
np.testing.assert_array_equal(D, [[expected_distance]])
337+
338+
empty_selector = faiss.IDSelectorRange(4, 4)
339+
params = faiss.SearchParametersHNSW(
340+
efSearch=40,
341+
bounded_queue=False,
342+
sel=empty_selector,
343+
)
344+
_, I = index.search(xq, 1, params=params)
345+
np.testing.assert_array_equal(I, [[-1]])
346+
305347
def test_idmap(self):
306348
ds = datasets.SyntheticDataset(32, 100, 100, 20)
307349
rs = np.random.RandomState(123)

0 commit comments

Comments
 (0)