| // 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::{MetricType, QueryDistance}; |
| use crate::hnsw::{HnswBuildParams, HnswGraph}; |
| use crate::hnsw_search::{search_hnsw_lists, HnswSearchList}; |
| use crate::ivfflat::IVFFlatIndex; |
| use crate::ivfpq::RowIdFilter; |
| use crate::kmeans; |
| use crate::topk::TopKHeap; |
| use rayon::prelude::*; |
| use std::io; |
| |
| pub struct IVFHNSWFlatIndex { |
| /// Exposed to match the existing core index structs. Mutating `flat` |
| /// directly can stale `graphs`; call `build_graphs` again before HNSW search. |
| pub flat: IVFFlatIndex, |
| pub graphs: Vec<Option<HnswGraph>>, |
| pub hnsw_params: HnswBuildParams, |
| } |
| |
| impl IVFHNSWFlatIndex { |
| pub fn new(d: usize, nlist: usize, metric: MetricType, hnsw_params: HnswBuildParams) -> Self { |
| IVFHNSWFlatIndex { |
| flat: IVFFlatIndex::new(d, nlist, metric), |
| graphs: vec![None; nlist], |
| hnsw_params, |
| } |
| } |
| |
| pub fn train(&mut self, data: &[f32], n: usize) { |
| self.flat.train(data, n); |
| } |
| |
| pub fn add(&mut self, data: &[f32], ids: &[i64], n: usize) { |
| self.flat.add(data, ids, n); |
| self.graphs.fill(None); |
| } |
| |
| pub fn build_graphs(&mut self) -> io::Result<()> { |
| self.graphs = (0..self.flat.nlist) |
| .into_par_iter() |
| .map(|list_id| { |
| let count = self.flat.ids[list_id].len(); |
| if count == 0 { |
| Ok(None) |
| } else { |
| HnswGraph::build( |
| &self.flat.vectors[list_id], |
| count, |
| self.flat.d, |
| self.flat.metric, |
| self.hnsw_params, |
| ) |
| .map(Some) |
| } |
| }) |
| .collect::<io::Result<Vec<_>>>()?; |
| Ok(()) |
| } |
| |
| #[allow(clippy::too_many_arguments)] |
| pub fn search( |
| &self, |
| queries: &[f32], |
| nq: usize, |
| k: usize, |
| nprobe: usize, |
| ef_search: usize, |
| result_distances: &mut [f32], |
| result_labels: &mut [i64], |
| ) { |
| self.search_with_filter( |
| queries, |
| nq, |
| k, |
| nprobe, |
| ef_search, |
| None, |
| result_distances, |
| result_labels, |
| ); |
| } |
| |
| #[allow(clippy::too_many_arguments)] |
| pub fn search_with_filter( |
| &self, |
| queries: &[f32], |
| nq: usize, |
| k: usize, |
| nprobe: usize, |
| ef_search: usize, |
| filter: Option<&dyn RowIdFilter>, |
| result_distances: &mut [f32], |
| result_labels: &mut [i64], |
| ) { |
| let processed_queries = self.flat.preprocess_vectors(queries, nq); |
| let (all_probe_indices, _) = kmeans::find_topk_batch( |
| &processed_queries, |
| nq, |
| &self.flat.quantizer_centroids, |
| self.flat.nlist, |
| self.flat.d, |
| nprobe, |
| ); |
| |
| for qi in 0..nq { |
| let query = &processed_queries[qi * self.flat.d..(qi + 1) * self.flat.d]; |
| let lists: Vec<_> = all_probe_indices[qi] |
| .iter() |
| .map(|&list_id| HnswSearchList { |
| ids: self.flat.ids[list_id].as_slice(), |
| graph: self.graphs[list_id].as_ref(), |
| payload: list_id, |
| }) |
| .collect(); |
| let sorted = search_hnsw_lists(query, &lists, k, ef_search, filter, |list, heap| { |
| self.scan_flat_list(query, list.payload, filter, heap); |
| }); |
| 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; |
| } |
| } |
| } |
| |
| fn scan_flat_list( |
| &self, |
| query: &[f32], |
| list_id: usize, |
| filter: Option<&dyn RowIdFilter>, |
| heap: &mut TopKHeap, |
| ) { |
| let distance = QueryDistance::new(query, self.flat.metric); |
| for (local_id, &row_id) in self.flat.ids[list_id].iter().enumerate() { |
| if let Some(f) = filter { |
| if !f.contains(row_id) { |
| continue; |
| } |
| } |
| let vector = |
| &self.flat.vectors[list_id][local_id * self.flat.d..(local_id + 1) * self.flat.d]; |
| heap.push(distance.distance_to(vector, None), row_id); |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use crate::distance::MetricType; |
| use crate::hnsw::HnswBuildParams; |
| |
| #[test] |
| fn test_ivfhnswflat_recalls_query_vector() { |
| let d = 4; |
| let nlist = 4; |
| let n = 128; |
| 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> = (0..n as i64).collect(); |
| |
| let mut index = IVFHNSWFlatIndex::new(d, nlist, MetricType::L2, HnswBuildParams::default()); |
| index.train(&data, n); |
| index.add(&data, &ids, n); |
| index.build_graphs().unwrap(); |
| |
| let query_id = 23; |
| 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, |
| 32, |
| &mut distances, |
| &mut labels, |
| ); |
| |
| assert_eq!(labels[0], ids[query_id]); |
| assert_eq!(distances[0], 0.0); |
| } |
| |
| #[test] |
| fn test_ivfhnswflat_without_built_graphs_falls_back_to_flat_scan() { |
| let d = 4; |
| let nlist = 4; |
| let n = 128; |
| 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> = (0..n as i64).collect(); |
| |
| let mut index = IVFHNSWFlatIndex::new(d, nlist, MetricType::L2, HnswBuildParams::default()); |
| index.train(&data, n); |
| index.add(&data, &ids, n); |
| |
| let query_id = 23; |
| 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, |
| 32, |
| &mut distances, |
| &mut labels, |
| ); |
| |
| assert_eq!(labels[0], ids[query_id]); |
| assert_eq!(distances[0], 0.0); |
| } |
| |
| #[test] |
| fn test_ivfhnswflat_selective_filter_uses_exact_results() { |
| use std::collections::HashSet; |
| |
| let d = 2; |
| let nlist = 1; |
| let n = 64; |
| let mut data = Vec::with_capacity(n * d); |
| for i in 0..n { |
| data.push(i as f32); |
| data.push(0.0); |
| } |
| let ids: Vec<i64> = (0..n as i64).collect(); |
| |
| let mut index = IVFHNSWFlatIndex::new(d, nlist, MetricType::L2, HnswBuildParams::default()); |
| index.train(&data, n); |
| index.add(&data, &ids, n); |
| index.build_graphs().unwrap(); |
| |
| let filter: HashSet<i64> = [63].into_iter().collect(); |
| let mut distances = vec![0.0; 1]; |
| let mut labels = vec![0; 1]; |
| index.search_with_filter( |
| &[0.0, 0.0], |
| 1, |
| 1, |
| 1, |
| 4, |
| Some(&filter), |
| &mut distances, |
| &mut labels, |
| ); |
| |
| assert_eq!(labels[0], 63); |
| assert_eq!(distances[0], 63.0 * 63.0); |
| } |
| |
| #[test] |
| fn test_ivfhnswflat_filter_backfills_when_graph_returns_too_few_matches() { |
| use std::collections::HashSet; |
| |
| let d = 2; |
| let nlist = 1; |
| let n = 128; |
| let mut data = Vec::with_capacity(n * d); |
| for i in 0..n { |
| data.push(i as f32); |
| data.push(0.0); |
| } |
| let ids: Vec<i64> = (0..n as i64).collect(); |
| |
| let mut index = IVFHNSWFlatIndex::new(d, nlist, MetricType::L2, HnswBuildParams::default()); |
| index.train(&data, n); |
| index.add(&data, &ids, n); |
| index.build_graphs().unwrap(); |
| |
| let filter: HashSet<i64> = (0..n as i64).filter(|id| id % 2 == 0).collect(); |
| let mut distances = vec![0.0; 10]; |
| let mut labels = vec![0; 10]; |
| index.search_with_filter( |
| &[127.0, 0.0], |
| 1, |
| 10, |
| 1, |
| 1, |
| Some(&filter), |
| &mut distances, |
| &mut labels, |
| ); |
| |
| assert_eq!( |
| labels, |
| vec![126, 124, 122, 120, 118, 116, 114, 112, 110, 108] |
| ); |
| assert!(labels.iter().all(|id| id % 2 == 0)); |
| } |
| |
| #[test] |
| fn test_topk_heap_keeps_closest_duplicate_id() { |
| let mut heap = TopKHeap::new(2); |
| |
| heap.push(10.0, 7); |
| heap.push(5.0, 8); |
| heap.push(1.0, 7); |
| |
| assert_eq!(heap.into_sorted(), vec![(1.0, 7), (5.0, 8)]); |
| } |
| } |