88#include < string>
99#include < algorithm>
1010
11+ #include < array>
12+ #include < unordered_map>
13+ #include < limits>
14+
1115#include " deglib/distances.h"
1216#include " deglib/filter.h"
17+ #include " deglib/graph/visited_list_pool.h"
18+ #include " deglib/utils/memory.h"
1319
1420// Forward declaration for friend access
1521namespace deglib ::builder {
@@ -202,6 +208,10 @@ class InternalGraph
202208 return search_intern ({ entry_vertex_index }, query_ptr, k, eps, include_entry, filter, max_distance_computation_count);
203209 }
204210
211+ protected:
212+ /* *
213+ * Virtual internal search entry point implemented by derived graph classes.
214+ */
205215 virtual deglib::graph::ResultSet search_intern (
206216 const std::vector<uint32_t >& entry_vertex_indices,
207217 const std::byte* query,
@@ -211,6 +221,332 @@ class InternalGraph
211221 const deglib::search::Filter* filter = nullptr ,
212222 const uint32_t max_distance_computation_count = 0 ) const = 0;
213223
224+ /* *
225+ * Statically dispatched exploration and k-NN search implementation.
226+ * Inlines graph member accesses (features, neighbors, labels) via GraphType to avoid vtable calls.
227+ */
228+ template <typename GraphType, deglib::distances::DistanceFunction COMPARATOR , bool use_max_distance_count, bool use_filter>
229+ static deglib::graph::ResultSet searchImpl (
230+ const GraphType& self,
231+ const std::vector<uint32_t >& entry_vertex_indices,
232+ const std::byte* query,
233+ const uint32_t initial_k,
234+ const float eps,
235+ const bool include_entry,
236+ const deglib::search::Filter* filter,
237+ const uint32_t max_distance_computation_count)
238+ {
239+ uint32_t distance_computation_count = 0 ;
240+ const auto dist_func_param = self.feature_space_ .get_dist_func_param ();
241+ const auto feature_size = self.feature_space_ .get_data_size ();
242+ const size_t vertex_count = self.size ();
243+ size_t k = std::min (vertex_count, static_cast <size_t >(initial_k));
244+
245+ // set of checked vertex ids
246+ const auto vl = self.visited_list_pool_ ->getFreeVisitedList ();
247+ auto * checked_ids = vl->get_visited ();
248+ const auto checked_ids_tag = vl->get_tag ();
249+
250+ // items to traverse next
251+ auto next_vertices = deglib::graph::UncheckedSet ();
252+ next_vertices.reserve (k * self.edges_per_vertex_ );
253+
254+ // result set
255+ auto results = deglib::graph::ResultSet ();
256+ results.reserve (k + 1 );
257+
258+ // if the filter only contains few valid ids brute force them all
259+ if constexpr (use_filter) {
260+ if (vertex_count < 1'000 || (filter->get_inclusion_rate () * vertex_count) < 10'000 || filter->get_inclusion_rate () < 0 .10f ) {
261+ auto radius = std::numeric_limits<float >::max ();
262+ filter->for_each_valid_label ([&](uint32_t valid_label) {
263+ auto valid_index = self.getInternalIndex (valid_label);
264+ const auto feature = reinterpret_cast <const float *>(self.feature_by_index (valid_index));
265+ const auto distance = COMPARATOR::compare (query, feature, dist_func_param);
266+
267+ // remember the vertex, if its better than the worst in the result list
268+ if (distance < radius) {
269+ results.emplace (valid_index, distance);
270+
271+ // update the search radius
272+ if (results.size () > k) {
273+ results.pop ();
274+ radius = results.top ().getDistance ();
275+ }
276+ }
277+ });
278+ return results;
279+ }
280+ }
281+
282+ // copy the initial entry vertices and their distances to the query into the three containers
283+ for (auto && index : entry_vertex_indices) {
284+ if (checked_ids[index] != checked_ids_tag) {
285+ checked_ids[index] = checked_ids_tag;
286+
287+ const auto feature = self.feature_by_index (index);
288+ const auto distance = COMPARATOR::compare (query, feature, dist_func_param);
289+ next_vertices.emplace (index, distance);
290+ if (include_entry) {
291+ if constexpr (use_filter) {
292+ if (filter->is_valid (self.label_by_index (index))) {
293+ results.emplace (index, distance);
294+ }
295+ } else {
296+ results.emplace (index, distance);
297+ }
298+ }
299+
300+ // early stop after to many computations
301+ if constexpr (use_max_distance_count) {
302+ if (++distance_computation_count >= max_distance_computation_count) {
303+ return results;
304+ }
305+ }
306+ }
307+ }
308+
309+ // search radius
310+ auto radius = std::numeric_limits<float >::max ();
311+ auto exploration_radius = radius;
312+
313+ // iterate as long as good elements are in the next_vertices queue
314+ auto good_neighbors = std::array<uint32_t , 256 >();
315+ alignas (32 ) auto db_arr = std::array<const void *, 256 >();
316+ alignas (32 ) auto dists = std::array<float , 256 >();
317+ while (next_vertices.empty () == false )
318+ {
319+ // next vertex to check
320+ const auto next_vertex = next_vertices.top ();
321+ next_vertices.pop ();
322+
323+ // max distance reached
324+ if (next_vertex.getDistance () > exploration_radius)
325+ break ;
326+
327+ size_t good_neighbor_count = 0 ;
328+ const auto neighbor_indices = self.neighbors_by_index (next_vertex.getIdentifier ());
329+ for (size_t i = 0 ; i < self.edges_per_vertex_ ; i++) {
330+ const auto neighbor_index = neighbor_indices[i];
331+ if (checked_ids[neighbor_index] != checked_ids_tag) {
332+ checked_ids[neighbor_index] = checked_ids_tag;
333+ good_neighbors[good_neighbor_count++] = neighbor_index;
334+ }
335+ }
336+
337+ if (good_neighbor_count == 0 )
338+ continue ;
339+
340+ // Cap the neighbor count based on the remaining distance budget
341+ if constexpr (use_max_distance_count) {
342+ if (distance_computation_count + good_neighbor_count > max_distance_computation_count) {
343+ good_neighbor_count = max_distance_computation_count - distance_computation_count;
344+ }
345+ }
346+
347+ // Construct features pointer array
348+ for (size_t i = 0 ; i < good_neighbor_count; ++i) {
349+ db_arr[i] = self.feature_by_index (good_neighbors[i]);
350+ if (i < 8 )
351+ memory::prefetch (reinterpret_cast <const char *>(db_arr[i]), feature_size);
352+ }
353+
354+ // Compute distances in batch
355+ COMPARATOR::compare_batch (query, db_arr.data (), good_neighbor_count, dist_func_param, dists.data ());
356+
357+ // Process results sequentially
358+ for (size_t i = 0 ; i < good_neighbor_count; ++i) {
359+ const auto neighbor_index = good_neighbors[i];
360+ const auto neighbor_distance = dists[i];
361+
362+ // check the neighborhood of this vertex later, if its good enough
363+ if (neighbor_distance <= exploration_radius) {
364+ next_vertices.emplace (neighbor_index, neighbor_distance);
365+
366+ // remember the vertex, if its better than the worst in the result list
367+ if (neighbor_distance < radius) {
368+ if constexpr (use_filter) {
369+ if (filter->is_valid (self.label_by_index (neighbor_index))) {
370+ results.emplace (neighbor_index, neighbor_distance);
371+ }
372+ } else {
373+ results.emplace (neighbor_index, neighbor_distance);
374+ }
375+
376+ // update the search radius
377+ if (results.size () > k) {
378+ results.pop ();
379+ radius = results.top ().getDistance ();
380+ exploration_radius = radius * ((radius < 0 ) ? (1 - eps) : (1 + eps));
381+ }
382+ }
383+ }
384+ }
385+
386+ if constexpr (use_max_distance_count) {
387+ distance_computation_count += good_neighbor_count;
388+ if (distance_computation_count >= max_distance_computation_count) {
389+ return results;
390+ }
391+ }
392+ }
393+
394+ return results;
395+ }
396+
397+ /* *
398+ * Dispatches runtime metric and filter settings to compile-time specialized searchImpl instantiations.
399+ */
400+ template <typename GraphType>
401+ static deglib::graph::ResultSet searchInternImpl (
402+ const GraphType& self,
403+ const std::vector<uint32_t >& entry_vertex_indices,
404+ const std::byte* query,
405+ const uint32_t k,
406+ const float eps = 0 .0f ,
407+ const bool include_entry = true ,
408+ const deglib::search::Filter* filter = nullptr ,
409+ const uint32_t max_distance_computation_count = 0 )
410+ {
411+ return self.feature_space_ .compute ([&]<deglib::distances::DistanceFunction Dist>(Dist) -> deglib::graph::ResultSet {
412+ if (filter) {
413+ if (max_distance_computation_count == 0 ) {
414+ return searchImpl<GraphType, Dist, false , true >(self, entry_vertex_indices, query, k, eps, include_entry, filter, 0 );
415+ } else {
416+ return searchImpl<GraphType, Dist, true , true >(self, entry_vertex_indices, query, k, eps, include_entry, filter, max_distance_computation_count);
417+ }
418+ } else {
419+ if (max_distance_computation_count == 0 ) {
420+ return searchImpl<GraphType, Dist, false , false >(self, entry_vertex_indices, query, k, eps, include_entry, nullptr , 0 );
421+ } else {
422+ return searchImpl<GraphType, Dist, true , false >(self, entry_vertex_indices, query, k, eps, include_entry, nullptr , max_distance_computation_count);
423+ }
424+ }
425+ });
426+ }
427+
428+ /* *
429+ * Greedy best-first reachability search.
430+ * Backtracks predecessors and returns an ordered path from to_vertex back to entry.
431+ */
432+ template <typename GraphType>
433+ static std::vector<deglib::graph::ObjectDistance> hasPathImpl (
434+ const GraphType& self,
435+ const std::vector<uint32_t >& entry_vertex_indices,
436+ const uint32_t to_vertex,
437+ const float eps,
438+ const uint32_t k)
439+ {
440+ const auto query = self.feature_by_index (to_vertex);
441+ const auto dist_func = self.feature_space_ .get_dist_func ();
442+ const auto dist_func_param = self.feature_space_ .get_dist_func_param ();
443+ const auto feature_size = self.feature_space_ .get_data_size ();
444+
445+ // set of checked vertex ids
446+ const auto vl = self.visited_list_pool_ ->getFreeVisitedList ();
447+ auto * checked_ids = vl->get_visited ();
448+ const auto checked_ids_tag = vl->get_tag ();
449+
450+ // items to traverse next
451+ auto next_vertices = deglib::graph::UncheckedSet ();
452+
453+ // trackable information
454+ auto trackback = std::unordered_map<uint32_t , deglib::graph::ObjectDistance>();
455+
456+ // result set
457+ auto results = deglib::graph::ResultSet ();
458+
459+ // copy the initial entry vertices and their distances to the query into the three containers
460+ for (auto && index : entry_vertex_indices) {
461+ if (checked_ids[index] != checked_ids_tag) {
462+ checked_ids[index] = checked_ids_tag;
463+
464+ const auto feature = self.feature_by_index (index);
465+ const auto distance = dist_func (query, feature, dist_func_param);
466+ results.emplace (index, distance);
467+ next_vertices.emplace (index, distance);
468+ trackback.emplace (index, deglib::graph::ObjectDistance (index, distance));
469+ }
470+ }
471+
472+ // search radius
473+ auto radius = std::numeric_limits<float >::max ();
474+ auto exploration_radius = radius;
475+
476+ // iterate as long as good elements are in the next_vertices queue
477+ auto good_neighbors = std::array<uint32_t , 256 >();
478+ while (next_vertices.empty () == false )
479+ {
480+ // next vertex to check
481+ const auto next_vertex = next_vertices.top ();
482+ next_vertices.pop ();
483+
484+ // max distance reached
485+ if (next_vertex.getDistance () > exploration_radius)
486+ break ;
487+
488+ size_t good_neighbor_count = 0 ;
489+ const auto neighbor_indices = self.neighbors_by_index (next_vertex.getIdentifier ());
490+ for (size_t i = 0 ; i < self.edges_per_vertex_ ; i++) {
491+ const auto neighbor_index = neighbor_indices[i];
492+
493+ // found our target vertex, create a path back to the entry vertex
494+ if (neighbor_index == to_vertex) {
495+ auto path = std::vector<deglib::graph::ObjectDistance>();
496+ path.emplace_back (to_vertex, 0 .f );
497+ path.emplace_back (next_vertex.getIdentifier (), next_vertex.getDistance ());
498+
499+ auto last_vertex = trackback.find (next_vertex.getIdentifier ());
500+ while (last_vertex != trackback.cend () && last_vertex->first != last_vertex->second .getIdentifier ()) {
501+ path.emplace_back (last_vertex->second .getIdentifier (), last_vertex->second .getDistance ());
502+ last_vertex = trackback.find (last_vertex->second .getIdentifier ());
503+ }
504+
505+ return path;
506+ }
507+
508+ // collect
509+ if (checked_ids[neighbor_index] != checked_ids_tag) {
510+ checked_ids[neighbor_index] = checked_ids_tag;
511+ good_neighbors[good_neighbor_count++] = neighbor_index;
512+ }
513+ }
514+
515+ if (good_neighbor_count == 0 )
516+ continue ;
517+
518+ memory::prefetch (reinterpret_cast <const char *>(self.feature_by_index (good_neighbors[0 ])), feature_size);
519+ for (size_t i = 0 ; i < good_neighbor_count; i++) {
520+ memory::prefetch (reinterpret_cast <const char *>(self.feature_by_index (good_neighbors[std::min (i + 1 , good_neighbor_count - 1 )])), feature_size);
521+
522+ const auto neighbor_index = good_neighbors[i];
523+ const auto neighbor_feature_vector = self.feature_by_index (neighbor_index);
524+ const auto neighbor_distance = dist_func (query, neighbor_feature_vector, dist_func_param);
525+
526+ // check the neighborhood of this vertex later, if its good enough
527+ if (neighbor_distance <= exploration_radius) {
528+ next_vertices.emplace (neighbor_index, neighbor_distance);
529+ trackback.insert ({neighbor_index, deglib::graph::ObjectDistance (next_vertex.getIdentifier (), next_vertex.getDistance ())});
530+
531+ // remember the vertex, if its better than the worst in the result list
532+ if (neighbor_distance < radius) {
533+ results.emplace (neighbor_index, neighbor_distance);
534+
535+ // update the search radius
536+ if (results.size () > k) {
537+ results.pop ();
538+ radius = results.top ().getDistance ();
539+ exploration_radius = radius * ((radius < 0 ) ? (1 - eps) : (1 + eps));
540+ }
541+ }
542+ }
543+ }
544+ }
545+
546+ // there is no path
547+ return std::vector<deglib::graph::ObjectDistance>();
548+ }
549+
214550 friend class deglib ::builder::EvenRegularGraphBuilder;
215551};
216552
0 commit comments