use super::{Collection, HashSet, QuerySearchOptions, Result, SearchResult, MAX_LIMIT};
impl Collection {
pub(crate) fn evaluate_graph_match_anchor_ids(
&self,
predicate: &crate::velesql::GraphMatchPredicate,
params: &std::collections::HashMap<String, serde_json::Value>,
from_aliases: &[String],
) -> Result<HashSet<u64>> {
let anchor_alias = Self::resolve_anchor_alias(predicate, from_aliases)?;
let clause = Self::build_anchor_match_clause(predicate);
let matches = self.execute_match(&clause, params)?;
let mut ids = HashSet::with_capacity(matches.len());
for m in matches {
if let Some(id) = m.bindings.get(&anchor_alias) {
ids.insert(*id);
}
}
Ok(ids)
}
fn resolve_anchor_alias(
predicate: &crate::velesql::GraphMatchPredicate,
from_aliases: &[String],
) -> Result<String> {
let first_node = predicate.pattern.nodes.first().ok_or_else(|| {
crate::error::Error::Config("MATCH predicate requires at least one node".to_string())
})?;
let anchor_alias = first_node.alias.clone().ok_or_else(|| {
crate::error::Error::Config(
"MATCH predicate in SELECT WHERE requires an alias on the first node, \
e.g. MATCH (d:Doc)-[:REL]->(x)"
.to_string(),
)
})?;
if !from_aliases.is_empty() && !from_aliases.iter().any(|a| a == &anchor_alias) {
return Err(crate::error::Error::Config(format!(
"MATCH predicate anchor alias '{}' must match one of the FROM/JOIN aliases: {:?}",
anchor_alias, from_aliases
)));
}
Ok(anchor_alias)
}
fn build_anchor_match_clause(
predicate: &crate::velesql::GraphMatchPredicate,
) -> crate::velesql::MatchClause {
crate::velesql::MatchClause {
patterns: vec![predicate.pattern.clone()],
where_clause: None,
return_clause: crate::velesql::ReturnClause {
items: vec![crate::velesql::ReturnItem {
expression: "*".to_string(),
alias: None,
}],
order_by: None,
limit: Some(u64::MAX),
},
}
}
fn apply_optional_metadata_filter(
filtered: Vec<SearchResult>,
filter_cond: Option<&crate::velesql::Condition>,
skip_metadata_prefilter_for_graph_or: bool,
execution_limit: usize,
) -> Vec<SearchResult> {
let Some(cond) = filter_cond else {
return filtered;
};
if skip_metadata_prefilter_for_graph_or {
return filtered;
}
let Some(metadata_cond) = Self::extract_metadata_filter(cond) else {
return filtered;
};
let filter = crate::filter::Filter::new(crate::filter::Condition::from(metadata_cond));
filtered
.into_iter()
.filter(|r| match r.point.payload.as_ref() {
Some(p) => filter.matches(p),
None => filter.matches(&serde_json::Value::Null),
})
.take(execution_limit)
.collect()
}
fn apply_similarity_cascade(
&self,
candidates: Vec<SearchResult>,
first_similarity: &(String, Vec<f32>, crate::velesql::CompareOp, f64),
similarity_conditions: &[(String, Vec<f32>, crate::velesql::CompareOp, f64)],
filter_k: usize,
) -> Vec<SearchResult> {
let (field, vec, op, threshold) = first_similarity;
let mut filtered =
self.filter_by_similarity(candidates, field, vec, *op, *threshold, filter_k);
for (sim_field, sim_vec, sim_op, sim_threshold) in similarity_conditions.iter().skip(1) {
filtered = self.filter_by_similarity(
filtered,
sim_field,
sim_vec,
*sim_op,
*sim_threshold,
filter_k,
);
}
filtered
}
#[allow(clippy::too_many_arguments)] fn dispatch_near_with_filter(
&self,
vector: &[f32],
cond: &crate::velesql::Condition,
execution_limit: usize,
skip_metadata_prefilter_for_graph_or: bool,
search_opts: &QuerySearchOptions,
cbo_strategy: crate::velesql::ExecutionStrategy,
cbo_over_fetch: usize,
) -> Result<Vec<SearchResult>> {
if let Some(text_query) = Self::extract_match_query(cond) {
let fusion = search_opts.fusion_clause.as_ref();
let vector_weight = fusion.and_then(|fc| fc.vector_weight).map(|w| {
#[allow(clippy::cast_possible_truncation)]
let w_f32 = w as f32;
w_f32
});
let rrf_k = fusion.and_then(|fc| fc.k);
if let Some(metadata_cond) = Self::extract_metadata_filter(cond) {
let filter =
crate::filter::Filter::new(crate::filter::Condition::from(metadata_cond));
return self.hybrid_search_with_filter(
vector,
&text_query,
execution_limit,
vector_weight,
&filter,
rrf_k,
);
}
return self.hybrid_search(vector, &text_query, execution_limit, vector_weight, rrf_k);
}
let cbo_search_k = execution_limit
.saturating_mul(cbo_over_fetch)
.min(MAX_LIMIT);
if skip_metadata_prefilter_for_graph_or {
return self.search_with_opts(vector, execution_limit, search_opts);
}
if let Some(metadata_cond) = Self::extract_metadata_filter(cond) {
let filter = crate::filter::Filter::new(crate::filter::Condition::from(metadata_cond));
return match cbo_strategy {
crate::velesql::ExecutionStrategy::GraphFirst => {
Ok(self.scan_and_score_by_vector(&filter, vector, execution_limit))
}
_ => self.search_with_filter_and_opts(vector, cbo_search_k, &filter, search_opts),
};
}
self.search_with_opts(vector, execution_limit, search_opts)
}
fn dispatch_metadata_only(
&self,
cond: &crate::velesql::Condition,
execution_limit: usize,
skip_metadata_prefilter_for_graph_or: bool,
) -> Result<Vec<SearchResult>> {
if let crate::velesql::Condition::Match(ref m) = cond {
return self.text_search(&m.query, execution_limit);
}
let empty_filter =
|| crate::filter::Filter::new(crate::filter::Condition::And { conditions: vec![] });
if skip_metadata_prefilter_for_graph_or {
return Ok(self.execute_scan_query(&empty_filter(), execution_limit));
}
let Some(metadata_cond) = Self::extract_metadata_filter(cond) else {
return Ok(self.execute_scan_query(&empty_filter(), execution_limit));
};
Ok(self.dispatch_metadata_filter(cond, metadata_cond, execution_limit))
}
fn dispatch_metadata_filter(
&self,
cond: &crate::velesql::Condition,
metadata_cond: crate::velesql::Condition,
execution_limit: usize,
) -> Vec<SearchResult> {
let filter =
crate::filter::Filter::new(crate::filter::Condition::from(metadata_cond.clone()));
if let Some(bitmap_results) = self.try_bitmap_prefilter(&filter, execution_limit) {
return bitmap_results;
}
tracing::debug!("dispatch_metadata_only: trying indexed path");
if let Some(indexed) = self.execute_indexed_metadata_query(&metadata_cond, execution_limit)
{
tracing::debug!("dispatch_metadata_only: indexed path succeeded");
return indexed;
}
tracing::debug!("dispatch_metadata_only: indexed path returned None, trying BM25");
if let Some(like_results) = self.try_like_via_text_index(cond, execution_limit) {
return like_results;
}
let filter = crate::filter::Filter::new(crate::filter::Condition::from(metadata_cond));
self.execute_scan_query(&filter, execution_limit)
}
fn try_bitmap_prefilter(
&self,
filter: &crate::filter::Filter,
execution_limit: usize,
) -> Option<Vec<SearchResult>> {
let bitmap = self.build_prefilter_bitmap(filter)?;
if bitmap.is_empty() {
return Some(Vec::new());
}
let candidate_ids: Vec<u64> = bitmap.iter().map(u64::from).collect();
let candidate_budget = execution_limit.saturating_mul(50).max(1000);
if candidate_ids.len() <= candidate_budget {
return Some(self.scan_ids_with_filter(&candidate_ids, filter, execution_limit));
}
None
}
fn try_like_via_text_index(
&self,
cond: &crate::velesql::Condition,
limit: usize,
) -> Option<Vec<SearchResult>> {
let candidate_ids = self.bm25_candidates_for_like(cond, limit)?;
let filter = crate::filter::Filter::new(crate::filter::Condition::from(cond.clone()));
let results = self.collect_matching_points(&candidate_ids, &filter, limit);
if results.len() >= limit {
Some(results)
} else {
None }
}
fn bm25_candidates_for_like(
&self,
cond: &crate::velesql::Condition,
limit: usize,
) -> Option<Vec<u64>> {
let pattern = Self::extract_like_pattern(cond)?;
let word = pattern.trim_matches('%');
if word.len() < 3 {
return None;
}
let text_results = self.text_index.search(word, limit.saturating_mul(10));
if text_results.is_empty() {
return None;
}
Some(text_results.iter().map(|(id, _)| *id).collect())
}
fn collect_matching_points(
&self,
candidate_ids: &[u64],
filter: &crate::filter::Filter,
limit: usize,
) -> Vec<SearchResult> {
let mut results = Vec::new();
for point in self.get(candidate_ids).into_iter().flatten() {
let payload = point.payload.clone().unwrap_or(serde_json::Value::Null);
if filter.matches(&payload) {
results.push(SearchResult::new(point, 1.0));
if results.len() >= limit {
break;
}
}
}
results
}
fn extract_like_pattern(cond: &crate::velesql::Condition) -> Option<String> {
match cond {
crate::velesql::Condition::Like(like) => Some(like.pattern.clone()),
crate::velesql::Condition::And(left, right) => {
Self::extract_like_pattern(left).or_else(|| Self::extract_like_pattern(right))
}
crate::velesql::Condition::Group(inner) => Self::extract_like_pattern(inner),
_ => None,
}
}
#[allow(clippy::too_many_arguments)] pub(super) fn dispatch_vector_query(
&self,
vector_search: Option<&Vec<f32>>,
first_similarity: Option<&(String, Vec<f32>, crate::velesql::CompareOp, f64)>,
similarity_conditions: &[(String, Vec<f32>, crate::velesql::CompareOp, f64)],
filter_condition: Option<&crate::velesql::Condition>,
execution_limit: usize,
skip_metadata_prefilter_for_graph_or: bool,
search_opts: &QuerySearchOptions,
cbo_strategy: crate::velesql::ExecutionStrategy,
cbo_over_fetch: usize,
) -> Result<Vec<SearchResult>> {
match (vector_search, first_similarity, filter_condition) {
(search_vec, Some(sim), filter_cond) => self.dispatch_similarity_query(
search_vec.map(Vec::as_slice),
sim,
similarity_conditions,
filter_cond,
execution_limit,
skip_metadata_prefilter_for_graph_or,
search_opts,
),
(Some(vector), None, Some(cond)) => self.dispatch_near_with_filter(
vector,
cond,
execution_limit,
skip_metadata_prefilter_for_graph_or,
search_opts,
cbo_strategy,
cbo_over_fetch,
),
(Some(vector), None, None) => {
self.dispatch_pure_near(vector, execution_limit, search_opts)
}
(None, None, Some(cond)) => self.dispatch_metadata_only(
cond,
execution_limit,
skip_metadata_prefilter_for_graph_or,
),
(None, None, None) => Ok(self.execute_scan_query(
&crate::filter::Filter::new(crate::filter::Condition::And { conditions: vec![] }),
execution_limit,
)),
}
}
#[allow(clippy::too_many_arguments)] fn dispatch_similarity_query(
&self,
search_vector: Option<&[f32]>,
sim: &(String, Vec<f32>, crate::velesql::CompareOp, f64),
similarity_conditions: &[(String, Vec<f32>, crate::velesql::CompareOp, f64)],
filter_cond: Option<&crate::velesql::Condition>,
execution_limit: usize,
skip_metadata_prefilter_for_graph_or: bool,
search_opts: &QuerySearchOptions,
) -> Result<Vec<SearchResult>> {
let k = execution_limit
.saturating_mul(10 * similarity_conditions.len().max(1))
.min(MAX_LIMIT);
let search_vec = search_vector.unwrap_or(&sim.1);
let candidates = self.search_with_opts(search_vec, k, search_opts)?;
let filtered = self.apply_similarity_cascade(
candidates,
sim,
similarity_conditions,
execution_limit.saturating_mul(2),
);
Ok(Self::apply_optional_metadata_filter(
filtered,
filter_cond,
skip_metadata_prefilter_for_graph_or,
execution_limit,
))
}
fn dispatch_pure_near(
&self,
vector: &[f32],
execution_limit: usize,
search_opts: &QuerySearchOptions,
) -> Result<Vec<SearchResult>> {
self.search_with_opts(vector, execution_limit, search_opts)
}
}