faiss 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/lib/faiss/version.rb +1 -1
  4. data/vendor/faiss/faiss/AutoTune.cpp +3 -1
  5. data/vendor/faiss/faiss/Clustering.cpp +9 -1
  6. data/vendor/faiss/faiss/IVFlib.cpp +14 -3
  7. data/vendor/faiss/faiss/Index.h +2 -2
  8. data/vendor/faiss/faiss/IndexAdditiveQuantizer.cpp +9 -10
  9. data/vendor/faiss/faiss/IndexAdditiveQuantizerFastScan.cpp +2 -3
  10. data/vendor/faiss/faiss/IndexBinaryFromFloat.cpp +1 -2
  11. data/vendor/faiss/faiss/IndexBinaryHNSW.cpp +10 -12
  12. data/vendor/faiss/faiss/IndexBinaryHash.cpp +5 -9
  13. data/vendor/faiss/faiss/IndexBinaryIVF.cpp +5 -7
  14. data/vendor/faiss/faiss/IndexEDEN.cpp +273 -0
  15. data/vendor/faiss/faiss/IndexEDEN.h +57 -0
  16. data/vendor/faiss/faiss/IndexFastScan.cpp +15 -4
  17. data/vendor/faiss/faiss/IndexFlat.cpp +13 -50
  18. data/vendor/faiss/faiss/IndexHNSW.cpp +177 -148
  19. data/vendor/faiss/faiss/IndexIDMap.cpp +16 -3
  20. data/vendor/faiss/faiss/IndexIDMap.h +2 -0
  21. data/vendor/faiss/faiss/IndexIVF.cpp +19 -8
  22. data/vendor/faiss/faiss/IndexIVFAdditiveQuantizer.cpp +3 -3
  23. data/vendor/faiss/faiss/IndexIVFAdditiveQuantizerFastScan.cpp +3 -4
  24. data/vendor/faiss/faiss/IndexIVFEDEN.cpp +302 -0
  25. data/vendor/faiss/faiss/IndexIVFEDEN.h +70 -0
  26. data/vendor/faiss/faiss/IndexIVFFastScan.cpp +5 -6
  27. data/vendor/faiss/faiss/IndexIVFFlat.cpp +6 -5
  28. data/vendor/faiss/faiss/IndexIVFFlatPanorama.cpp +3 -3
  29. data/vendor/faiss/faiss/IndexIVFIndependentQuantizer.cpp +1 -1
  30. data/vendor/faiss/faiss/IndexIVFPQ.cpp +42 -25
  31. data/vendor/faiss/faiss/IndexIVFPQFastScan.cpp +0 -1
  32. data/vendor/faiss/faiss/IndexIVFPQR.cpp +2 -3
  33. data/vendor/faiss/faiss/IndexIVFRaBitQ.cpp +23 -62
  34. data/vendor/faiss/faiss/IndexIVFRaBitQFastScan.cpp +180 -76
  35. data/vendor/faiss/faiss/IndexIVFRaBitQFastScan.h +5 -4
  36. data/vendor/faiss/faiss/IndexIVFSpectralHash.cpp +8 -6
  37. data/vendor/faiss/faiss/IndexLSH.cpp +2 -3
  38. data/vendor/faiss/faiss/IndexLattice.cpp +5 -0
  39. data/vendor/faiss/faiss/IndexNNDescent.cpp +10 -3
  40. data/vendor/faiss/faiss/IndexNSG.cpp +8 -4
  41. data/vendor/faiss/faiss/IndexPQ.cpp +6 -8
  42. data/vendor/faiss/faiss/IndexPreTransform.cpp +15 -0
  43. data/vendor/faiss/faiss/IndexRaBitQ.cpp +2 -2
  44. data/vendor/faiss/faiss/IndexRaBitQFastScan.cpp +1 -2
  45. data/vendor/faiss/faiss/IndexRaBitQFastScan.h +5 -1
  46. data/vendor/faiss/faiss/IndexRefine.cpp +30 -1
  47. data/vendor/faiss/faiss/IndexReplicas.cpp +1 -2
  48. data/vendor/faiss/faiss/IndexScalarQuantizer.cpp +68 -6
  49. data/vendor/faiss/faiss/IndexScalarQuantizer.h +10 -0
  50. data/vendor/faiss/faiss/IndexShards.cpp +2 -2
  51. data/vendor/faiss/faiss/IndexShardsIVF.cpp +2 -2
  52. data/vendor/faiss/faiss/MetaIndexes.cpp +2 -4
  53. data/vendor/faiss/faiss/SuperKMeans.cpp +256 -240
  54. data/vendor/faiss/faiss/SuperKMeans.h +30 -0
  55. data/vendor/faiss/faiss/VectorTransform.cpp +33 -2
  56. data/vendor/faiss/faiss/clone_index.cpp +5 -0
  57. data/vendor/faiss/faiss/cppcontrib/SaDecodeKernels.h +1 -1
  58. data/vendor/faiss/faiss/cppcontrib/sa_decode/Level2-neon-inl.h +902 -12
  59. data/vendor/faiss/faiss/cppcontrib/sa_decode/PQ-neon-inl.h +702 -10
  60. data/vendor/faiss/faiss/factory_tools.cpp +51 -4
  61. data/vendor/faiss/faiss/gpu/GpuCloner.cpp +11 -11
  62. data/vendor/faiss/faiss/gpu/GpuIndex.h +34 -11
  63. data/vendor/faiss/faiss/gpu/GpuIndexCagra.h +47 -0
  64. data/vendor/faiss/faiss/gpu/GpuIndexIVF.h +17 -0
  65. data/vendor/faiss/faiss/gpu/GpuIndexIVFScalarQuantizer.h +16 -0
  66. data/vendor/faiss/faiss/gpu/GpuResources.h +3 -2
  67. data/vendor/faiss/faiss/gpu/StandardGpuResources.cpp +11 -12
  68. data/vendor/faiss/faiss/gpu/StandardGpuResources.h +3 -3
  69. data/vendor/faiss/faiss/gpu/perf/PerfClustering.cpp +1 -1
  70. data/vendor/faiss/faiss/gpu/perf/PerfIVFPQAdd.cpp +2 -2
  71. data/vendor/faiss/faiss/gpu/test/TestGpuIndexIVFScalarQuantizer.cpp +180 -0
  72. data/vendor/faiss/faiss/gpu_metal/MetalDistance.h +87 -0
  73. data/vendor/faiss/faiss/gpu_metal/MetalIndex.h +7 -0
  74. data/vendor/faiss/faiss/gpu_metal/MetalIndexIVFFlat.h +177 -0
  75. data/vendor/faiss/faiss/gpu_metal/MetalIndexIVFPQ.h +88 -0
  76. data/vendor/faiss/faiss/gpu_metal/MetalKernels.h +48 -3
  77. data/vendor/faiss/faiss/gpu_metal/MetalPythonBridge.h +45 -0
  78. data/vendor/faiss/faiss/gpu_metal/impl/MetalIVFFlat.h +193 -0
  79. data/vendor/faiss/faiss/gpu_metal/impl/MetalIVFPQ.h +134 -0
  80. data/vendor/faiss/faiss/impl/ClusteringInitialization.cpp +2 -2
  81. data/vendor/faiss/faiss/impl/DistanceComputer.h +34 -0
  82. data/vendor/faiss/faiss/impl/EDENQuantizer.h +119 -0
  83. data/vendor/faiss/faiss/impl/HNSW.cpp +658 -344
  84. data/vendor/faiss/faiss/impl/HNSW.h +51 -13
  85. data/vendor/faiss/faiss/impl/LocalSearchQuantizer.cpp +2 -2
  86. data/vendor/faiss/faiss/impl/NSG.cpp +18 -12
  87. data/vendor/faiss/faiss/impl/Panorama.h +20 -7
  88. data/vendor/faiss/faiss/impl/PolysemousTraining.cpp +152 -84
  89. data/vendor/faiss/faiss/impl/ProductQuantizer.cpp +59 -24
  90. data/vendor/faiss/faiss/impl/RaBitQUtils.cpp +45 -37
  91. data/vendor/faiss/faiss/impl/RaBitQUtils.h +35 -0
  92. data/vendor/faiss/faiss/impl/RaBitQuantizer.cpp +175 -68
  93. data/vendor/faiss/faiss/impl/RaBitQuantizer.h +19 -0
  94. data/vendor/faiss/faiss/impl/RaBitQuantizerMultiBit.cpp +2 -11
  95. data/vendor/faiss/faiss/impl/ResultHandler.h +26 -31
  96. data/vendor/faiss/faiss/impl/ScalarQuantizer.cpp +522 -58
  97. data/vendor/faiss/faiss/impl/ScalarQuantizer.h +70 -0
  98. data/vendor/faiss/faiss/impl/ThreadedIndex-inl.h +2 -2
  99. data/vendor/faiss/faiss/impl/VisitedTable.cpp +33 -13
  100. data/vendor/faiss/faiss/impl/VisitedTable.h +88 -33
  101. data/vendor/faiss/faiss/impl/binary_hamming/IndexBinaryIVF_impl.h +1 -1
  102. data/vendor/faiss/faiss/impl/binary_hamming/avx2.cpp +4 -4
  103. data/vendor/faiss/faiss/impl/fast_scan/dispatching.h +38 -3
  104. data/vendor/faiss/faiss/impl/hnsw/LockVector.cpp +1 -1
  105. data/vendor/faiss/faiss/impl/hnsw/MinimaxHeap.cpp +35 -43
  106. data/vendor/faiss/faiss/impl/hnsw/MinimaxHeap.h +64 -15
  107. data/vendor/faiss/faiss/impl/hnsw/avx2.cpp +86 -40
  108. data/vendor/faiss/faiss/impl/hnsw/avx512.cpp +81 -50
  109. data/vendor/faiss/faiss/impl/index_read.cpp +476 -75
  110. data/vendor/faiss/faiss/impl/index_write.cpp +56 -4
  111. data/vendor/faiss/faiss/impl/io_macros.h +25 -0
  112. data/vendor/faiss/faiss/impl/lattice_Zn.cpp +8 -9
  113. data/vendor/faiss/faiss/impl/platform_macros.h +15 -9
  114. data/vendor/faiss/faiss/impl/polysemous_training/avx512.cpp +284 -0
  115. data/vendor/faiss/faiss/impl/polysemous_training/dispatch.h +115 -0
  116. data/vendor/faiss/faiss/impl/pq_code_distance/IVFPQ_QueryTables.cpp +0 -1
  117. data/vendor/faiss/faiss/impl/pq_code_distance/PQDistanceComputer_impl.h +26 -15
  118. data/vendor/faiss/faiss/impl/pq_code_distance/avx2.cpp +6 -4
  119. data/vendor/faiss/faiss/impl/pq_code_distance/avx512.cpp +2 -0
  120. data/vendor/faiss/faiss/impl/pq_code_distance/neon.cpp +2 -0
  121. data/vendor/faiss/faiss/impl/pq_code_distance/pq_code_distance-generic.cpp +20 -0
  122. data/vendor/faiss/faiss/impl/pq_code_distance/pq_code_distance-inl.h +36 -0
  123. data/vendor/faiss/faiss/impl/pq_code_distance/pq_code_distance-sve.cpp +5 -0
  124. data/vendor/faiss/faiss/impl/pq_code_distance/pq_scan_impl.h +105 -0
  125. data/vendor/faiss/faiss/impl/pq_code_distance/rvv.cpp +2 -0
  126. data/vendor/faiss/faiss/impl/result_handler/ResultHandler.cpp +195 -0
  127. data/vendor/faiss/faiss/impl/result_handler/avx2.cpp +133 -0
  128. data/vendor/faiss/faiss/impl/result_handler/avx512.cpp +281 -0
  129. data/vendor/faiss/faiss/impl/scalar_quantizer/EDENQuantizer-avx2.cpp +72 -0
  130. data/vendor/faiss/faiss/impl/scalar_quantizer/EDENQuantizer-avx512.cpp +228 -0
  131. data/vendor/faiss/faiss/impl/scalar_quantizer/EDENQuantizer.cpp +882 -0
  132. data/vendor/faiss/faiss/impl/scalar_quantizer/distance_computers.h +6 -0
  133. data/vendor/faiss/faiss/impl/scalar_quantizer/quantizers.h +336 -26
  134. data/vendor/faiss/faiss/impl/scalar_quantizer/sq-avx2.cpp +331 -32
  135. data/vendor/faiss/faiss/impl/scalar_quantizer/sq-avx512-impl.h +553 -0
  136. data/vendor/faiss/faiss/impl/scalar_quantizer/sq-avx512-spr.cpp +558 -0
  137. data/vendor/faiss/faiss/impl/scalar_quantizer/sq-avx512.cpp +284 -45
  138. data/vendor/faiss/faiss/impl/scalar_quantizer/sq-dispatch.h +502 -3
  139. data/vendor/faiss/faiss/impl/scalar_quantizer/sq-neon.cpp +157 -32
  140. data/vendor/faiss/faiss/impl/scalar_quantizer/sq-rvv.cpp +26 -0
  141. data/vendor/faiss/faiss/impl/simd_dispatch.h +86 -8
  142. data/vendor/faiss/faiss/index_factory.cpp +37 -7
  143. data/vendor/faiss/faiss/index_io.h +16 -0
  144. data/vendor/faiss/faiss/invlists/DirectMap.cpp +5 -2
  145. data/vendor/faiss/faiss/invlists/InvertedLists.cpp +15 -15
  146. data/vendor/faiss/faiss/invlists/InvertedLists.h +2 -2
  147. data/vendor/faiss/faiss/invlists/OnDiskInvertedLists.cpp +19 -4
  148. data/vendor/faiss/faiss/python/python_callbacks.cpp +3 -1
  149. data/vendor/faiss/faiss/svs/IndexSVSFaissUtils.h +60 -0
  150. data/vendor/faiss/faiss/svs/IndexSVSFlat.cpp +26 -1
  151. data/vendor/faiss/faiss/svs/IndexSVSFlat.h +13 -0
  152. data/vendor/faiss/faiss/svs/IndexSVSIVF.cpp +1 -1
  153. data/vendor/faiss/faiss/svs/IndexSVSIVFLeanVec.cpp +1 -1
  154. data/vendor/faiss/faiss/svs/IndexSVSVamana.cpp +150 -23
  155. data/vendor/faiss/faiss/svs/IndexSVSVamana.h +30 -7
  156. data/vendor/faiss/faiss/svs/IndexSVSVamanaLVQ.cpp +3 -2
  157. data/vendor/faiss/faiss/svs/IndexSVSVamanaLVQ.h +2 -1
  158. data/vendor/faiss/faiss/svs/IndexSVSVamanaLeanVec.cpp +65 -25
  159. data/vendor/faiss/faiss/svs/IndexSVSVamanaLeanVec.h +3 -2
  160. data/vendor/faiss/faiss/utils/approx_topk_hamming/approx_topk_hamming.h +1 -1
  161. data/vendor/faiss/faiss/utils/bf16.h +34 -0
  162. data/vendor/faiss/faiss/utils/distances.cpp +14 -2
  163. data/vendor/faiss/faiss/utils/distances_simd.cpp +4 -4
  164. data/vendor/faiss/faiss/utils/extra_distances.cpp +4 -14
  165. data/vendor/faiss/faiss/utils/extra_distances.h +1 -2
  166. data/vendor/faiss/faiss/utils/hamming.cpp +9 -9
  167. data/vendor/faiss/faiss/utils/hamming_distance/hamming_avx2.cpp +2 -1
  168. data/vendor/faiss/faiss/utils/hamming_distance/hamming_avx512_spr.cpp +15 -0
  169. data/vendor/faiss/faiss/utils/hamming_distance/hamming_computer-avx512.h +6 -30
  170. data/vendor/faiss/faiss/utils/hamming_distance/hamming_computer-avx512_spr.h +171 -0
  171. data/vendor/faiss/faiss/utils/partitioning.cpp +0 -2
  172. data/vendor/faiss/faiss/utils/quantize_lut.cpp +29 -8
  173. data/vendor/faiss/faiss/utils/rabitq_simd.h +202 -0
  174. data/vendor/faiss/faiss/utils/simd_impl/distances_avx2.cpp +0 -1
  175. data/vendor/faiss/faiss/utils/simd_impl/distances_avx512.cpp +263 -15
  176. data/vendor/faiss/faiss/utils/simd_impl/distances_rvv.cpp +160 -18
  177. data/vendor/faiss/faiss/utils/simd_impl/partitioning_simdlib256.h +14 -68
  178. data/vendor/faiss/faiss/utils/simd_impl/rabitq_avx2.cpp +245 -0
  179. data/vendor/faiss/faiss/utils/simd_impl/rabitq_avx512.cpp +273 -0
  180. data/vendor/faiss/faiss/utils/simd_impl/rabitq_avx512_spr.cpp +435 -0
  181. data/vendor/faiss/faiss/utils/simd_impl/rabitq_neon.cpp +11 -0
  182. data/vendor/faiss/faiss/utils/simd_impl/rabitq_rvv.cpp +143 -6
  183. data/vendor/faiss/faiss/utils/simd_levels.cpp +56 -2
  184. data/vendor/faiss/faiss/utils/simd_levels.h +14 -0
  185. data/vendor/faiss/faiss/utils/utils.cpp +9 -27
  186. metadata +27 -2
