use crate::prune::CandidateSource;
#[cfg(feature = "vicinity")]
pub struct VicinityCandidates<'a, F> {
index: &'a vicinity::hnsw::HNSWIndex,
query: F,
k: usize,
ef: usize,
}
#[cfg(feature = "vicinity")]
impl<'a, F> VicinityCandidates<'a, F> {
pub fn new(index: &'a vicinity::hnsw::HNSWIndex, query: F, k: usize, ef: usize) -> Self {
Self {
index,
query,
k,
ef,
}
}
pub fn k(&self) -> usize {
self.k
}
pub fn ef(&self) -> usize {
self.ef
}
}
#[cfg(feature = "vicinity")]
impl<F> CandidateSource for VicinityCandidates<'_, F>
where
F: Fn(usize, usize) -> Option<Vec<f32>>,
{
fn candidates(&self, anchor: usize, relation: usize) -> Option<Vec<usize>> {
let query = (self.query)(anchor, relation)?;
self.index.search(&query, self.k, self.ef).ok().map(|hits| {
hits.into_iter()
.filter_map(|(id, _)| usize::try_from(id).ok())
.collect()
})
}
}
#[cfg(feature = "precinct")]
pub struct PrecinctSubsumers<'a, R: precinct::Region> {
index: &'a precinct::RegionIndex<R>,
regions: &'a [R],
relation: usize,
min_prob: f32,
ef: usize,
overretrieve: usize,
exclude_anchor: bool,
}
#[cfg(feature = "precinct")]
impl<'a, R: precinct::Region> PrecinctSubsumers<'a, R> {
pub fn new(index: &'a precinct::RegionIndex<R>, regions: &'a [R], relation: usize) -> Self {
let params = precinct::SearchParams::default();
Self {
index,
regions,
relation,
min_prob: 0.0,
ef: params.ef,
overretrieve: params.overretrieve,
exclude_anchor: true,
}
}
pub fn with_min_prob(mut self, min_prob: f32) -> Self {
self.min_prob = if min_prob.is_finite() {
min_prob.clamp(0.0, 1.0)
} else {
0.0
};
self
}
pub fn with_search_params(mut self, params: precinct::SearchParams) -> Self {
self.ef = params.ef;
self.overretrieve = params.overretrieve;
self
}
pub fn include_anchor(mut self) -> Self {
self.exclude_anchor = false;
self
}
fn params(&self) -> precinct::SearchParams {
precinct::SearchParams {
ef: self.ef,
overretrieve: self.overretrieve,
}
}
}
#[cfg(feature = "precinct")]
impl<R: precinct::Region> CandidateSource for PrecinctSubsumers<'_, R> {
fn candidates(&self, anchor: usize, relation: usize) -> Option<Vec<usize>> {
if relation != self.relation || anchor >= self.regions.len() {
return Some(Vec::new());
}
self.index
.subsumers_soft(&self.regions[anchor], self.min_prob, self.params())
.ok()
.map(|hits| {
hits.into_iter()
.filter_map(|(id, _)| usize::try_from(id).ok())
.filter(|&id| !self.exclude_anchor || id != anchor)
.collect()
})
}
}
#[cfg(feature = "hyperball")]
pub struct HyperbolicKnnCandidates<'a, F> {
ball: hyperball::core::PoincareBallCore<f32>,
points: &'a [Vec<f32>],
query: F,
k: usize,
}
#[cfg(feature = "hyperball")]
impl<'a, F> HyperbolicKnnCandidates<'a, F> {
pub fn new(
ball: hyperball::core::PoincareBallCore<f32>,
points: &'a [Vec<f32>],
query: F,
k: usize,
) -> Self {
Self {
ball,
points,
query,
k,
}
}
pub fn k(&self) -> usize {
self.k
}
}
#[cfg(feature = "hyperball")]
impl<F> CandidateSource for HyperbolicKnnCandidates<'_, F>
where
F: Fn(usize, usize) -> Option<Vec<f32>>,
{
fn candidates(&self, anchor: usize, relation: usize) -> Option<Vec<usize>> {
let query = (self.query)(anchor, relation)?;
let mut scored: Vec<(usize, f32)> = self
.points
.iter()
.enumerate()
.filter(|(_, point)| point.len() == query.len())
.map(|(id, point)| (id, self.ball.distance(&query, point)))
.collect();
scored.sort_unstable_by(|a, b| {
a.1.partial_cmp(&b.1)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.0.cmp(&b.0))
});
scored.truncate(self.k);
Some(scored.into_iter().map(|(id, _)| id).collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "hyperball")]
#[test]
fn hyperbolic_candidates_return_nearest_points() {
let points = vec![vec![0.0, 0.0], vec![0.2, 0.0], vec![0.6, 0.0]];
let source = HyperbolicKnnCandidates::new(
hyperball::core::PoincareBallCore::new(1.0),
&points,
|_anchor, _relation| Some(vec![0.19, 0.0]),
2,
);
assert_eq!(source.candidates(0, 0), Some(vec![1, 0]));
}
#[cfg(feature = "precinct")]
#[test]
fn precinct_subsumers_return_soft_ancestors() {
use precinct::{AxisBox, IndexParams, RegionIndex, SearchParams};
let boxes = vec![
AxisBox::new(vec![-2.0, -2.0], vec![2.0, 2.0]),
AxisBox::new(vec![-1.0, -1.0], vec![1.0, 1.0]),
AxisBox::new(vec![-0.25, -0.25], vec![0.25, 0.25]),
];
let mut index = RegionIndex::new(2, IndexParams::default()).unwrap();
for (id, region) in boxes.iter().cloned().enumerate() {
index.add(id as u32, region).unwrap();
}
index.build().unwrap();
let source = PrecinctSubsumers::new(&index, &boxes, 0)
.with_min_prob(0.5)
.with_search_params(SearchParams {
ef: 32,
overretrieve: 8,
});
let candidates = source.candidates(2, 0).unwrap();
assert!(candidates.contains(&0));
assert!(candidates.contains(&1));
assert!(!candidates.contains(&2));
}
#[cfg(feature = "vicinity")]
#[test]
fn vicinity_candidates_search_hnsw() {
let mut index = vicinity::hnsw::HNSWIndex::builder(2)
.metric(vicinity::DistanceMetric::L2)
.build()
.unwrap();
index.add_slice(0, &[0.0, 0.0]).unwrap();
index.add_slice(1, &[1.0, 0.0]).unwrap();
index.add_slice(2, &[3.0, 0.0]).unwrap();
index.build().unwrap();
let source =
VicinityCandidates::new(&index, |_anchor, _relation| Some(vec![0.9, 0.0]), 2, 8);
assert_eq!(source.candidates(0, 0).unwrap()[0], 1);
}
}