use super::resolve;
use crate::collection::types::Collection;
use crate::distance::DistanceMetric;
use crate::error::{Error, Result};
use crate::index::VectorIndex;
use crate::point::{Point, SearchResult};
use crate::quantization::{distance_pq_l2, PQVector, ProductQuantizer, StorageMode};
use crate::scored_result::ScoredResult;
use crate::storage::{PayloadStorage, VectorStorage};
use crate::validation::validate_dimension_match;
impl Collection {
fn search_ids_with_adc_if_pq(&self, query: &[f32], k: usize) -> Vec<ScoredResult> {
let config = self.config.read();
let is_pq = matches!(config.storage_mode, StorageMode::ProductQuantization);
let higher_is_better = config.metric.higher_is_better();
let metric = config.metric;
#[allow(clippy::cast_possible_truncation)]
let oversampling = config.pq_rescore_oversampling.unwrap_or(0) as usize;
drop(config);
if !is_pq || oversampling == 0 {
let results = self.index.search(query, k);
return self.merge_delta(results, query, k, metric);
}
let candidates_k = k.saturating_mul(oversampling).max(k + 32);
let index_results = self.index.search(query, candidates_k);
let rescored =
self.rescore_pq_candidates(query, k, metric, higher_is_better, index_results);
self.merge_delta(rescored, query, k, metric)
}
fn rescore_pq_candidates(
&self,
query: &[f32],
k: usize,
metric: DistanceMetric,
higher_is_better: bool,
index_results: Vec<ScoredResult>,
) -> Vec<ScoredResult> {
let pq_cache = self.pq_cache.read();
let quantizer = self.pq_quantizer.read();
let Some(quantizer) = quantizer.as_ref() else {
return index_results.into_iter().take(k).collect();
};
let mut rescored: Vec<ScoredResult> = index_results
.into_iter()
.map(|sr| {
let score = pq_cache.get(&sr.id).map_or(sr.score, |pq_vec| {
rescore_with_metric(query, pq_vec, quantizer, metric).unwrap_or_else(|err| {
tracing::warn!(sr.id, %err, "PQ rescore failed; using HNSW score");
sr.score
})
});
ScoredResult::new(sr.id, score)
})
.collect();
resolve::sort_scored_by_metric(&mut rescored, higher_is_better);
rescored.truncate(k);
rescored
}
#[cfg(feature = "persistence")]
#[inline]
pub(crate) fn merge_delta(
&self,
results: Vec<ScoredResult>,
query: &[f32],
k: usize,
metric: DistanceMetric,
) -> Vec<ScoredResult> {
crate::collection::streaming::merge_with_delta_scored(
results,
&self.delta_buffer,
query,
k,
metric,
)
}
#[cfg(not(feature = "persistence"))]
#[inline]
pub(crate) fn merge_delta(
&self,
results: Vec<ScoredResult>,
_query: &[f32],
_k: usize,
_metric: DistanceMetric,
) -> Vec<ScoredResult> {
results
}
}
fn rescore_with_metric(
query: &[f32],
pq_vec: &PQVector,
quantizer: &ProductQuantizer,
metric: DistanceMetric,
) -> Result<f32> {
if metric == DistanceMetric::Euclidean {
Ok(distance_pq_l2(query, pq_vec, quantizer))
} else {
let rotated_query = quantizer.apply_rotation(query);
let reconstructed = quantizer.reconstruct(pq_vec)?;
Ok(metric.calculate(&rotated_query, &reconstructed))
}
}
impl Collection {
pub fn search(&self, query: &[f32], k: usize) -> Result<Vec<SearchResult>> {
let config = self.config.read();
if config.metadata_only {
return Err(Error::SearchNotSupported(config.name.clone()));
}
validate_dimension_match(config.dimension, query.len())?;
drop(config);
let index_results = self.search_ids_with_adc_if_pq(query, k);
let vector_storage = self.vector_storage.read();
let payload_storage = self.payload_storage.read();
Ok(resolve::resolve_scored_results(
&index_results,
&*vector_storage,
&*payload_storage,
))
}
pub fn search_with_ef(
&self,
query: &[f32],
k: usize,
ef_search: usize,
) -> Result<Vec<SearchResult>> {
let config = self.config.read();
validate_dimension_match(config.dimension, query.len())?;
drop(config);
let quality = match ef_search {
0..=64 => crate::SearchQuality::Fast,
65..=128 => crate::SearchQuality::Balanced,
129..=512 => crate::SearchQuality::Accurate,
_ => crate::SearchQuality::Perfect,
};
let metric = self.config.read().metric;
let index_results = self.index.search_with_quality(query, k, quality);
let index_results = self.merge_delta(index_results, query, k, metric);
let vector_storage = self.vector_storage.read();
let payload_storage = self.payload_storage.read();
Ok(resolve::resolve_scored_results(
&index_results,
&*vector_storage,
&*payload_storage,
))
}
pub fn search_ids(&self, query: &[f32], k: usize) -> Result<Vec<ScoredResult>> {
let config = self.config.read();
validate_dimension_match(config.dimension, query.len())?;
drop(config);
let results = self.search_ids_with_adc_if_pq(query, k);
Ok(results)
}
pub fn search_with_filter(
&self,
query: &[f32],
k: usize,
filter: &crate::filter::Filter,
) -> Result<Vec<SearchResult>> {
let config = self.config.read();
validate_dimension_match(config.dimension, query.len())?;
let higher_is_better = config.metric.higher_is_better();
drop(config);
let selectivity = estimate_filter_selectivity(filter);
#[allow(clippy::cast_precision_loss)]
let k_f64 = k as f64;
#[allow(clippy::cast_precision_loss)]
let lower = (k + 10) as f64;
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let candidates_k = (k_f64 / selectivity).ceil().clamp(lower, 10_000.0) as usize;
let index_results = self.search_ids_with_adc_if_pq(query, candidates_k);
let vector_storage = self.vector_storage.read();
let payload_storage = self.payload_storage.read();
let mut results: Vec<SearchResult> = index_results
.into_iter()
.filter_map(|sr| {
let payload = payload_storage.retrieve(sr.id).ok().flatten();
let matches = match payload.as_ref() {
Some(p) => filter.matches(p),
None => filter.matches(&serde_json::Value::Null),
};
if !matches {
return None;
}
let vector = vector_storage.retrieve(sr.id).ok().flatten()?;
Some(SearchResult::new(
Point {
id: sr.id,
vector,
payload,
sparse_vectors: None,
},
sr.score,
))
})
.collect();
resolve::sort_results_by_metric(&mut results, higher_is_better);
results.truncate(k);
Ok(results)
}
}
fn estimate_filter_selectivity(filter: &crate::filter::Filter) -> f64 {
estimate_condition_selectivity(&filter.condition)
}
fn estimate_condition_selectivity(cond: &crate::filter::Condition) -> f64 {
use crate::filter::Condition;
match cond {
Condition::Eq { .. } | Condition::IsNull { .. } => 0.1,
Condition::Gt { .. }
| Condition::Gte { .. }
| Condition::Lt { .. }
| Condition::Lte { .. }
| Condition::Contains { .. }
| Condition::Like { .. }
| Condition::ILike { .. } => 0.3,
Condition::In { values, .. } => {
#[allow(clippy::cast_precision_loss)]
let sel = values.len() as f64 * 0.05;
sel.min(0.8)
}
Condition::Neq { .. } | Condition::IsNotNull { .. } => 0.9,
Condition::And { conditions } => conditions
.iter()
.map(estimate_condition_selectivity)
.product::<f64>()
.max(0.01),
Condition::Or { conditions } => conditions
.iter()
.map(estimate_condition_selectivity)
.sum::<f64>()
.min(1.0),
Condition::Not { condition } => (1.0 - estimate_condition_selectivity(condition)).max(0.01),
}
}