@@ -7,9 +7,11 @@
7
7
 
8
8
  #include <faiss/impl/HNSW.h>
9
9
 
10
+ #include <array>
10
11
  #include <cinttypes>
11
12
  #include <cstddef>
12
13
  #include <cstdlib>
14
+ #include <type_traits>
13
15
 
14
16
  #include <faiss/IndexHNSW.h>
15
17
 
@@ -171,17 +173,21 @@ void HNSW::print_neighbor_stats(int level) const {
171
173
  }
172
174
 
173
175
  void HNSW::fill_with_random_links(size_t n) {
174
- int max_level_2 = prepare_level_tab(n);
176
+ if (n == 0) {
177
+ return;
178
+ }
179
+ max_level = prepare_level_tab(n);
180
+ entry_point = 0;
181
+
175
182
  RandomGenerator rng2(456);
176
183
 
177
- for (int level = max_level_2 - 1; level >= 0; --level) {
184
+ for (int level = max_level - 1; level >= 0; --level) {
178
185
  std::vector<int> elts;
179
186
  for (size_t i = 0; i < n; i++) {
180
187
  if (levels[i] > level) {
181
188
  elts.push_back(i);
182
189
  }
183
190
  }
184
- printf("linking %zd elements in level %d\n", elts.size(), level);
185
191
 
186
192
  if (elts.size() == 1) {
187
193
  continue;
@@ -233,28 +239,32 @@ int HNSW::prepare_level_tab(size_t n, bool preset_levels) {
233
239
  * neighbor only if there is no previous neighbor that is closer to
234
240
  * that vertex than the query.
235
241
  */
242
+ template <class Comp>
236
243
  void HNSW::shrink_neighbor_list(
237
244
  DistanceComputer& qdis,
238
- std::priority_queue<NodeDistFarther>& input,
239
- std::vector<NodeDistFarther>& output,
245
+ std::priority_queue<NodeDistFartherT<Comp>>& input,
246
+ std::vector<NodeDistFartherT<Comp>>& output,
240
247
  size_t max_size,
241
248
  bool keep_max_size_level0) {
242
249
  // This prevents number of neighbors at
243
250
  // level 0 from being shrunk to less than 2 * M.
244
251
  // This is essential in making sure
245
252
  // `faiss::gpu::GpuIndexCagra::copyFrom(IndexHNSWCagra*)` is functional
246
- std::vector<NodeDistFarther> outsiders;
253
+ std::vector<NodeDistFartherT<Comp>> outsiders;
247
254
 
248
255
  while (input.size() > 0) {
249
- NodeDistFarther v1 = input.top();
256
+ NodeDistFartherT<Comp> v1 = input.top();
250
257
  input.pop();
251
258
  float dist_v1_q = v1.d;
252
259
 
253
260
  bool good = true;
254
- for (NodeDistFarther v2 : output) {
261
+ for (NodeDistFartherT<Comp> v2 : output) {
255
262
  float dist_v1_v2 = qdis.symmetric_dis(v2.id, v1.id);
256
263
 
257
- if (dist_v1_v2 < dist_v1_q) {
264
+ // "v1 is bad" if some previously-kept neighbor v2 is closer
265
+ // (more similar, under CMin) to v1 than the query is. Encoded
266
+ // generically as: v1v2 is "better than" v1q under Comp.
267
+ if (Comp::cmp(dist_v1_q, dist_v1_v2)) {
258
268
  good = false;
259
269
  break;
260
270
  }
@@ -277,44 +287,88 @@ void HNSW::shrink_neighbor_list(
277
287
  }
278
288
  }
279
289
 
290
+ // Explicit instantiations for the two supported comparators.
291
+ template void HNSW::shrink_neighbor_list<HNSW::C_distance>(
292
+ DistanceComputer&,
293
+ std::priority_queue<HNSW::NodeDistFartherT<HNSW::C_distance>>&,
294
+ std::vector<HNSW::NodeDistFartherT<HNSW::C_distance>>&,
295
+ size_t,
296
+ bool);
297
+ template void HNSW::shrink_neighbor_list<HNSW::C_similarity>(
298
+ DistanceComputer&,
299
+ std::priority_queue<HNSW::NodeDistFartherT<HNSW::C_similarity>>&,
300
+ std::vector<HNSW::NodeDistFartherT<HNSW::C_similarity>>&,
301
+ size_t,
302
+ bool);
303
+
280
304
  namespace {
281
305
 
282
306
  using storage_idx_t = HNSW::storage_idx_t;
283
- using NodeDistCloser = HNSW::NodeDistCloser;
284
- using NodeDistFarther = HNSW::NodeDistFarther;
307
+
308
+ // Map a (high-level) HNSW comparator C — which uses int64_t IDs — to the
309
+ // (low-level) MinimaxHeap comparator HC, which uses int32_t IDs.
310
+ template <class C>
311
+ using HC_for = std::
312
+ conditional_t<C::is_max, CMax<float, int32_t>, CMin<float, int32_t>>;
313
+
314
+ // Priority queue types used by the unbounded search variant. For CMax
315
+ // (distance) "top_candidates" is a max-heap of the kept-so-far results
316
+ // (top is the farthest) and "candidates" is a min-heap of the next nodes
317
+ // to explore (top is the closest). For CMin (similarity) the orderings are
318
+ // swapped: top_candidates is a min-heap (top is the least similar) and
319
+ // candidates is a max-heap (top is the most similar).
320
+ template <class C>
321
+ using TopCandidatesQueue = std::conditional_t<
322
+ C::is_max,
323
+ std::priority_queue<HNSW::Node>,
324
+ std::priority_queue<
325
+ HNSW::Node,
326
+ std::vector<HNSW::Node>,
327
+ std::greater<HNSW::Node>>>;
328
+
329
+ template <class C>
330
+ using CandidatesQueue = std::conditional_t<
331
+ C::is_max,
332
+ std::priority_queue<
333
+ HNSW::Node,
334
+ std::vector<HNSW::Node>,
335
+ std::greater<HNSW::Node>>,
336
+ std::priority_queue<HNSW::Node>>;
285
337
 
286
338
  /**************************************************************
287
339
  * Addition subroutines
288
340
  **************************************************************/
289
341
 
290
342
  /// remove neighbors from the list to make it smaller than max_size
291
- void shrink_neighbor_list(
343
+ template <class C>
344
+ void shrink_neighbor_list_inner(
292
345
  DistanceComputer& qdis,
293
- std::priority_queue<NodeDistCloser>& resultSet1,
346
+ std::priority_queue<HNSW::NodeDistCloserT<C>>& resultSet1,
294
347
  size_t max_size,
295
348
  bool keep_max_size_level0 = false) {
296
349
  if (resultSet1.size() < static_cast<size_t>(max_size)) {
297
350
  return;
298
351
  }
299
- std::priority_queue<NodeDistFarther> resultSet;
300
- std::vector<NodeDistFarther> returnlist;
352
+ std::priority_queue<HNSW::NodeDistFartherT<C>> resultSet;
353
+ std::vector<HNSW::NodeDistFartherT<C>> returnlist;
301
354
 
302
355
  while (resultSet1.size() > 0) {
303
356
  resultSet.emplace(resultSet1.top().d, resultSet1.top().id);
304
357
  resultSet1.pop();
305
358
  }
306
359
 
307
- HNSW::shrink_neighbor_list(
360
+ HNSW::shrink_neighbor_list<C>(
308
361
  qdis, resultSet, returnlist, max_size, keep_max_size_level0);
309
362
 
310
- for (NodeDistFarther curen2 : returnlist) {
363
+ for (HNSW::NodeDistFartherT<C> curen2 : returnlist) {
311
364
  resultSet1.emplace(curen2.d, curen2.id);
312
365
  }
313
366
  }
314
367
 
315
368
  /// add a link between two elements, possibly shrinking the list
316
369
  /// of links to make room for it.
317
- void add_link(
370
+ template <class C>
371
+ void add_link_tpl(
318
372
  HNSW& hnsw,
319
373
  DistanceComputer& qdis,
320
374
  storage_idx_t src,
@@ -339,16 +393,17 @@ void add_link(
339
393
  // otherwise we let them fight out which to keep
340
394
 
341
395
  // copy to resultSet...
342
- std::priority_queue<NodeDistCloser> resultSet;
396
+ std::priority_queue<HNSW::NodeDistCloserT<C>> resultSet;
343
397
  resultSet.emplace(qdis.symmetric_dis(src, dest), dest);
344
- for (size_t i = begin; i < end; i++) { // HERE WAS THE BUG
398
+ for (size_t i = begin; i < end; i++) {
345
399
  storage_idx_t neigh = hnsw.neighbors[i];
346
400
  resultSet.emplace(qdis.symmetric_dis(src, neigh), neigh);
347
401
  }
348
402
 
349
403
  size_t max_size = end - begin;
350
404
  max_size -= max_size * std::clamp(hnsw.prune_headroom, 0.0f, 0.5f);
351
- shrink_neighbor_list(qdis, resultSet, max_size, keep_max_size_level0);
405
+ shrink_neighbor_list_inner<C>(
406
+ qdis, resultSet, max_size, keep_max_size_level0);
352
407
 
353
408
  // ...and back
354
409
  size_t i = begin;
@@ -362,31 +417,33 @@ void add_link(
362
417
  }
363
418
  }
364
419
 
365
- } // namespace
366
-
367
- /// search neighbors on a single level, starting from an entry point
368
- void search_neighbors_to_add(
420
+ /** Templated body of `search_neighbors_to_add` — instantiated once per final
421
+ * VisitedTable subclass × comparator so that `vt.set/advance` are inlined
422
+ * and the cost of virtual dispatch is paid only once at the top of the call.
423
+ */
424
+ template <typename VTType, class C>
425
+ static void search_neighbors_to_add_fixVT(
369
426
  HNSW& hnsw,
370
427
  DistanceComputer& qdis,
371
- std::priority_queue<NodeDistCloser>& results,
428
+ std::priority_queue<HNSW::NodeDistCloserT<C>>& results,
372
429
  int entry_point,
373
430
  float d_entry_point,
374
431
  int level,
375
- VisitedTable& vt,
432
+ VTType& vt,
376
433
  bool reference_version) {
377
434
  // top is nearest candidate
378
- std::priority_queue<NodeDistFarther> candidates;
435
+ std::priority_queue<HNSW::NodeDistFartherT<C>> candidates;
379
436
 
380
- NodeDistFarther ev(d_entry_point, entry_point);
437
+ HNSW::NodeDistFartherT<C> ev(d_entry_point, entry_point);
381
438
  candidates.push(ev);
382
439
  results.emplace(d_entry_point, entry_point);
383
440
  vt.set(entry_point);
384
441
 
385
442
  while (!candidates.empty()) {
386
443
  // get nearest
387
- const NodeDistFarther& currEv = candidates.top();
444
+ const HNSW::NodeDistFartherT<C>& currEv = candidates.top();
388
445
 
389
- if (currEv.d > results.top().d) {
446
+ if (C::cmp(currEv.d, results.top().d)) {
390
447
  break;
391
448
  }
392
449
  int currNode = currEv.id;
@@ -407,7 +464,7 @@ void search_neighbors_to_add(
407
464
  if (reference_version) {
408
465
  // a reference version
409
466
  for (size_t i = begin; i < end; i++) {
410
- storage_idx_t nodeId = hnsw.neighbors[i];
467
+ HNSW::storage_idx_t nodeId = hnsw.neighbors[i];
411
468
  if (nodeId < 0) {
412
469
  break;
413
470
  }
@@ -416,10 +473,10 @@ void search_neighbors_to_add(
416
473
  }
417
474
 
418
475
  float dis = qdis(nodeId);
419
- NodeDistFarther evE1(dis, nodeId);
476
+ HNSW::NodeDistFartherT<C> evE1(dis, nodeId);
420
477
 
421
478
  if (results.size() < static_cast<size_t>(hnsw.efConstruction) ||
422
- results.top().d > dis) {
479
+ C::cmp(results.top().d, dis)) {
423
480
  results.emplace(dis, nodeId);
424
481
  candidates.emplace(dis, nodeId);
425
482
  if (results.size() >
@@ -432,10 +489,10 @@ void search_neighbors_to_add(
432
489
  // a faster version
433
490
 
434
491
  // the following version processes 4 neighbors at a time
435
- auto update_with_candidate = [&](const storage_idx_t idx,
492
+ auto update_with_candidate = [&](const HNSW::storage_idx_t idx,
436
493
  const float dis) {
437
494
  if (results.size() < static_cast<size_t>(hnsw.efConstruction) ||
438
- results.top().d > dis) {
495
+ C::cmp(results.top().d, dis)) {
439
496
  results.emplace(dis, idx);
440
497
  candidates.emplace(dis, idx);
441
498
  if (results.size() >
@@ -446,10 +503,10 @@ void search_neighbors_to_add(
446
503
  };
447
504
 
448
505
  int n_buffered = 0;
449
- storage_idx_t buffered_ids[4];
506
+ HNSW::storage_idx_t buffered_ids[4];
450
507
 
451
508
  for (size_t j = begin; j < end; j++) {
452
- storage_idx_t nodeId = hnsw.neighbors[j];
509
+ HNSW::storage_idx_t nodeId = hnsw.neighbors[j];
453
510
  if (nodeId < 0) {
454
511
  break;
455
512
  }
@@ -491,9 +548,41 @@ void search_neighbors_to_add(
491
548
  vt.advance();
492
549
  }
493
550
 
494
- /// Finds neighbors and builds links with them, starting from an entry
495
- /// point. The own neighbor list is assumed to be locked.
496
- void HNSW::add_links_starting_from(
551
+ /// Dispatches the VisitedTable concrete type for a given C, then calls
552
+ /// the templated `search_neighbors_to_add_fixVT<VTType, C>`.
553
+ template <class C>
554
+ void search_neighbors_to_add_dispatch(
555
+ HNSW& hnsw,
556
+ DistanceComputer& qdis,
557
+ std::priority_queue<HNSW::NodeDistCloserT<C>>& results,
558
+ int entry_point,
559
+ float d_entry_point,
560
+ int level,
561
+ VisitedTable& vt,
562
+ bool reference_version) {
563
+ auto call = [&]<typename VTType>(VTType& vt_concrete) {
564
+ search_neighbors_to_add_fixVT<VTType, C>(
565
+ hnsw,
566
+ qdis,
567
+ results,
568
+ entry_point,
569
+ d_entry_point,
570
+ level,
571
+ vt_concrete,
572
+ reference_version);
573
+ };
574
+ if (VisitedTableVector* vtv = dynamic_cast<VisitedTableVector*>(&vt)) {
575
+ call(*vtv);
576
+ return;
577
+ }
578
+ VisitedTableSet& vts = dynamic_cast<VisitedTableSet&>(vt);
579
+ call(vts);
580
+ }
581
+
582
+ /// Templated implementation of `HNSW::add_links_starting_from`.
583
+ template <class C>
584
+ void add_links_starting_from_impl(
585
+ HNSW& hnsw,
497
586
  DistanceComputer& ptdis,
498
587
  storage_idx_t pt_id,
499
588
  storage_idx_t nearest,
@@ -502,21 +591,22 @@ void HNSW::add_links_starting_from(
502
591
  LockVector& locks,
503
592
  VisitedTable& vt,
504
593
  bool keep_max_size_level0) {
505
- std::priority_queue<NodeDistCloser> link_targets;
594
+ std::priority_queue<HNSW::NodeDistCloserT<C>> link_targets;
506
595
 
507
- search_neighbors_to_add(
508
- *this, ptdis, link_targets, nearest, d_nearest, level, vt);
596
+ search_neighbors_to_add_dispatch<C>(
597
+ hnsw, ptdis, link_targets, nearest, d_nearest, level, vt, false);
509
598
 
510
599
  // but we can afford only this many neighbors
511
- int M = nb_neighbors(level);
600
+ int M = hnsw.nb_neighbors(level);
512
601
 
513
- ::faiss::shrink_neighbor_list(ptdis, link_targets, M, keep_max_size_level0);
602
+ shrink_neighbor_list_inner<C>(ptdis, link_targets, M, keep_max_size_level0);
514
603
 
515
604
  std::vector<storage_idx_t> neighbors_to_add;
516
605
  neighbors_to_add.reserve(link_targets.size());
517
606
  while (!link_targets.empty()) {
518
607
  storage_idx_t other_id = link_targets.top().id;
519
- add_link(*this, ptdis, pt_id, other_id, level, keep_max_size_level0);
608
+ add_link_tpl<C>(
609
+ hnsw, ptdis, pt_id, other_id, level, keep_max_size_level0);
520
610
  neighbors_to_add.push_back(other_id);
521
611
  link_targets.pop();
522
612
  }
@@ -524,33 +614,197 @@ void HNSW::add_links_starting_from(
524
614
  locks.unlock(pt_id);
525
615
  for (storage_idx_t other_id : neighbors_to_add) {
526
616
  locks.lock(other_id);
527
- add_link(*this, ptdis, other_id, pt_id, level, keep_max_size_level0);
617
+ add_link_tpl<C>(
618
+ hnsw, ptdis, other_id, pt_id, level, keep_max_size_level0);
528
619
  locks.unlock(other_id);
529
620
  }
530
621
  locks.lock(pt_id);
531
622
  }
532
623
 
624
+ } // namespace
625
+
626
+ /// Finds neighbors and builds links with them, starting from an entry
627
+ /// point. The own neighbor list is assumed to be locked.
628
+ void HNSW::add_links_starting_from(
629
+ DistanceComputer& ptdis,
630
+ storage_idx_t pt_id,
631
+ storage_idx_t nearest,
632
+ float d_nearest,
633
+ int level,
634
+ LockVector& locks,
635
+ VisitedTable& vt,
636
+ bool keep_max_size_level0) {
637
+ if (is_similarity) {
638
+ add_links_starting_from_impl<C_similarity>(
639
+ *this,
640
+ ptdis,
641
+ pt_id,
642
+ nearest,
643
+ d_nearest,
644
+ level,
645
+ locks,
646
+ vt,
647
+ keep_max_size_level0);
648
+ } else {
649
+ add_links_starting_from_impl<C_distance>(
650
+ *this,
651
+ ptdis,
652
+ pt_id,
653
+ nearest,
654
+ d_nearest,
655
+ level,
656
+ locks,
657
+ vt,
658
+ keep_max_size_level0);
659
+ }
660
+ }
661
+
662
+ /// search neighbors on a single level, starting from an entry point.
663
+ /// Public dispatcher: always operates in distance (CMax) mode because its
664
+ /// `priority_queue<HNSW::NodeDistCloser>` signature is the back-compat
665
+ /// distance flavor. Internal callers that need similarity mode reach the
666
+ /// templated implementation directly via `search_neighbors_to_add_dispatch`.
667
+ void hnsw_detail::search_neighbors_to_add(
668
+ HNSW& hnsw,
669
+ DistanceComputer& qdis,
670
+ std::priority_queue<HNSW::NodeDistCloser>& results,
671
+ int entry_point,
672
+ float d_entry_point,
673
+ int level,
674
+ VisitedTable& vt,
675
+ bool reference_version) {
676
+ search_neighbors_to_add_dispatch<HNSW::C_distance>(
677
+ hnsw,
678
+ qdis,
679
+ results,
680
+ entry_point,
681
+ d_entry_point,
682
+ level,
683
+ vt,
684
+ reference_version);
685
+ }
686
+
533
687
  /**************************************************************
534
688
  * Building, parallel
535
689
  **************************************************************/
536
690
 
537
- void HNSW::add_with_locks(
691
+ namespace {
692
+
693
+ /// Greedy update of the nearest entry point at a given level.
694
+ template <class C>
695
+ HNSWStats greedy_update_nearest_impl(
696
+ const HNSW& hnsw,
697
+ DistanceComputer& qdis,
698
+ int level,
699
+ storage_idx_t& nearest,
700
+ float& d_nearest) {
701
+ HNSWStats stats;
702
+
703
+ for (;;) {
704
+ storage_idx_t prev_nearest = nearest;
705
+
706
+ size_t begin, end;
707
+ hnsw.neighbor_range(nearest, level, &begin, &end);
708
+
709
+ size_t ndis = 0;
710
+
711
+ // a faster version: reference version in unit test test_hnsw.cpp
712
+ // the following version processes 4 neighbors at a time
713
+ auto update_with_candidate = [&](const storage_idx_t idx,
714
+ const float dis) {
715
+ if (C::cmp(d_nearest, dis)) {
716
+ nearest = idx;
717
+ d_nearest = dis;
718
+ }
719
+ };
720
+
721
+ int n_buffered = 0;
722
+ storage_idx_t buffered_ids[4];
723
+
724
+ for (size_t j = begin; j < end; j++) {
725
+ storage_idx_t v = hnsw.neighbors[j];
726
+ if (v < 0) {
727
+ break;
728
+ }
729
+ ndis += 1;
730
+
731
+ buffered_ids[n_buffered] = v;
732
+ n_buffered += 1;
733
+
734
+ if (n_buffered == 4) {
735
+ float dis[4];
736
+ qdis.distances_batch_4(
737
+ buffered_ids[0],
738
+ buffered_ids[1],
739
+ buffered_ids[2],
740
+ buffered_ids[3],
741
+ dis[0],
742
+ dis[1],
743
+ dis[2],
744
+ dis[3]);
745
+
746
+ for (size_t id4 = 0; id4 < 4; id4++) {
747
+ update_with_candidate(buffered_ids[id4], dis[id4]);
748
+ }
749
+
750
+ n_buffered = 0;
751
+ }
752
+ }
753
+
754
+ // process leftovers
755
+ for (int icnt = 0; icnt < n_buffered; icnt++) {
756
+ float dis = qdis(buffered_ids[icnt]);
757
+ update_with_candidate(buffered_ids[icnt], dis);
758
+ }
759
+
760
+ // update stats
761
+ stats.ndis += ndis;
762
+ stats.nhops += 1;
763
+
764
+ if (nearest == prev_nearest) {
765
+ return stats;
766
+ }
767
+ }
768
+ }
769
+
770
+ } // namespace
771
+
772
+ /// greedily update a nearest vector at a given level
773
+ HNSWStats hnsw_detail::greedy_update_nearest(
774
+ const HNSW& hnsw,
775
+ DistanceComputer& qdis,
776
+ int level,
777
+ storage_idx_t& nearest,
778
+ float& d_nearest) {
779
+ if (hnsw.is_similarity) {
780
+ return greedy_update_nearest_impl<HNSW::C_similarity>(
781
+ hnsw, qdis, level, nearest, d_nearest);
782
+ }
783
+ return greedy_update_nearest_impl<HNSW::C_distance>(
784
+ hnsw, qdis, level, nearest, d_nearest);
785
+ }
786
+
787
+ namespace {
788
+
789
+ template <class C>
790
+ void add_with_locks_impl(
791
+ HNSW& hnsw,
538
792
  DistanceComputer& ptdis,
539
793
  int pt_level,
540
794
  int pt_id,
541
795
  LockVector& locks,
542
796
  VisitedTable& vt,
543
797
  bool keep_max_size_level0) {
544
- storage_idx_t nearest = entry_point;
798
+ storage_idx_t nearest = hnsw.entry_point;
545
799
  if (nearest == -1) { // avoid locking after the first point.
546
800
  #pragma omp critical
547
- if (entry_point == -1) { // double-check under lock.
548
- max_level = pt_level;
549
- entry_point = pt_id;
801
+ if (hnsw.entry_point == -1) { // double-check under lock.
802
+ hnsw.max_level = pt_level;
803
+ hnsw.entry_point = pt_id;
550
804
  // leave nearest = -1 to trigger early exit after critical block.
551
805
  } else {
552
806
  // else: Another thread set the entry point.
553
- nearest = entry_point;
807
+ nearest = hnsw.entry_point;
554
808
  }
555
809
  }
556
810
 
@@ -560,16 +814,17 @@ void HNSW::add_with_locks(
560
814
 
561
815
  locks.lock(pt_id);
562
816
 
563
- int level = max_level; // level at which we start adding neighbors
817
+ int level = hnsw.max_level; // level at which we start adding neighbors
564
818
  float d_nearest = ptdis(nearest);
565
819
 
566
820
  // greedy search on upper levels
567
821
  for (; level > pt_level; level--) {
568
- greedy_update_nearest(*this, ptdis, level, nearest, d_nearest);
822
+ greedy_update_nearest_impl<C>(hnsw, ptdis, level, nearest, d_nearest);
569
823
  }
570
824
 
571
825
  for (; level >= 0; level--) {
572
- add_links_starting_from(
826
+ add_links_starting_from_impl<C>(
827
+ hnsw,
573
828
  ptdis,
574
829
  pt_id,
575
830
  nearest,
@@ -584,22 +839,39 @@ void HNSW::add_with_locks(
584
839
 
585
840
  #pragma omp critical
586
841
  {
587
- if (pt_level > max_level) {
588
- max_level = pt_level;
589
- entry_point = pt_id;
842
+ if (pt_level > hnsw.max_level) {
843
+ hnsw.max_level = pt_level;
844
+ hnsw.entry_point = pt_id;
590
845
  }
591
846
  }
592
847
  }
593
848
 
849
+ } // namespace
850
+
851
+ void HNSW::add_with_locks(
852
+ DistanceComputer& ptdis,
853
+ int pt_level,
854
+ int pt_id,
855
+ LockVector& locks,
856
+ VisitedTable& vt,
857
+ bool keep_max_size_level0) {
858
+ if (is_similarity) {
859
+ add_with_locks_impl<C_similarity>(
860
+ *this, ptdis, pt_level, pt_id, locks, vt, keep_max_size_level0);
861
+ } else {
862
+ add_with_locks_impl<C_distance>(
863
+ *this, ptdis, pt_level, pt_id, locks, vt, keep_max_size_level0);
864
+ }
865
+ }
866
+
594
867
  /**************************************************************
595
868
  * Searching
596
869
  **************************************************************/
597
870
 
598
- using Node = HNSW::Node;
599
- using C = HNSW::C;
871
+ namespace {
600
872
 
601
873
  /** Helper to extract search parameters from HNSW and SearchParameters */
602
- static inline void extract_search_params(
874
+ inline void extract_search_params(
603
875
  const HNSW& hnsw,
604
876
  const SearchParameters* params,
605
877
  bool& do_dis_check,
@@ -619,13 +891,16 @@ static inline void extract_search_params(
619
891
  }
620
892
  }
621
893
 
622
- /** Do a BFS on the candidates list */
623
- int search_from_candidates(
894
+ /** Templated body of `search_from_candidates` instantiated once per
895
+ * VisitedTable subclass × comparator.
896
+ */
897
+ template <typename VTType, class C>
898
+ int search_from_candidates_fixVT(
624
899
  const HNSW& hnsw,
625
900
  DistanceComputer& qdis,
626
901
  ResultHandler& res,
627
- MinimaxHeap& candidates,
628
- VisitedTable& vt,
902
+ MinimaxHeapT<HC_for<C>>& candidates,
903
+ VTType& vt,
629
904
  HNSWStats& stats,
630
905
  int level,
631
906
  int nres_in,
@@ -638,13 +913,15 @@ int search_from_candidates(
638
913
  const IDSelector* sel;
639
914
  extract_search_params(hnsw, params, do_dis_check, efSearch, sel);
640
915
 
641
- C::T threshold = res.threshold;
916
+ vt.reserve(efSearch);
917
+
918
+ typename C::T threshold = res.threshold;
642
919
  for (int i = 0; i < candidates.size(); i++) {
643
920
  idx_t v1 = candidates.ids[i];
644
921
  float d = candidates.dis[i];
645
922
  FAISS_ASSERT(v1 >= 0);
646
923
  if (!sel || sel->is_member(v1)) {
647
- if (d < threshold) {
924
+ if (C::cmp(threshold, d)) {
648
925
  if (res.add_result(d, v1)) {
649
926
  threshold = res.threshold;
650
927
  }
@@ -693,7 +970,7 @@ int search_from_candidates(
693
970
 
694
971
  auto add_to_heap = [&](const size_t idx, const float dis) {
695
972
  if (!sel || sel->is_member(idx)) {
696
- if (dis < threshold) {
973
+ if (C::cmp(threshold, dis)) {
697
974
  if (res.add_result(dis, idx)) {
698
975
  threshold = res.threshold;
699
976
  nres += 1;
@@ -756,7 +1033,58 @@ int search_from_candidates(
756
1033
  return nres;
757
1034
  }
758
1035
 
759
- int search_from_candidates_panorama(
1036
+ /// Dispatches the VisitedTable concrete type for a given C, then calls
1037
+ /// the templated `search_from_candidates_fixVT<VTType, C>`.
1038
+ template <class C>
1039
+ int search_from_candidates_dispatch(
1040
+ const HNSW& hnsw,
1041
+ DistanceComputer& qdis,
1042
+ ResultHandler& res,
1043
+ MinimaxHeapT<HC_for<C>>& candidates,
1044
+ VisitedTable& vt,
1045
+ HNSWStats& stats,
1046
+ int level,
1047
+ int nres_in,
1048
+ const SearchParameters* params) {
1049
+ auto call = [&]<typename VTType>(VTType& vt_concrete) -> int {
1050
+ return search_from_candidates_fixVT<VTType, C>(
1051
+ hnsw,
1052
+ qdis,
1053
+ res,
1054
+ candidates,
1055
+ vt_concrete,
1056
+ stats,
1057
+ level,
1058
+ nres_in,
1059
+ params);
1060
+ };
1061
+ if (VisitedTableVector* vtv = dynamic_cast<VisitedTableVector*>(&vt)) {
1062
+ return call(*vtv);
1063
+ }
1064
+ VisitedTableSet& vts = dynamic_cast<VisitedTableSet&>(vt);
1065
+ return call(vts);
1066
+ }
1067
+
1068
+ } // namespace
1069
+
1070
+ /** Do a BFS on the candidates list. Public dispatcher: only handles the
1071
+ * distance (CMax) flavor because its `MinimaxHeap` parameter is the
1072
+ * CMax instantiation. */
1073
+ int hnsw_detail::search_from_candidates(
1074
+ const HNSW& hnsw,
1075
+ DistanceComputer& qdis,
1076
+ ResultHandler& res,
1077
+ MinimaxHeap& candidates,
1078
+ VisitedTable& vt,
1079
+ HNSWStats& stats,
1080
+ int level,
1081
+ int nres_in,
1082
+ const SearchParameters* params) {
1083
+ return search_from_candidates_dispatch<HNSW::C_distance>(
1084
+ hnsw, qdis, res, candidates, vt, stats, level, nres_in, params);
1085
+ }
1086
+
1087
+ int hnsw_detail::search_from_candidates_panorama(
760
1088
  const HNSW& hnsw,
761
1089
  const IndexHNSW* index,
762
1090
  DistanceComputer& qdis,
@@ -767,6 +1095,14 @@ int search_from_candidates_panorama(
767
1095
  int level,
768
1096
  int nres_in,
769
1097
  const SearchParameters* params) {
1098
+ // Panorama's progressive-bound math is L2-specific: refuse to run in
1099
+ // similarity mode.
1100
+ FAISS_THROW_IF_MSG(
1101
+ hnsw.is_similarity,
1102
+ "search_from_candidates_panorama does not support is_similarity=true");
1103
+
1104
+ using C = HNSW::C_distance;
1105
+
770
1106
  int nres = nres_in;
771
1107
  int ndis = 0;
772
1108
 
@@ -781,7 +1117,7 @@ int search_from_candidates_panorama(
781
1117
  float d = candidates.dis[i];
782
1118
  FAISS_ASSERT(v1 >= 0);
783
1119
  if (!sel || sel->is_member(v1)) {
784
- if (d < threshold) {
1120
+ if (C::cmp(threshold, d)) {
785
1121
  if (res.add_result(d, v1)) {
786
1122
  threshold = res.threshold;
787
1123
  }
@@ -801,177 +1137,129 @@ int search_from_candidates_panorama(
801
1137
  flat_codes_qdis,
802
1138
  "DistanceComputer must be a FlatCodesDistanceComputer");
803
1139
 
804
- // Allocate space for the index array and exact distances.
805
- size_t M = hnsw.nb_neighbors(0);
806
- std::vector<idx_t> index_array(M);
807
- std::vector<float> exact_distances(M);
1140
+ const auto& pano = panorama_index->pano;
1141
+ const size_t nb_per_parent = static_cast<size_t>(hnsw.nb_neighbors(level));
1142
+ const size_t num_panorama_levels = pano.n_levels;
1143
+ const size_t level_width_floats = pano.level_width_floats;
808
1144
 
809
- const float* query = flat_codes_qdis->q;
810
- std::vector<float> query_cum_sums(panorama_index->pano.n_levels + 1);
811
- panorama_index->pano.compute_query_cum_sums(query, query_cum_sums.data());
812
- float query_norm_sq = query_cum_sums[0] * query_cum_sums[0];
1145
+ constexpr size_t kTargetBatch = 64;
1146
+ const size_t buf_cap = kTargetBatch + nb_per_parent;
1147
+ std::vector<uint32_t> index_array(buf_cap);
1148
+ std::vector<float> exact_distances(buf_cap);
1149
+ std::vector<float> dot_buffer(buf_cap);
1150
+ std::vector<float> query_cum_sums_buf(num_panorama_levels + 1);
813
1151
 
814
- int nstep = 0;
1152
+ const float* query = flat_codes_qdis->q;
815
1153
  const size_t d = static_cast<size_t>(panorama_index->d);
816
1154
 
817
- PanoramaStats local_pano_stats;
818
- local_pano_stats.reset();
819
-
820
- while (candidates.size() > 0) {
821
- float d0 = 0;
822
- int v0 = candidates.pop_min(&d0);
1155
+ const float* cum_base = panorama_index->get_cum_sum(0);
1156
+ const size_t cum_stride = num_panorama_levels + 1;
1157
+ const auto* flat_storage =
1158
+ static_cast<const IndexFlat*>(panorama_index->storage);
1159
+ const float* xb_base =
1160
+ reinterpret_cast<const float*>(flat_storage->codes.data());
1161
+ const size_t feat_stride = d;
823
1162
 
824
- if (do_dis_check) {
825
- // tricky stopping condition: there are more than ef
826
- // distances that are processed already that are smaller
827
- // than d0
1163
+ pano.compute_query_cum_sums(query, query_cum_sums_buf.data());
1164
+ const float* query_cum_sums = query_cum_sums_buf.data();
1165
+ const float query_norm_sq = query_cum_sums[0] * query_cum_sums[0];
828
1166
 
829
- int n_dis_below = candidates.count_below(d0);
830
- if (n_dis_below >= efSearch) {
831
- break;
832
- }
833
- }
1167
+ int nstep = 0;
834
1168
 
835
- size_t begin, end;
836
- hnsw.neighbor_range(v0, level, &begin, &end);
1169
+ PanoramaStats local_pano_stats;
1170
+ local_pano_stats.reset();
837
1171
 
838
- // Unlike the vanilla HNSW, we already remove (and compact) the visited
839
- // nodes from the candidates list at this stage. We also remove nodes
840
- // that are not selected.
1172
+ bool stop_flag = false;
1173
+ while (candidates.size() > 0 && !stop_flag) {
841
1174
  size_t initial_size = 0;
842
- for (size_t j = begin; j < end; j++) {
843
- int v1 = hnsw.neighbors[j];
844
- if (v1 < 0) {
845
- break;
1175
+ size_t k_popped = 0;
1176
+ while (initial_size < kTargetBatch && candidates.size() > 0) {
1177
+ float d0 = 0;
1178
+ int v0 = candidates.pop_min(&d0);
1179
+ if (do_dis_check) {
1180
+ int n_dis_below = candidates.count_below(d0);
1181
+ if (n_dis_below >= efSearch) {
1182
+ if (k_popped == 0) {
1183
+ // Standard early-stop: nothing queued this
1184
+ // iteration, terminate the outer loop entirely
1185
+ // (matches the single-pop path's behavior).
1186
+ stop_flag = true;
1187
+ } else {
1188
+ // We already have parents queued; un-pop this
1189
+ // one so the next outer iteration sees it and
1190
+ // re-applies the stop check from a clean state.
1191
+ candidates.push(v0, d0);
1192
+ }
1193
+ break;
1194
+ }
846
1195
  }
1196
+ k_popped++;
847
1197
 
848
- const float* cum_sums_v1 = panorama_index->get_cum_sum(v1);
849
- index_array[initial_size] = v1;
850
- exact_distances[initial_size] =
851
- query_norm_sq + cum_sums_v1[0] * cum_sums_v1[0];
1198
+ size_t begin, end;
1199
+ hnsw.neighbor_range(v0, level, &begin, &end);
1200
+ for (size_t j = begin; j < end; j++) {
1201
+ int v1 = hnsw.neighbors[j];
1202
+ if (v1 < 0) {
1203
+ break;
1204
+ }
852
1205
 
853
- bool is_selected = !sel || sel->is_member(v1);
854
- initial_size += is_selected && vt.set(v1) ? 1 : 0;
1206
+ bool is_new = vt.set(v1);
1207
+ bool is_selected = !sel || sel->is_member(v1);
1208
+ if (is_new && is_selected) {
1209
+ const float vsum =
1210
+ cum_base[static_cast<size_t>(v1) * cum_stride];
1211
+ index_array[initial_size] = v1;
1212
+ exact_distances[initial_size] = query_norm_sq + vsum * vsum;
1213
+ initial_size++;
1214
+ }
1215
+ }
855
1216
  }
856
1217
 
857
1218
  local_pano_stats.total_dims += initial_size * d;
1219
+
858
1220
  size_t batch_size = initial_size;
859
1221
  size_t curr_panorama_level = 0;
860
- const size_t num_panorama_levels = panorama_index->pano.n_levels;
861
1222
  while (curr_panorama_level < num_panorama_levels && batch_size > 0) {
862
- float query_cum_norm = query_cum_sums[curr_panorama_level + 1];
863
-
864
- size_t start_dim = curr_panorama_level *
865
- panorama_index->pano.level_width_floats;
866
- size_t end_dim = (curr_panorama_level + 1) *
867
- panorama_index->pano.level_width_floats;
868
- end_dim = std::min(end_dim, static_cast<size_t>(panorama_index->d));
1223
+ const size_t cs_level_idx = curr_panorama_level + 1;
1224
+ const float query_cum_norm = query_cum_sums[cs_level_idx];
1225
+ const float two_qc = 2.0f * query_cum_norm;
1226
+
1227
+ const size_t start_dim = curr_panorama_level * level_width_floats;
1228
+ size_t end_dim = (curr_panorama_level + 1) * level_width_floats;
1229
+ end_dim = std::min(end_dim, d);
1230
+ const size_t dim_span = end_dim - start_dim;
1231
+
1232
+ const float* level_base = xb_base + start_dim;
1233
+ with_level_width(dim_span, [&]<size_t W>() {
1234
+ compute_level_dot_kernel<false, W>(
1235
+ query + start_dim,
1236
+ level_base,
1237
+ index_array.data(),
1238
+ batch_size,
1239
+ dim_span,
1240
+ dot_buffer.data(),
1241
+ feat_stride);
1242
+ });
1243
+ ndis += batch_size;
869
1244
 
870
- size_t i = 0;
871
1245
  size_t next_batch_size = 0;
872
- for (; i + 3 < batch_size; i += 4) {
873
- idx_t idx_0 = index_array[i];
874
- idx_t idx_1 = index_array[i + 1];
875
- idx_t idx_2 = index_array[i + 2];
876
- idx_t idx_3 = index_array[i + 3];
877
-
878
- float dp[4];
879
- flat_codes_qdis->partial_dot_product_batch_4(
880
- idx_0,
881
- idx_1,
882
- idx_2,
883
- idx_3,
884
- dp[0],
885
- dp[1],
886
- dp[2],
887
- dp[3],
888
- start_dim,
889
- end_dim - start_dim);
890
- ndis += 4;
891
1246
 
892
- float new_exact_0 = exact_distances[i + 0] - 2 * dp[0];
893
- float new_exact_1 = exact_distances[i + 1] - 2 * dp[1];
894
- float new_exact_2 = exact_distances[i + 2] - 2 * dp[2];
895
- float new_exact_3 = exact_distances[i + 3] - 2 * dp[3];
896
-
897
- float cum_sum_0 = panorama_index->get_cum_sum(
898
- idx_0)[curr_panorama_level + 1];
899
- float cum_sum_1 = panorama_index->get_cum_sum(
900
- idx_1)[curr_panorama_level + 1];
901
- float cum_sum_2 = panorama_index->get_cum_sum(
902
- idx_2)[curr_panorama_level + 1];
903
- float cum_sum_3 = panorama_index->get_cum_sum(
904
- idx_3)[curr_panorama_level + 1];
905
-
906
- float cs_bound_0 = 2.0f * cum_sum_0 * query_cum_norm;
907
- float cs_bound_1 = 2.0f * cum_sum_1 * query_cum_norm;
908
- float cs_bound_2 = 2.0f * cum_sum_2 * query_cum_norm;
909
- float cs_bound_3 = 2.0f * cum_sum_3 * query_cum_norm;
910
-
911
- float lower_bound_0 = new_exact_0 - cs_bound_0;
912
- float lower_bound_1 = new_exact_1 - cs_bound_1;
913
- float lower_bound_2 = new_exact_2 - cs_bound_2;
914
- float lower_bound_3 = new_exact_3 - cs_bound_3;
915
-
916
- // The following code is not the most branch friendly (due to
917
- // the maintenance of the candidate heap), but micro-benchmarks
918
- // have shown that it is not worth it to write horrible code to
919
- // squeeze out those cycles.
920
- if (lower_bound_0 <= threshold) {
921
- exact_distances[next_batch_size] = new_exact_0;
922
- index_array[next_batch_size] = idx_0;
923
- next_batch_size += 1;
1247
+ for (size_t i = 0; i < batch_size; i++) {
1248
+ float ne = exact_distances[i] - 2.0f * dot_buffer[i];
1249
+ float cum = cum_base
1250
+ [static_cast<size_t>(index_array[i]) * cum_stride +
1251
+ cs_level_idx];
1252
+ float lb = ne - two_qc * cum;
1253
+ if (lb <= threshold) {
1254
+ exact_distances[next_batch_size] = ne;
1255
+ index_array[next_batch_size] = index_array[i];
1256
+ next_batch_size++;
924
1257
  } else {
925
- candidates.push(idx_0, new_exact_0);
926
- }
927
- if (lower_bound_1 <= threshold) {
928
- exact_distances[next_batch_size] = new_exact_1;
929
- index_array[next_batch_size] = idx_1;
930
- next_batch_size += 1;
931
- } else {
932
- candidates.push(idx_1, new_exact_1);
933
- }
934
- if (lower_bound_2 <= threshold) {
935
- exact_distances[next_batch_size] = new_exact_2;
936
- index_array[next_batch_size] = idx_2;
937
- next_batch_size += 1;
938
- } else {
939
- candidates.push(idx_2, new_exact_2);
940
- }
941
- if (lower_bound_3 <= threshold) {
942
- exact_distances[next_batch_size] = new_exact_3;
943
- index_array[next_batch_size] = idx_3;
944
- next_batch_size += 1;
945
- } else {
946
- candidates.push(idx_3, new_exact_3);
947
- }
948
- }
949
-
950
- // Process the remaining candidates.
951
- for (; i < batch_size; i++) {
952
- idx_t idx = index_array[i];
953
-
954
- float dp = flat_codes_qdis->partial_dot_product(
955
- idx, start_dim, end_dim - start_dim);
956
- ndis += 1;
957
- float new_exact = exact_distances[i] - 2.0f * dp;
958
-
959
- float cum_sum = panorama_index->get_cum_sum(
960
- idx)[curr_panorama_level + 1];
961
- float cs_bound = 2.0f * cum_sum * query_cum_norm;
962
- float lower_bound = new_exact - cs_bound;
963
-
964
- if (lower_bound <= threshold) {
965
- exact_distances[next_batch_size] = new_exact;
966
- index_array[next_batch_size] = idx;
967
- next_batch_size += 1;
968
- } else {
969
- candidates.push(idx, new_exact);
1258
+ candidates.push(index_array[i], ne);
970
1259
  }
971
1260
  }
972
1261
 
973
- local_pano_stats.total_dims_scanned +=
974
- batch_size * (end_dim - start_dim);
1262
+ local_pano_stats.total_dims_scanned += batch_size * dim_span;
975
1263
  batch_size = next_batch_size;
976
1264
  curr_panorama_level++;
977
1265
  }
@@ -986,7 +1274,7 @@ int search_from_candidates_panorama(
986
1274
  candidates.push(idx, exact_distances[i]);
987
1275
  }
988
1276
 
989
- nstep++;
1277
+ nstep += static_cast<int>(k_popped);
990
1278
  if (!do_dis_check && nstep > efSearch) {
991
1279
  break;
992
1280
  }
@@ -1005,6 +1293,8 @@ int search_from_candidates_panorama(
1005
1293
  return nres;
1006
1294
  }
1007
1295
 
1296
+ namespace {
1297
+
1008
1298
  template <typename T, typename Container, typename Compare>
1009
1299
  void reservePriorityQueue(
1010
1300
  std::priority_queue<T, Container, Compare>& q,
@@ -1017,31 +1307,35 @@ void reservePriorityQueue(
1017
1307
  q = std::move(access);
1018
1308
  }
1019
1309
 
1020
- std::priority_queue<HNSW::Node> search_from_candidate_unbounded(
1310
+ /// Templated body of `search_from_candidate_unbounded`. The choice of
1311
+ /// max-heap vs min-heap for both `top_candidates` and `candidates` is
1312
+ /// derived from C via `TopCandidatesQueue` / `CandidatesQueue`.
1313
+ template <typename VTType, class C>
1314
+ TopCandidatesQueue<C> search_from_candidate_unbounded_fixVT(
1021
1315
  const HNSW& hnsw,
1022
- const Node& node,
1316
+ const HNSW::Node& node,
1023
1317
  DistanceComputer& qdis,
1024
1318
  int ef,
1025
- VisitedTable* vt,
1319
+ VTType& vt,
1026
1320
  HNSWStats& stats) {
1027
1321
  int ndis = 0;
1028
- std::priority_queue<Node> top_candidates;
1322
+ TopCandidatesQueue<C> top_candidates;
1029
1323
  reservePriorityQueue(top_candidates, ef);
1030
1324
 
1031
- std::priority_queue<Node, std::vector<Node>, std::greater<Node>> candidates;
1325
+ CandidatesQueue<C> candidates;
1032
1326
  reservePriorityQueue(candidates, ef);
1033
1327
 
1034
1328
  top_candidates.push(node);
1035
1329
  candidates.push(node);
1036
1330
 
1037
- vt->set(node.second);
1331
+ vt.set(node.second);
1038
1332
 
1039
1333
  while (!candidates.empty()) {
1040
1334
  float d0;
1041
1335
  storage_idx_t v0;
1042
1336
  std::tie(d0, v0) = candidates.top();
1043
1337
 
1044
- if (d0 > top_candidates.top().first) {
1338
+ if (C::cmp(d0, top_candidates.top().first)) {
1045
1339
  break;
1046
1340
  }
1047
1341
 
@@ -1059,7 +1353,7 @@ std::priority_queue<HNSW::Node> search_from_candidate_unbounded(
1059
1353
  break;
1060
1354
  }
1061
1355
 
1062
- vt->prefetch(v1);
1356
+ vt.prefetch(v1);
1063
1357
  jmax += 1;
1064
1358
  }
1065
1359
 
@@ -1067,12 +1361,12 @@ std::priority_queue<HNSW::Node> search_from_candidate_unbounded(
1067
1361
  size_t saved_j[4];
1068
1362
 
1069
1363
  auto add_to_heap = [&](const size_t idx, const float dis) {
1070
- if (top_candidates.top().first > dis ||
1071
- top_candidates.size() < ef) {
1364
+ if (C::cmp(top_candidates.top().first, dis) ||
1365
+ top_candidates.size() < static_cast<size_t>(ef)) {
1072
1366
  candidates.emplace(dis, idx);
1073
1367
  top_candidates.emplace(dis, idx);
1074
1368
 
1075
- if (top_candidates.size() > ef) {
1369
+ if (top_candidates.size() > static_cast<size_t>(ef)) {
1076
1370
  top_candidates.pop();
1077
1371
  }
1078
1372
  }
@@ -1082,7 +1376,7 @@ std::priority_queue<HNSW::Node> search_from_candidate_unbounded(
1082
1376
  int v1 = hnsw.neighbors[j];
1083
1377
 
1084
1378
  saved_j[counter] = v1;
1085
- counter += vt->set(v1) ? 1 : 0;
1379
+ counter += vt.set(v1) ? 1 : 0;
1086
1380
 
1087
1381
  if (counter == 4) {
1088
1382
  float dis[4];
@@ -1125,111 +1419,59 @@ std::priority_queue<HNSW::Node> search_from_candidate_unbounded(
1125
1419
  return top_candidates;
1126
1420
  }
1127
1421
 
1128
- /// greedily update a nearest vector at a given level
1129
- HNSWStats greedy_update_nearest(
1422
+ } // namespace
1423
+
1424
+ /// Public dispatcher: only the distance (CMax) flavor is exposed because
1425
+ /// its return type — `std::priority_queue<HNSW::Node>` — is the CMax
1426
+ /// max-heap. Internal callers that need similarity mode use the same
1427
+ /// dispatch pattern inline.
1428
+ std::priority_queue<HNSW::Node> hnsw_detail::search_from_candidate_unbounded(
1130
1429
  const HNSW& hnsw,
1430
+ const HNSW::Node& node,
1131
1431
  DistanceComputer& qdis,
1132
- int level,
1133
- storage_idx_t& nearest,
1134
- float& d_nearest) {
1135
- HNSWStats stats;
1136
-
1137
- for (;;) {
1138
- storage_idx_t prev_nearest = nearest;
1139
-
1140
- size_t begin, end;
1141
- hnsw.neighbor_range(nearest, level, &begin, &end);
1142
-
1143
- size_t ndis = 0;
1144
-
1145
- // a faster version: reference version in unit test test_hnsw.cpp
1146
- // the following version processes 4 neighbors at a time
1147
- auto update_with_candidate = [&](const storage_idx_t idx,
1148
- const float dis) {
1149
- if (dis < d_nearest) {
1150
- nearest = idx;
1151
- d_nearest = dis;
1152
- }
1153
- };
1154
-
1155
- int n_buffered = 0;
1156
- storage_idx_t buffered_ids[4];
1157
-
1158
- for (size_t j = begin; j < end; j++) {
1159
- storage_idx_t v = hnsw.neighbors[j];
1160
- if (v < 0) {
1161
- break;
1162
- }
1163
- ndis += 1;
1164
-
1165
- buffered_ids[n_buffered] = v;
1166
- n_buffered += 1;
1167
-
1168
- if (n_buffered == 4) {
1169
- float dis[4];
1170
- qdis.distances_batch_4(
1171
- buffered_ids[0],
1172
- buffered_ids[1],
1173
- buffered_ids[2],
1174
- buffered_ids[3],
1175
- dis[0],
1176
- dis[1],
1177
- dis[2],
1178
- dis[3]);
1179
-
1180
- for (size_t id4 = 0; id4 < 4; id4++) {
1181
- update_with_candidate(buffered_ids[id4], dis[id4]);
1182
- }
1183
-
1184
- n_buffered = 0;
1185
- }
1186
- }
1187
-
1188
- // process leftovers
1189
- for (int icnt = 0; icnt < n_buffered; icnt++) {
1190
- float dis = qdis(buffered_ids[icnt]);
1191
- update_with_candidate(buffered_ids[icnt], dis);
1192
- }
1193
-
1194
- // update stats
1195
- stats.ndis += ndis;
1196
- stats.nhops += 1;
1197
-
1198
- if (nearest == prev_nearest) {
1199
- return stats;
1200
- }
1432
+ int ef,
1433
+ VisitedTable* vt,
1434
+ HNSWStats& stats) {
1435
+ using C = HNSW::C_distance;
1436
+ auto call = [&]<typename VTType>(VTType& vt_concrete) {
1437
+ return search_from_candidate_unbounded_fixVT<VTType, C>(
1438
+ hnsw, node, qdis, ef, vt_concrete, stats);
1439
+ };
1440
+ if (VisitedTableVector* vtv = dynamic_cast<VisitedTableVector*>(vt)) {
1441
+ return call(*vtv);
1201
1442
  }
1443
+ VisitedTableSet& vts = dynamic_cast<VisitedTableSet&>(*vt);
1444
+ return call(vts);
1202
1445
  }
1203
1446
 
1204
1447
  namespace {
1205
- using Node = HNSW::Node;
1206
- using C = HNSW::C;
1207
1448
 
1208
1449
  // just used as a lower bound for the minmaxheap, but it is set for heap search
1450
+ template <class C>
1209
1451
  int extract_k_from_ResultHandler(ResultHandler& res) {
1210
1452
  using RH = HeapBlockResultHandler<C>;
1211
- if (auto hres = dynamic_cast<RH::SingleResultHandler*>(&res)) {
1453
+ if (auto hres = dynamic_cast<typename RH::SingleResultHandler*>(&res)) {
1212
1454
  return hres->k;
1213
1455
  }
1214
1456
  return 1;
1215
1457
  }
1216
1458
 
1217
- } // namespace
1218
-
1219
- HNSWStats HNSW::search(
1459
+ template <class C>
1460
+ HNSWStats search_impl(
1461
+ const HNSW& hnsw,
1220
1462
  DistanceComputer& qdis,
1221
1463
  const IndexHNSW* index,
1222
1464
  ResultHandler& res,
1223
1465
  VisitedTable& vt,
1224
- const SearchParameters* params) const {
1466
+ const SearchParameters* params) {
1225
1467
  HNSWStats stats;
1226
- if (entry_point == -1) {
1468
+ if (hnsw.entry_point == -1) {
1227
1469
  return stats;
1228
1470
  }
1229
- int k = extract_k_from_ResultHandler(res);
1471
+ int k = extract_k_from_ResultHandler<C>(res);
1230
1472
 
1231
- bool bounded_queue = this->search_bounded_queue;
1232
- int cur_efSearch = this->efSearch;
1473
+ bool bounded_queue = hnsw.search_bounded_queue;
1474
+ int cur_efSearch = hnsw.efSearch;
1233
1475
  if (params) {
1234
1476
  if (const SearchParametersHNSW* hnsw_params =
1235
1477
  dynamic_cast<const SearchParametersHNSW*>(params)) {
@@ -1239,42 +1481,63 @@ HNSWStats HNSW::search(
1239
1481
  }
1240
1482
 
1241
1483
  // greedy search on upper levels
1242
- storage_idx_t nearest = entry_point;
1484
+ storage_idx_t nearest = hnsw.entry_point;
1243
1485
  float d_nearest = qdis(nearest);
1244
1486
 
1245
- for (int level = max_level; level >= 1; level--) {
1246
- HNSWStats local_stats =
1247
- greedy_update_nearest(*this, qdis, level, nearest, d_nearest);
1487
+ for (int level = hnsw.max_level; level >= 1; level--) {
1488
+ HNSWStats local_stats = greedy_update_nearest_impl<C>(
1489
+ hnsw, qdis, level, nearest, d_nearest);
1248
1490
  stats.combine(local_stats);
1249
1491
  }
1250
1492
 
1251
1493
  int ef = std::max(cur_efSearch, k);
1252
1494
  if (bounded_queue) { // this is the most common branch, for now we only
1253
1495
  // support Panorama search in this branch
1254
- MinimaxHeap candidates(ef);
1496
+ MinimaxHeapT<HC_for<C>> candidates(ef);
1255
1497
 
1256
1498
  candidates.push(nearest, d_nearest);
1257
1499
 
1258
- if (!is_panorama) {
1259
- search_from_candidates(
1260
- *this, qdis, res, candidates, vt, stats, 0, 0, params);
1500
+ if (!hnsw.is_panorama) {
1501
+ search_from_candidates_dispatch<C>(
1502
+ hnsw, qdis, res, candidates, vt, stats, 0, 0, params);
1261
1503
  } else {
1262
- search_from_candidates_panorama(
1263
- *this,
1264
- index,
1265
- qdis,
1266
- res,
1267
- candidates,
1268
- vt,
1269
- stats,
1270
- 0,
1271
- 0,
1272
- params);
1504
+ // Panorama is L2-specific and is only valid for C_distance.
1505
+ // The public dispatch ensures we never reach this code path
1506
+ // with C != C_distance, but assert in debug builds.
1507
+ if constexpr (std::is_same_v<C, HNSW::C_distance>) {
1508
+ hnsw_detail::search_from_candidates_panorama(
1509
+ hnsw,
1510
+ index,
1511
+ qdis,
1512
+ res,
1513
+ candidates,
1514
+ vt,
1515
+ stats,
1516
+ 0,
1517
+ 0,
1518
+ params);
1519
+ } else {
1520
+ FAISS_THROW_MSG(
1521
+ "Panorama search is not supported with is_similarity=true");
1522
+ }
1273
1523
  }
1274
1524
  } else {
1275
- std::priority_queue<Node> top_candidates =
1276
- search_from_candidate_unbounded(
1277
- *this, Node(d_nearest, nearest), qdis, ef, &vt, stats);
1525
+ auto call = [&]<typename VTType>(VTType& vt_concrete) {
1526
+ return search_from_candidate_unbounded_fixVT<VTType, C>(
1527
+ hnsw,
1528
+ HNSW::Node(d_nearest, nearest),
1529
+ qdis,
1530
+ ef,
1531
+ vt_concrete,
1532
+ stats);
1533
+ };
1534
+ TopCandidatesQueue<C> top_candidates;
1535
+ if (VisitedTableVector* vtv = dynamic_cast<VisitedTableVector*>(&vt)) {
1536
+ top_candidates = call(*vtv);
1537
+ } else {
1538
+ VisitedTableSet& vts = dynamic_cast<VisitedTableSet&>(vt);
1539
+ top_candidates = call(vts);
1540
+ }
1278
1541
 
1279
1542
  while (top_candidates.size() > static_cast<size_t>(k)) {
1280
1543
  top_candidates.pop();
@@ -1294,7 +1557,9 @@ HNSWStats HNSW::search(
1294
1557
  return stats;
1295
1558
  }
1296
1559
 
1297
- void HNSW::search_level_0(
1560
+ template <class C>
1561
+ void search_level_0_impl(
1562
+ const HNSW& hnsw,
1298
1563
  DistanceComputer& qdis,
1299
1564
  ResultHandler& res,
1300
1565
  idx_t nprobe,
@@ -1303,9 +1568,7 @@ void HNSW::search_level_0(
1303
1568
  int search_type,
1304
1569
  HNSWStats& search_stats,
1305
1570
  VisitedTable& vt,
1306
- const SearchParameters* params) const {
1307
- const HNSW& hnsw = *this;
1308
-
1571
+ const SearchParameters* params) {
1309
1572
  auto cur_efSearch = hnsw.efSearch;
1310
1573
  if (params) {
1311
1574
  if (const SearchParametersHNSW* hnsw_params =
@@ -1314,7 +1577,7 @@ void HNSW::search_level_0(
1314
1577
  }
1315
1578
  }
1316
1579
 
1317
- int k = extract_k_from_ResultHandler(res);
1580
+ int k = extract_k_from_ResultHandler<C>(res);
1318
1581
 
1319
1582
  if (search_type == 1) {
1320
1583
  int nres = 0;
@@ -1331,11 +1594,11 @@ void HNSW::search_level_0(
1331
1594
  }
1332
1595
 
1333
1596
  int candidates_size = std::max(cur_efSearch, k);
1334
- MinimaxHeap candidates(candidates_size);
1597
+ MinimaxHeapT<HC_for<C>> candidates(candidates_size);
1335
1598
 
1336
1599
  candidates.push(cj, nearest_d[j]);
1337
1600
 
1338
- nres = search_from_candidates(
1601
+ nres = search_from_candidates_dispatch<C>(
1339
1602
  hnsw,
1340
1603
  qdis,
1341
1604
  res,
@@ -1351,7 +1614,7 @@ void HNSW::search_level_0(
1351
1614
  int candidates_size = std::max(cur_efSearch, int(k));
1352
1615
  candidates_size = std::max(candidates_size, int(nprobe));
1353
1616
 
1354
- MinimaxHeap candidates(candidates_size);
1617
+ MinimaxHeapT<HC_for<C>> candidates(candidates_size);
1355
1618
  for (idx_t j = 0; j < nprobe; j++) {
1356
1619
  storage_idx_t cj = nearest_i[j];
1357
1620
 
@@ -1361,11 +1624,62 @@ void HNSW::search_level_0(
1361
1624
  candidates.push(cj, nearest_d[j]);
1362
1625
  }
1363
1626
 
1364
- search_from_candidates(
1627
+ search_from_candidates_dispatch<C>(
1365
1628
  hnsw, qdis, res, candidates, vt, search_stats, 0, 0, params);
1366
1629
  }
1367
1630
  }
1368
1631
 
1632
+ } // namespace
1633
+
1634
+ HNSWStats HNSW::search(
1635
+ DistanceComputer& qdis,
1636
+ const IndexHNSW* index,
1637
+ ResultHandler& res,
1638
+ VisitedTable& vt,
1639
+ const SearchParameters* params) const {
1640
+ if (is_similarity) {
1641
+ return search_impl<C_similarity>(*this, qdis, index, res, vt, params);
1642
+ }
1643
+ return search_impl<C_distance>(*this, qdis, index, res, vt, params);
1644
+ }
1645
+
1646
+ void HNSW::search_level_0(
1647
+ DistanceComputer& qdis,
1648
+ ResultHandler& res,
1649
+ idx_t nprobe,
1650
+ const storage_idx_t* nearest_i,
1651
+ const float* nearest_d,
1652
+ int search_type,
1653
+ HNSWStats& search_stats,
1654
+ VisitedTable& vt,
1655
+ const SearchParameters* params) const {
1656
+ if (is_similarity) {
1657
+ search_level_0_impl<C_similarity>(
1658
+ *this,
1659
+ qdis,
1660
+ res,
1661
+ nprobe,
1662
+ nearest_i,
1663
+ nearest_d,
1664
+ search_type,
1665
+ search_stats,
1666
+ vt,
1667
+ params);
1668
+ } else {
1669
+ search_level_0_impl<C_distance>(
1670
+ *this,
1671
+ qdis,
1672
+ res,
1673
+ nprobe,
1674
+ nearest_i,
1675
+ nearest_d,
1676
+ search_type,
1677
+ search_stats,
1678
+ vt,
1679
+ params);
1680
+ }
1681
+ }
1682
+
1369
1683
  void HNSW::permute_entries(const idx_t* map) {
1370
1684
  // remap levels
1371
1685
  storage_idx_t ntotal = levels.size();