Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions diskann-benchmark/example/disk-index-filter.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{
"search_directories": [
"test_data/disk_index_search"
],
"jobs": [
{
"type": "disk-index",
"content": {
"source": {
"disk-index-source": "Build",
"data_type": "float32",
"data": "disk_index_siftsmall_learn_256pts_data.fbin",
"distance": "squared_l2",
"dim": 128,
"max_degree": 32,
"l_build": 50,
"num_threads": 1,
"build_ram_limit_gb": 2.0,
"num_pq_chunks": 128,
"quantization_type": "FP",
"save_path": "siftsmall_index_filter_graph"
},
"search_phase": {
"queries": "disk_index_sample_query_10pts.fbin",
"groundtruth": "disk_index_10pts_idx_uint32_truth_search_filter_res.bin",
"search_list": [10, 20, 40],
"beam_width": 4,
"recall_at": 10,
"num_threads": 1,
"is_flat_search": false,
"distance": "squared_l2",
"vector_filters_file": "disk_index_10pts_idx_uint32_range_res_r_100000.bin"
}
}
},
{
"type": "disk-index",
"content": {
"source": {
"disk-index-source": "Build",
"data_type": "float32",
"data": "disk_index_siftsmall_learn_256pts_data.fbin",
"distance": "squared_l2",
"dim": 128,
"max_degree": 32,
"l_build": 50,
"num_threads": 1,
"build_ram_limit_gb": 2.0,
"num_pq_chunks": 128,
"quantization_type": "FP",
"save_path": "siftsmall_index_filter_flat"
},
"search_phase": {
"queries": "disk_index_sample_query_10pts.fbin",
"groundtruth": "disk_index_10pts_idx_uint32_truth_search_filter_res.bin",
"search_list": [10, 20, 40],
"beam_width": 4,
"recall_at": 10,
"num_threads": 1,
"is_flat_search": true,
"distance": "squared_l2",
"vector_filters_file": "disk_index_10pts_idx_uint32_range_res_r_100000.bin"
}
}
}
]
}
27 changes: 18 additions & 9 deletions diskann-benchmark/src/backend/disk_index/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@ use diskann::utils::VectorRepr;
use diskann_benchmark_runner::{files::InputFile, utils::MicroSeconds};
use diskann_disk::{
data_model::{AdHoc, CachingStrategy},
search::provider::{
disk_provider::DiskIndexSearcher, disk_vertex_provider_factory::DiskVertexProviderFactory,
search::{
plan::SearchPlan,
provider::{
disk_provider::DiskIndexSearcher,
disk_vertex_provider_factory::DiskVertexProviderFactory,
},
},
storage::disk_index_reader::DiskIndexReader,
utils::{instrumentation::PerfLogger, statistics, AlignedFileReaderFactory, QueryStatistics},
Expand Down Expand Up @@ -264,20 +268,25 @@ where
zipped.for_each_in_pool(
pool.as_ref(),
|(((((q, vf), id_chunk), dist_chunk), stats), rc)| {
let vector_filter = if search_params.vector_filters_file.is_none() {
None
} else {
Some(Box::new(move |vid: &u32| vf.contains(vid))
as Box<dyn Fn(&u32) -> bool + Send + Sync>)
// Construct the plan from the JSON-driven
// `(is_flat_search, has_filter)` pair. JSON config doesn't
// expose AdaptiveL yet, so `InlineFilter` is unreachable here.
let has_filter = search_params.vector_filters_file.is_some();
let plan: SearchPlan<'_> = match (search_params.is_flat_search, has_filter) {
(true, false) => SearchPlan::flat(),
(true, true) => SearchPlan::flat_filtered(move |vid: &u32| vf.contains(vid)),
(false, false) => SearchPlan::graph(),
(false, true) => {
SearchPlan::graph_filtered(move |vid: &u32| vf.contains(vid))
}
};

match searcher.search(
q,
search_params.recall_at,
l,
Some(search_params.beam_width),
vector_filter,
search_params.is_flat_search,
plan,
) {
Ok(search_result) => {
*stats = search_result.stats.query_statistics;
Expand Down
31 changes: 31 additions & 0 deletions diskann-benchmark/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,37 @@ mod tests {
let raw = value_from_file(&example_directory().join("graph-index-inline-filter.json"));
run_integration_test(raw);
}

/// Filtered disk search end-to-end: drives the disk-index backend through
/// `disk-index-filter.json`
#[test]
#[cfg(feature = "disk-index")]
fn disk_index_filter_integration() {
let mut raw = value_from_file(&example_directory().join("disk-index-filter.json"));
prefix_search_directories(&mut raw, &root_directory());

let tempdir = tempfile::tempdir().unwrap();
let input_path = tempdir.path().join("disk-index-filter.json");
save_to_file(&input_path, &raw);
let output_path = tempdir.path().join("output.json");

let command = Commands::Run {
input_file: input_path.to_owned(),
output_file: output_path.to_owned(),
dry_run: false,
allow_debug: true,
};
let cli = Cli::from_commands(command, true);
let mut output = Memory::new();
let result = cli.run(&mut output);
let output_str = String::from_utf8(output.into_inner()).unwrap();
println!("output = {}", output_str);
result.expect("disk-index-filter run failed");

assert!(output_path.exists());
let results: Vec<Value> = load_from_file(&output_path);
assert_eq!(results.len(), num_jobs(&raw));
}

#[test]
fn graph_index_filter_integration_with_gt_compute() {
Expand Down
3 changes: 1 addition & 2 deletions diskann-disk/src/build/builder/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1089,8 +1089,7 @@ pub(crate) mod disk_index_builder_tests {
&mut indices,
&mut distances,
&mut associated_data,
&|_| true,
false,
&crate::search::plan::SearchPlan::graph(),
);

diskann_providers::test_utils::assert_top_k_exactly_match(
Expand Down
1 change: 1 addition & 0 deletions diskann-disk/src/search/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

//! Model module containing data structures, providers, and traits for disk index operations

pub mod plan;
pub mod pq;
pub mod provider;
pub mod traits;
155 changes: 155 additions & 0 deletions diskann-disk/src/search/plan.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
*/

//! Top-level disk search plan.
//!
//! Encodes the algorithm + filter combination for `DiskIndexSearcher::search`
//! as a sum type so that invalid combinations (flat scan with adaptive L,
//! inline filter without a predicate) are unrepresentable at the API boundary.

use diskann::graph::search::AdaptiveL;

/// Owned closure used to filter vector IDs during disk search.
///
/// The `&u32` argument is the disk-side internal/external ID (they coincide
/// on the disk path by construction).
pub type SearchPredicate<'a> = Box<dyn Fn(&u32) -> bool + Send + Sync + 'a>;

/// Top-level disk search plan.
///
/// Three variants encode the algorithm + filter combination:
///
/// * `FlatScan` — brute-force linear scan, with or without an inline filter.
/// * `Graph` — plain greedy beam search; the optional filter is applied as a
/// hard post-filter during reranking (no traversal-time effect).
/// * `InlineFilter` — label-filtered graph search; the predicate is consulted
/// at visit time (not just during rerank). `adaptive_l = Some(_)` grows the
/// beam mid-search if the observed match specificity is low.

pub enum SearchPlan<'a> {
Comment thread
dyhyfu marked this conversation as resolved.
Outdated
FlatScan { filter: Option<SearchPredicate<'a>> },

Graph { filter: Option<SearchPredicate<'a>> },

InlineFilter {
predicate: SearchPredicate<'a>,
adaptive_l: Option<AdaptiveL>,
},
}

impl<'a> SearchPlan<'a> {
/// Flat scan over all vectors. Recall baseline.
pub fn flat() -> Self {
Self::FlatScan { filter: None }
}

/// Flat scan restricted to vectors that satisfy `predicate`.
pub fn flat_filtered<F>(predicate: F) -> Self
where
F: Fn(&u32) -> bool + Send + Sync + 'a,
{
Self::FlatScan {
filter: Some(Box::new(predicate)),
}
}

/// Plain greedy graph search; no filter.
pub fn graph() -> Self {
Self::Graph { filter: None }
}

/// Plain greedy graph search with a hard post-filter applied during
/// reranking. Traversal is unaffected.
pub fn graph_filtered<F>(predicate: F) -> Self
where
F: Fn(&u32) -> bool + Send + Sync + 'a,
{
Self::Graph {
filter: Some(Box::new(predicate)),
}
}

/// Inline label-filtered graph search. `adaptive_l = Some(_)` enables
/// mid-search beam widening; `None` runs inline tracking only (no
/// resizing).
pub fn inline_filter<F>(predicate: F, adaptive_l: Option<AdaptiveL>) -> Self
where
F: Fn(&u32) -> bool + Send + Sync + 'a,
{
Self::InlineFilter {
predicate: Box::new(predicate),
adaptive_l,
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn flat_no_filter_constructor() {
let plan = SearchPlan::flat();
assert!(matches!(plan, SearchPlan::FlatScan { filter: None }));
}

#[test]
fn flat_filtered_constructor() {
let plan = SearchPlan::flat_filtered(|id| *id == 5);
match &plan {
SearchPlan::FlatScan { filter: Some(p) } => {
assert!(p(&5));
assert!(!p(&4));
}
_ => panic!("expected FlatScan with filter"),
}
}

#[test]
fn graph_no_filter_constructor() {
let plan = SearchPlan::graph();
assert!(matches!(plan, SearchPlan::Graph { filter: None }));
}

#[test]
fn graph_filtered_constructor() {
let plan = SearchPlan::graph_filtered(|id| *id == 7);
match &plan {
SearchPlan::Graph { filter: Some(p) } => {
assert!(p(&7));
assert!(!p(&6));
}
_ => panic!("expected Graph with filter"),
}
}

#[test]
fn inline_filter_constructor_without_adaptive_l() {
let plan = SearchPlan::inline_filter(|id| *id == 3, None);
match &plan {
SearchPlan::InlineFilter {
predicate,
adaptive_l: None,
} => {
assert!(predicate(&3));
assert!(!predicate(&2));
}
_ => panic!("expected InlineFilter with adaptive_l = None"),
}
}

#[test]
fn inline_filter_constructor_with_adaptive_l() {
let adaptive = AdaptiveL::new(5, 16.0).expect("valid AdaptiveL");
let plan = SearchPlan::inline_filter(|id| *id == 11, Some(adaptive));
match &plan {
SearchPlan::InlineFilter {
adaptive_l: Some(_),
..
} => {}
_ => panic!("expected InlineFilter with adaptive_l = Some"),
}
}
}
Loading