| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| use crate::distance::{preprocess_vectors, MetricType, QueryDistance}; |
| use crate::ivfpq::RowIdFilter; |
| use crate::kmeans::{self, KMeansConfig}; |
| |
| /// IVF-FLAT index. Stores raw vectors in each IVF list for exact per-list scan. |
| pub struct IVFFlatIndex { |
| pub d: usize, |
| pub nlist: usize, |
| pub metric: MetricType, |
| pub quantizer_centroids: Vec<f32>, |
| pub ids: Vec<Vec<i64>>, |
| pub vectors: Vec<Vec<f32>>, |
| } |
| |
| impl IVFFlatIndex { |
| pub fn new(d: usize, nlist: usize, metric: MetricType) -> Self { |
| IVFFlatIndex { |
| d, |
| nlist, |
| metric, |
| quantizer_centroids: Vec::new(), |
| ids: vec![Vec::new(); nlist], |
| vectors: vec![Vec::new(); nlist], |
| } |
| } |
| |
| pub fn train(&mut self, data: &[f32], n: usize) { |
| let train_data = self.preprocess_vectors(data, n); |
| self.quantizer_centroids = |
| kmeans::kmeans_train(&KMeansConfig::default(), &train_data, n, self.d, self.nlist); |
| } |
| |
| pub fn add(&mut self, data: &[f32], ids: &[i64], n: usize) { |
| let processed = self.preprocess_vectors(data, n); |
| let list_ids = kmeans::find_nearest_batch( |
| &processed, |
| n, |
| &self.quantizer_centroids, |
| self.nlist, |
| self.d, |
| ); |
| for i in 0..n { |
| let vector = &processed[i * self.d..(i + 1) * self.d]; |
| let list_id = list_ids[i]; |
| self.ids[list_id].push(ids[i]); |
| self.vectors[list_id].extend_from_slice(vector); |
| } |
| } |
| |
| pub fn total_vectors(&self) -> usize { |
| self.ids.iter().map(Vec::len).sum() |
| } |
| |
| pub fn search( |
| &self, |
| queries: &[f32], |
| nq: usize, |
| k: usize, |
| nprobe: usize, |
| result_distances: &mut [f32], |
| result_labels: &mut [i64], |
| ) { |
| self.search_with_filter( |
| queries, |
| nq, |
| k, |
| nprobe, |
| None, |
| result_distances, |
| result_labels, |
| ); |
| } |
| |
| pub fn search_with_filter( |
| &self, |
| queries: &[f32], |
| nq: usize, |
| k: usize, |
| nprobe: usize, |
| filter: Option<&dyn RowIdFilter>, |
| result_distances: &mut [f32], |
| result_labels: &mut [i64], |
| ) { |
| let processed_queries = self.preprocess_vectors(queries, nq); |
| let (all_probe_indices, _) = kmeans::find_topk_batch( |
| &processed_queries, |
| nq, |
| &self.quantizer_centroids, |
| self.nlist, |
| self.d, |
| nprobe, |
| ); |
| |
| for qi in 0..nq { |
| let query = &processed_queries[qi * self.d..(qi + 1) * self.d]; |
| let distance = QueryDistance::new(query, self.metric); |
| let mut heap = FlatTopKHeap::new(k); |
| |
| for &list_id in &all_probe_indices[qi] { |
| let ids = &self.ids[list_id]; |
| let vectors = &self.vectors[list_id]; |
| for (local_idx, &id) in ids.iter().enumerate() { |
| if let Some(f) = filter { |
| if !f.contains(id) { |
| continue; |
| } |
| } |
| let vector = &vectors[local_idx * self.d..(local_idx + 1) * self.d]; |
| heap.push(distance.distance_to(vector, None), id); |
| } |
| } |
| |
| let sorted = heap.into_sorted(); |
| let out_base = qi * k; |
| for (i, &(dist, id)) in sorted.iter().enumerate() { |
| result_distances[out_base + i] = dist; |
| result_labels[out_base + i] = id; |
| } |
| for i in sorted.len()..k { |
| result_distances[out_base + i] = f32::MAX; |
| result_labels[out_base + i] = -1; |
| } |
| } |
| } |
| |
| pub(crate) fn preprocess_vectors(&self, data: &[f32], n: usize) -> Vec<f32> { |
| preprocess_vectors(data, n, self.d, self.metric) |
| } |
| } |
| |
| struct FlatTopKHeap { |
| k: usize, |
| data: Vec<(f32, i64)>, |
| } |
| |
| impl FlatTopKHeap { |
| fn new(k: usize) -> Self { |
| Self { |
| k, |
| data: Vec::with_capacity(k), |
| } |
| } |
| |
| fn push(&mut self, dist: f32, id: i64) { |
| if self.k == 0 { |
| return; |
| } |
| if self.data.len() < self.k { |
| self.data.push((dist, id)); |
| return; |
| } |
| if let Some((worst_idx, _)) = self |
| .data |
| .iter() |
| .enumerate() |
| .max_by(|(_, a), (_, b)| a.0.partial_cmp(&b.0).unwrap()) |
| { |
| if dist < self.data[worst_idx].0 { |
| self.data[worst_idx] = (dist, id); |
| } |
| } |
| } |
| |
| fn into_sorted(mut self) -> Vec<(f32, i64)> { |
| self.data.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); |
| self.data |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use crate::distance::MetricType; |
| |
| #[test] |
| fn test_ivfflat_add_assigns_all_vectors() { |
| let d = 4; |
| let nlist = 2; |
| let n = 16; |
| let data: Vec<f32> = (0..n) |
| .flat_map(|i| [i as f32, 0.0, i as f32 + 0.5, 1.0]) |
| .collect(); |
| let ids: Vec<i64> = (0..n as i64).collect(); |
| |
| let mut index = IVFFlatIndex::new(d, nlist, MetricType::L2); |
| index.train(&data, n); |
| index.add(&data, &ids, n); |
| |
| assert_eq!(index.total_vectors(), n); |
| for list_id in 0..nlist { |
| assert_eq!(index.vectors[list_id].len(), index.ids[list_id].len() * d); |
| } |
| } |
| |
| #[test] |
| fn test_ivfflat_recalls_query_vector() { |
| let d = 4; |
| let nlist = 4; |
| let n = 64; |
| let data: Vec<f32> = (0..n) |
| .flat_map(|i| { |
| let cluster = (i % nlist) as f32 * 100.0; |
| [cluster + i as f32 * 0.01, 1.0, 2.0, 3.0] |
| }) |
| .collect(); |
| let ids: Vec<i64> = (1000..1000 + n as i64).collect(); |
| |
| let mut index = IVFFlatIndex::new(d, nlist, MetricType::L2); |
| index.train(&data, n); |
| index.add(&data, &ids, n); |
| |
| let query_id = 7; |
| let mut distances = vec![0.0; 5]; |
| let mut labels = vec![0; 5]; |
| index.search( |
| &data[query_id * d..(query_id + 1) * d], |
| 1, |
| 5, |
| nlist, |
| &mut distances, |
| &mut labels, |
| ); |
| |
| assert_eq!(labels[0], ids[query_id]); |
| assert_eq!(distances[0], 0.0); |
| for i in 1..5 { |
| assert!(distances[i] >= distances[i - 1]); |
| } |
| } |
| |
| #[test] |
| fn test_ivfflat_search_with_filter() { |
| use std::collections::HashSet; |
| |
| let d = 2; |
| let nlist = 1; |
| let data = vec![0.0, 0.0, 0.1, 0.0, 10.0, 10.0]; |
| let ids = vec![10, 11, 12]; |
| |
| let mut index = IVFFlatIndex::new(d, nlist, MetricType::L2); |
| index.train(&data, 3); |
| index.add(&data, &ids, 3); |
| |
| let filter: HashSet<i64> = [12].into_iter().collect(); |
| let mut distances = vec![0.0; 2]; |
| let mut labels = vec![0; 2]; |
| index.search_with_filter( |
| &[0.0, 0.0], |
| 1, |
| 2, |
| 1, |
| Some(&filter), |
| &mut distances, |
| &mut labels, |
| ); |
| |
| assert_eq!(labels, vec![12, -1]); |
| assert!(distances[0] > 0.0); |
| assert_eq!(distances[1], f32::MAX); |
| } |
| } |