use crate::{
ANNResult,
graph::glue::{self, Accept, Decision},
neighbor::Neighbor,
provider::{DataProvider, HasId},
utils::VectorId,
};
pub trait QueryLabelProvider<I>: std::fmt::Debug + Send + Sync
where
I: VectorId,
{
fn is_match(&self, i: I) -> bool;
}
fn decide<T, I>(provider: &T, i: I) -> Decision<I>
where
I: VectorId,
T: QueryLabelProvider<I> + ?Sized,
{
if provider.is_match(i) {
Decision::accept(i)
} else {
Decision::reject(i)
}
}
#[derive(Debug, Clone, Copy)]
pub struct Filtered<'a, S, I> {
strategy: S,
labels: &'a dyn QueryLabelProvider<I>,
}
impl<'a, S, I> Filtered<'a, S, I> {
pub fn new(strategy: S, labels: &'a dyn QueryLabelProvider<I>) -> Self {
Self { strategy, labels }
}
pub fn labels(&self) -> &'a dyn QueryLabelProvider<I> {
self.labels
}
}
impl<'a, S, DP, T> glue::SearchStrategy<'a, DP, T> for Filtered<'_, S, DP::InternalId>
where
DP: DataProvider,
S: glue::SearchStrategy<'a, DP, T>,
{
type SearchAccessor = FilteredAccessor<'a, S::SearchAccessor>;
type SearchAccessorError = S::SearchAccessorError;
fn search_accessor(
&'a self,
provider: &'a DP,
context: &'a DP::Context,
query: T,
) -> Result<Self::SearchAccessor, Self::SearchAccessorError> {
let inner = self.strategy.search_accessor(provider, context, query)?;
Ok(FilteredAccessor {
inner,
labels: self.labels,
})
}
}
impl<'a, S, DP, T, O> glue::DefaultPostProcessor<'a, DP, T, O> for Filtered<'_, S, DP::InternalId>
where
S: glue::DefaultPostProcessor<'a, DP, T, O>,
DP: DataProvider,
O: Send,
{
type Processor = glue::Pipeline<Unfiltered, S::Processor>;
fn default_post_processor(&'a self) -> Self::Processor {
glue::Pipeline::new(Unfiltered, self.strategy.default_post_processor())
}
}
#[derive(Debug)]
pub struct FilteredAccessor<'a, A>
where
A: HasId,
{
inner: A,
labels: &'a dyn QueryLabelProvider<A::Id>,
}
impl<'a, A> FilteredAccessor<'a, A>
where
A: HasId,
{
#[cfg(test)]
pub(crate) fn new(inner: A, labels: &'a dyn QueryLabelProvider<A::Id>) -> Self {
Self { inner, labels }
}
}
impl<A> HasId for FilteredAccessor<'_, A>
where
A: HasId,
{
type Id = A::Id;
}
impl<'a, A> glue::FilteredAccessor for FilteredAccessor<'a, A>
where
A: glue::SearchAccessor,
{
async fn start_point_distances<F>(&mut self, mut f: F) -> ANNResult<()>
where
F: FnMut(Decision<Self::Id>, f32) + Send,
{
self.inner
.start_point_distances(|id, distance| f(decide(self.labels, id), distance))
.await
}
async fn expand_beam_filtered<Itr, P, F>(
&mut self,
ids: Itr,
pred: P,
mut on_neighbors: F,
) -> ANNResult<()>
where
Itr: Iterator<Item = Self::Id> + Send,
P: glue::HybridPredicate<Self::Id> + Send + Sync,
F: FnMut(Decision<Self::Id>, f32) + Send,
{
self.inner
.expand_beam(ids, pred, |id, distance| {
on_neighbors(decide(self.labels, id), distance)
})
.await
}
async fn expand_beam_accept_only<Itr, P, F>(
&mut self,
ids: Itr,
pred: P,
mut on_neighbors: F,
) -> ANNResult<()>
where
Itr: Iterator<Item = Self::Id> + Send,
P: glue::Predicate<Self::Id> + glue::PredicateMut<Accept<Self::Id>> + Send + Sync,
F: FnMut(Accept<Self::Id>, f32) + Send,
{
self.inner
.expand_beam(ids, EvalFiltered::new(pred, self.labels), |id, distance| {
on_neighbors(Accept::new(id), distance)
})
.await
}
fn terminate_early(&mut self) -> bool {
self.inner.terminate_early()
}
fn num_starting_points(&self) -> impl std::future::Future<Output = ANNResult<usize>> + Send {
self.inner.num_starting_points()
}
}
#[derive(Debug, Clone, Copy)]
pub struct Unfiltered;
impl<A, T, O> glue::SearchPostProcessStep<FilteredAccessor<'_, A>, T, O> for Unfiltered
where
A: HasId,
{
type Error<NextError>
= NextError
where
NextError: crate::error::StandardError;
type NextAccessor = A;
fn post_process_step<I, B, Next>(
&self,
next: &Next,
accessor: &mut FilteredAccessor<'_, A>,
query: T,
candidates: I,
output: &mut B,
) -> impl std::future::Future<Output = Result<usize, Self::Error<Next::Error>>> + Send
where
I: Iterator<Item = Neighbor<A::Id>> + Send,
B: crate::graph::SearchOutputBuffer<O> + Send + ?Sized,
Next: glue::SearchPostProcess<Self::NextAccessor, T, O> + Sync,
{
next.post_process(&mut accessor.inner, query, candidates, output)
}
}
#[derive(Debug)]
struct EvalFiltered<'a, P, I>
where
I: VectorId,
{
inner: P,
labels: &'a dyn QueryLabelProvider<I>,
}
impl<'a, P, I> EvalFiltered<'a, P, I>
where
I: VectorId,
{
fn new(inner: P, labels: &'a dyn QueryLabelProvider<I>) -> Self {
Self { inner, labels }
}
}
impl<P, I> glue::Predicate<I> for EvalFiltered<'_, P, I>
where
P: glue::Predicate<I>,
I: VectorId,
{
fn eval(&self, item: &I) -> bool {
self.inner.eval(item) && self.labels.is_match(*item)
}
}
impl<P, I> glue::PredicateMut<I> for EvalFiltered<'_, P, I>
where
P: glue::PredicateMut<Accept<I>>,
I: VectorId,
{
fn eval_mut(&mut self, item: &I) -> bool {
self.labels.is_match(*item) && self.inner.eval_mut(&Accept::new(*item))
}
}
impl<P, I> glue::HybridPredicate<I> for EvalFiltered<'_, P, I>
where
P: glue::Predicate<I> + glue::PredicateMut<Accept<I>>,
I: VectorId,
{
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use super::*;
use crate::graph::{
glue::{FilteredAccessor as _, HybridPredicate, Predicate, PredicateMut, SearchStrategy},
test::{provider as test_provider, synthetic::Grid},
};
#[derive(Debug)]
struct EvenFilter;
impl QueryLabelProvider<u32> for EvenFilter {
fn is_match(&self, id: u32) -> bool {
id.is_multiple_of(2)
}
}
struct NotIn<'a>(&'a mut HashSet<u32>);
impl Predicate<u32> for NotIn<'_> {
fn eval(&self, item: &u32) -> bool {
!self.0.contains(item)
}
}
impl Predicate<Accept<u32>> for NotIn<'_> {
fn eval(&self, item: &Accept<u32>) -> bool {
self.eval(item.get())
}
}
impl PredicateMut<u32> for NotIn<'_> {
fn eval_mut(&mut self, item: &u32) -> bool {
self.0.insert(*item)
}
}
impl PredicateMut<Accept<u32>> for NotIn<'_> {
fn eval_mut(&mut self, item: &Accept<u32>) -> bool {
self.eval_mut(item.get())
}
}
impl HybridPredicate<u32> for NotIn<'_> {}
impl HybridPredicate<Accept<u32>> for NotIn<'_> {}
#[tokio::test]
async fn filtered_accessor_wrapping() {
let provider = test_provider::Provider::grid(Grid::Two, 4).unwrap();
let filter = EvenFilter;
let inner = test_provider::Accessor::new(&provider, &[0.0, 0.0]).unwrap();
let mut accessor = FilteredAccessor::new(inner, &filter);
assert_eq!(accessor.num_starting_points().await.unwrap(), 1);
let mut start_results = Vec::new();
accessor
.start_point_distances(|d, dist| start_results.push((d, dist)))
.await
.unwrap();
assert_eq!(start_results.len(), 1);
assert!(start_results[0].0.is_reject());
let count_before = accessor.inner.get_vector_count();
let mut visited = HashSet::new();
let mut results_all: Vec<(Decision<u32>, f32)> = Vec::new();
accessor
.expand_beam_filtered(
[0, 5, 10, 15].into_iter(),
NotIn(&mut visited),
|d, dist| results_all.push((d, dist)),
)
.await
.unwrap();
let get_vector_all = accessor.inner.get_vector_count() - count_before;
assert_eq!(get_vector_all, results_all.len());
for (decision, _) in &results_all {
let id = decision.into_inner();
if id.is_multiple_of(2) {
assert!(decision.is_accept(), "even id {id} should be Accept");
} else {
assert!(decision.is_reject(), "odd id {id} should be Reject");
}
}
let count_before = accessor.inner.get_vector_count();
let mut visited2 = HashSet::new();
let mut results_accept: Vec<(Accept<u32>, f32)> = Vec::new();
accessor
.expand_beam_accept_only(
[0, 5, 10, 15].into_iter(),
NotIn(&mut visited2),
|d, dist| results_accept.push((d, dist)),
)
.await
.unwrap();
let get_vector_accept = accessor.inner.get_vector_count() - count_before;
assert!(
get_vector_accept < get_vector_all,
"AcceptOnly should compute fewer distances: {get_vector_accept} < {get_vector_all}"
);
assert_eq!(get_vector_accept, results_accept.len());
}
#[tokio::test]
async fn filtered_strategy_produces_accessor() {
let provider = test_provider::Provider::grid(Grid::Two, 4).unwrap();
let context = test_provider::Context::new();
let filter = EvenFilter;
let strategy = Filtered::new(test_provider::Strategy::new(), &filter);
let mut accessor = strategy
.search_accessor(&provider, &context, &[0.0, 0.0])
.unwrap();
let mut starts = Vec::new();
glue::FilteredAccessor::start_point_distances(&mut accessor, |decision, dist| {
starts.push((decision, dist));
})
.await
.unwrap();
assert_eq!(starts.len(), 1);
assert!(starts[0].0.is_reject());
assert_eq!(starts[0].0.into_inner(), u32::MAX);
}
#[test]
fn wrapper_predicate() {
let filter = EvenFilter;
let mut visited = HashSet::new();
let mut wrapper = EvalFiltered::new(NotIn(&mut visited), &filter);
assert!(wrapper.eval(&2), "2 is even and not visited");
assert!(!wrapper.eval(&3), "3 is odd → label rejects");
assert!(
wrapper.eval_mut(&4),
"4 is even and not visited → accept and insert"
);
assert!(!wrapper.eval_mut(&4), "4 is now visited → inner rejects");
assert!(!wrapper.eval_mut(&5), "5 is odd → label rejects");
assert!(!wrapper.eval(&4));
assert!(!wrapper.eval_mut(&4));
}
}