use roaring::RoaringBitmap;
use crate::distance::{DistanceMetric, distance};
use crate::error::{VectorError, check_dim};
use crate::hnsw::SearchResult;
use crate::hnsw::search::decode_filter_bitmap;
pub const DEFAULT_FLAT_INDEX_THRESHOLD: usize = 10_000;
pub struct FlatIndex {
dim: usize,
metric: DistanceMetric,
data: Vec<f32>,
deleted: Vec<bool>,
live_count: usize,
}
impl FlatIndex {
pub fn new(dim: usize, metric: DistanceMetric) -> Self {
Self {
dim,
metric,
data: Vec::new(),
deleted: Vec::new(),
live_count: 0,
}
}
pub fn insert(&mut self, vector: Vec<f32>) -> Result<u32, VectorError> {
check_dim(self.dim, vector.len())?;
let id = self.len() as u32;
self.data.extend_from_slice(&vector);
self.deleted.push(false);
self.live_count += 1;
Ok(id)
}
pub fn delete(&mut self, id: u32) -> bool {
let idx = id as usize;
if idx < self.deleted.len() && !self.deleted[idx] {
self.deleted[idx] = true;
self.live_count -= 1;
true
} else {
false
}
}
pub fn undelete(&mut self, id: u32) -> bool {
let idx = id as usize;
if idx < self.deleted.len() && self.deleted[idx] {
self.deleted[idx] = false;
self.live_count += 1;
true
} else {
false
}
}
pub fn search_with_metric(
&self,
query: &[f32],
top_k: usize,
metric: DistanceMetric,
) -> Result<Vec<SearchResult>, VectorError> {
self.scan(query, top_k, metric, None)
}
pub fn search(&self, query: &[f32], top_k: usize) -> Result<Vec<SearchResult>, VectorError> {
self.scan(query, top_k, self.metric, None)
}
pub fn search_filtered(
&self,
query: &[f32],
top_k: usize,
bitmap: &[u8],
) -> Result<Vec<SearchResult>, VectorError> {
self.search_filtered_offset(query, top_k, bitmap, 0)
}
pub fn search_filtered_offset_with_metric(
&self,
query: &[f32],
top_k: usize,
bitmap: &[u8],
id_offset: u32,
metric: DistanceMetric,
) -> Result<Vec<SearchResult>, VectorError> {
let filter = decode_filter_bitmap(bitmap)?;
self.scan(query, top_k, metric, Some((&filter, id_offset)))
}
pub fn search_filtered_offset(
&self,
query: &[f32],
top_k: usize,
bitmap: &[u8],
id_offset: u32,
) -> Result<Vec<SearchResult>, VectorError> {
self.search_filtered_offset_with_metric(query, top_k, bitmap, id_offset, self.metric)
}
fn scan(
&self,
query: &[f32],
top_k: usize,
metric: DistanceMetric,
filter: Option<(&RoaringBitmap, u32)>,
) -> Result<Vec<SearchResult>, VectorError> {
check_dim(self.dim, query.len())?;
let n = self.len();
if n == 0 || top_k == 0 {
return Ok(Vec::new());
}
let mut candidates: Vec<SearchResult> = Vec::with_capacity(n.min(top_k * 2));
for i in 0..n {
if self.deleted[i] {
continue;
}
if let Some((bitmap, id_offset)) = filter
&& !bitmap.contains((i as u32).saturating_add(id_offset))
{
continue;
}
let start = i * self.dim;
let vec_slice = &self.data[start..start + self.dim];
candidates.push(SearchResult {
id: i as u32,
distance: distance(query, vec_slice, metric),
});
}
if candidates.len() > top_k {
candidates.select_nth_unstable_by(top_k, |a, b| {
a.distance
.partial_cmp(&b.distance)
.unwrap_or(std::cmp::Ordering::Equal)
});
candidates.truncate(top_k);
}
candidates.sort_by(|a, b| {
a.distance
.partial_cmp(&b.distance)
.unwrap_or(std::cmp::Ordering::Equal)
});
Ok(candidates)
}
pub fn len(&self) -> usize {
self.deleted.len()
}
pub fn truncate(&mut self, len: usize) {
if len >= self.deleted.len() {
return;
}
let dropped_live = self.deleted[len..]
.iter()
.filter(|deleted| !**deleted)
.count();
self.live_count -= dropped_live;
self.deleted.truncate(len);
self.data.truncate(len * self.dim);
}
pub fn live_count(&self) -> usize {
self.live_count
}
pub fn is_empty(&self) -> bool {
self.live_count == 0
}
pub fn get_vector(&self, id: u32) -> Option<&[f32]> {
let idx = id as usize;
if idx < self.deleted.len() && !self.deleted[idx] {
let start = idx * self.dim;
Some(&self.data[start..start + self.dim])
} else {
None
}
}
pub fn get_vector_raw(&self, id: u32) -> Option<&[f32]> {
let idx = id as usize;
if idx < self.deleted.len() {
let start = idx * self.dim;
Some(&self.data[start..start + self.dim])
} else {
None
}
}
pub fn is_deleted(&self, id: u32) -> bool {
let idx = id as usize;
idx < self.deleted.len() && self.deleted[idx]
}
pub fn insert_tombstoned(&mut self, vector: Vec<f32>) -> Result<u32, VectorError> {
check_dim(self.dim, vector.len())?;
let id = self.len() as u32;
self.data.extend_from_slice(&vector);
self.deleted.push(true);
Ok(id)
}
pub fn dim(&self) -> usize {
self.dim
}
pub fn metric(&self) -> DistanceMetric {
self.metric
}
pub fn tombstone_count(&self) -> usize {
self.len().saturating_sub(self.live_count)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn insert_and_search() {
let mut idx = FlatIndex::new(3, DistanceMetric::L2);
for i in 0..100u32 {
idx.insert(vec![i as f32, 0.0, 0.0]).unwrap();
}
assert_eq!(idx.len(), 100);
assert_eq!(idx.live_count(), 100);
let results = idx.search(&[50.0, 0.0, 0.0], 3).unwrap();
assert_eq!(results.len(), 3);
assert_eq!(results[0].id, 50);
assert!(results[0].distance < 0.01);
}
#[test]
fn delete_excludes_from_search() {
let mut idx = FlatIndex::new(2, DistanceMetric::L2);
idx.insert(vec![0.0, 0.0]).unwrap();
idx.insert(vec![1.0, 0.0]).unwrap();
idx.insert(vec![2.0, 0.0]).unwrap();
assert!(idx.delete(1));
assert_eq!(idx.live_count(), 2);
let results = idx.search(&[1.0, 0.0], 3).unwrap();
assert_eq!(results.len(), 2);
assert!(results.iter().all(|r| r.id != 1));
}
#[test]
fn exact_results() {
let mut idx = FlatIndex::new(2, DistanceMetric::Cosine);
idx.insert(vec![1.0, 0.0]).unwrap();
idx.insert(vec![0.0, 1.0]).unwrap();
idx.insert(vec![1.0, 1.0]).unwrap();
let results = idx.search(&[1.0, 0.0], 1).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, 0);
}
#[test]
fn empty_search() {
let idx = FlatIndex::new(3, DistanceMetric::L2);
let results = idx.search(&[1.0, 0.0, 0.0], 5).unwrap();
assert!(results.is_empty());
}
#[test]
fn filtered_search() {
let mut idx = FlatIndex::new(2, DistanceMetric::L2);
for i in 0..8u32 {
idx.insert(vec![i as f32, 0.0]).unwrap();
}
let filter: RoaringBitmap = [2u32, 3, 6, 7].into_iter().collect();
let mut bitmap = Vec::new();
filter.serialize_into(&mut bitmap).unwrap();
let results = idx.search_filtered(&[4.0, 0.0], 2, &bitmap).unwrap();
assert_eq!(results.len(), 2);
assert_eq!(results[0].id, 3);
assert!(results.iter().all(|r| filter.contains(r.id)), "{results:?}");
}
#[test]
fn wrong_dimension_is_a_typed_error() {
let mut idx = FlatIndex::new(2, DistanceMetric::L2);
assert!(matches!(
idx.insert(vec![1.0, 2.0, 3.0]),
Err(VectorError::DimensionMismatch {
expected: 2,
got: 3
})
));
assert!(matches!(
idx.insert_tombstoned(vec![1.0]),
Err(VectorError::DimensionMismatch {
expected: 2,
got: 1
})
));
idx.insert(vec![1.0, 0.0]).unwrap();
let filter: RoaringBitmap = [0u32].into_iter().collect();
let mut bitmap = Vec::new();
filter.serialize_into(&mut bitmap).unwrap();
let short = [1.0_f32];
for result in [
idx.search(&short, 1),
idx.search_with_metric(&short, 1, DistanceMetric::Cosine),
idx.search_filtered(&short, 1, &bitmap),
idx.search_filtered_offset(&short, 1, &bitmap, 0),
idx.search_filtered_offset_with_metric(&short, 1, &bitmap, 0, DistanceMetric::L2),
] {
assert!(
matches!(
result,
Err(VectorError::DimensionMismatch {
expected: 2,
got: 1
})
),
"{result:?}"
);
}
}
#[test]
fn undecodable_filter_bitmap_is_a_typed_error() {
let mut idx = FlatIndex::new(2, DistanceMetric::L2);
idx.insert(vec![1.0, 0.0]).unwrap();
let result = idx.search_filtered(&[1.0, 0.0], 1, &[0b1100_1100]);
assert!(
matches!(result, Err(VectorError::InvalidFilterBitmap { .. })),
"{result:?}"
);
}
}