use crate::collection::graph::property_index::PredicateType;
use crate::collection::types::Collection;
use crate::error::Result;
use crate::point::SearchResult;
use crate::velesql::{CompareOp, Condition};
use super::MAX_LIMIT;
static MATCH_METRICS: std::sync::LazyLock<super::match_metrics::MatchMetrics> =
std::sync::LazyLock::new(super::match_metrics::MatchMetrics::new);
impl Collection {
#[allow(clippy::cast_precision_loss)]
pub(crate) fn compute_match_collection_stats(
&self,
) -> crate::velesql::match_planner::CollectionStats {
let total_nodes = self.len();
let total_edges = self.graph.edge_store.len();
let avg_degree = if total_nodes > 0 {
total_edges as f64 / total_nodes as f64
} else {
0.0
};
let label_count = self.graph.edge_store.label_count();
let label_selectivity = if label_count > 0 {
1.0 / label_count as f64
} else {
1.0
};
crate::velesql::match_planner::CollectionStats {
total_nodes,
total_edges,
avg_degree,
label_count,
label_selectivity,
}
}
pub(super) fn dispatch_match_query(
&self,
match_clause: &crate::velesql::MatchClause,
params: &std::collections::HashMap<String, serde_json::Value>,
ctx: &crate::guardrails::QueryContext,
) -> Result<Vec<SearchResult>> {
let raw = self.dispatch_match_strategy(match_clause, params, ctx)?;
self.finalize_match_results(match_clause, raw, ctx, params)
}
pub fn match_query_ordered(
&self,
match_clause: &crate::velesql::MatchClause,
params: &std::collections::HashMap<String, serde_json::Value>,
) -> Result<Vec<super::match_exec::MatchResult>> {
self.runtime
.guard_rails
.pre_check("default")
.map_err(crate::error::Error::from)?;
let ctx = self.runtime.guard_rails.create_context();
self.dispatch_match_ordered(match_clause, params, &ctx)
}
pub(in crate::collection::search::query) fn dispatch_match_ordered(
&self,
match_clause: &crate::velesql::MatchClause,
params: &std::collections::HashMap<String, serde_json::Value>,
ctx: &crate::guardrails::QueryContext,
) -> Result<Vec<super::match_exec::MatchResult>> {
let raw = self.dispatch_match_strategy(match_clause, params, ctx)?;
self.finalize_match_ordering(match_clause, raw, ctx, params)
}
fn dispatch_match_strategy(
&self,
match_clause: &crate::velesql::MatchClause,
params: &std::collections::HashMap<String, serde_json::Value>,
ctx: &crate::guardrails::QueryContext,
) -> Result<Vec<super::match_exec::MatchResult>> {
let start = std::time::Instant::now();
let stats = self.compute_match_collection_stats();
let strategy = crate::velesql::match_planner::MatchQueryPlanner::plan(match_clause, &stats);
tracing::debug!(strategy = ?strategy, "MATCH execution strategy selected");
let result = self.run_match_strategy(match_clause, params, ctx, &strategy);
let max_depth = crate::velesql::match_planner::MatchQueryPlanner::count_hops(match_clause);
match &result {
Ok(results) => {
MATCH_METRICS.record_success(start.elapsed(), results.len(), max_depth);
}
Err(_) => {
MATCH_METRICS.record_failure(start.elapsed());
}
}
if result.is_ok() {
#[allow(clippy::cast_possible_truncation)]
let elapsed_ms = start.elapsed().as_millis() as u64;
let (labels, properties, predicates) = extract_match_query_pattern(match_clause);
self.record_query_pattern(labels, properties, predicates, elapsed_ms);
}
result
}
fn run_match_strategy(
&self,
match_clause: &crate::velesql::MatchClause,
params: &std::collections::HashMap<String, serde_json::Value>,
ctx: &crate::guardrails::QueryContext,
strategy: &crate::velesql::match_planner::MatchExecutionStrategy,
) -> Result<Vec<super::match_exec::MatchResult>> {
match strategy {
crate::velesql::match_planner::MatchExecutionStrategy::VectorFirst {
similarity_alias,
top_k,
threshold,
} => self.execute_match_vector_first(
match_clause,
params,
ctx,
similarity_alias,
*top_k,
*threshold,
),
crate::velesql::match_planner::MatchExecutionStrategy::Parallel {
ref vector_hint,
..
} => self.execute_match_parallel(match_clause, params, ctx, vector_hint),
crate::velesql::match_planner::MatchExecutionStrategy::GraphFirst { .. } => {
self.execute_match_with_context(match_clause, params, Some(ctx))
}
}
}
fn execute_match_parallel(
&self,
match_clause: &crate::velesql::MatchClause,
params: &std::collections::HashMap<String, serde_json::Value>,
ctx: &crate::guardrails::QueryContext,
vector_hint: &crate::velesql::match_planner::MatchExecutionStrategy,
) -> Result<Vec<super::match_exec::MatchResult>> {
let vector_first =
if let crate::velesql::match_planner::MatchExecutionStrategy::VectorFirst {
similarity_alias,
top_k,
threshold,
} = vector_hint
{
Some((similarity_alias.as_str(), *top_k, *threshold))
} else {
tracing::warn!(
"Parallel strategy vector_hint is not VectorFirst; \
skipping vector path"
);
None
};
let graph_leg = || self.execute_match_with_context(match_clause, params, Some(ctx));
let vector_leg = || match vector_first {
Some((alias, top_k, threshold)) => {
self.execute_match_vector_first(match_clause, params, ctx, alias, top_k, threshold)
}
None => Ok(Vec::new()),
};
#[cfg(feature = "persistence")]
let (graph_results, vector_results) = rayon::join(graph_leg, vector_leg);
#[cfg(not(feature = "persistence"))]
let (graph_results, vector_results) = (graph_leg(), vector_leg());
let graph_results = graph_results?;
let vector_results = vector_results?;
let config = self.storage.config.read();
let higher_is_better = config.metric.higher_is_better();
drop(config);
Ok(merge_match_results(
graph_results,
vector_results,
higher_is_better,
))
}
fn finalize_match_results(
&self,
match_clause: &crate::velesql::MatchClause,
match_results: Vec<super::match_exec::MatchResult>,
ctx: &crate::guardrails::QueryContext,
params: &std::collections::HashMap<String, serde_json::Value>,
) -> Result<Vec<SearchResult>> {
ctx.check_timeout()
.map_err(crate::error::Error::from)
.inspect_err(|_| self.runtime.guard_rails.circuit_breaker.record_failure())?;
let mut sorted = match_results;
self.apply_match_order_by(&mut sorted, match_clause, params)
.inspect_err(|_| self.runtime.guard_rails.circuit_breaker.record_failure())?;
let mut results = self
.match_results_to_search_results(sorted)
.inspect_err(|_| self.runtime.guard_rails.circuit_breaker.record_failure())?;
ctx.check_cardinality(results.len())
.map_err(crate::error::Error::from)
.inspect_err(|_| self.runtime.guard_rails.circuit_breaker.record_failure())?;
if let Some(limit) = match_return_limit(match_clause) {
results.truncate(limit);
}
#[allow(clippy::cast_possible_truncation)]
let graph_latency_us = ctx.elapsed().as_micros() as u64;
self.query
.query_planner
.stats()
.update_graph_latency(graph_latency_us);
self.runtime.guard_rails.circuit_breaker.record_success();
Ok(results)
}
fn finalize_match_ordering(
&self,
match_clause: &crate::velesql::MatchClause,
match_results: Vec<super::match_exec::MatchResult>,
ctx: &crate::guardrails::QueryContext,
params: &std::collections::HashMap<String, serde_json::Value>,
) -> Result<Vec<super::match_exec::MatchResult>> {
ctx.check_timeout()
.map_err(crate::error::Error::from)
.inspect_err(|_| self.runtime.guard_rails.circuit_breaker.record_failure())?;
let mut sorted = match_results;
self.apply_match_order_by(&mut sorted, match_clause, params)
.inspect_err(|_| self.runtime.guard_rails.circuit_breaker.record_failure())?;
ctx.check_cardinality(sorted.len())
.map_err(crate::error::Error::from)
.inspect_err(|_| self.runtime.guard_rails.circuit_breaker.record_failure())?;
if let Some(limit) = match_return_limit(match_clause) {
sorted.truncate(limit);
}
self.runtime.guard_rails.circuit_breaker.record_success();
Ok(sorted)
}
pub(in crate::collection::search::query) fn apply_match_order_by(
&self,
results: &mut [super::match_exec::MatchResult],
match_clause: &crate::velesql::MatchClause,
params: &std::collections::HashMap<String, serde_json::Value>,
) -> Result<()> {
if let Some(order_by) = match_clause.return_clause.order_by.as_ref() {
sort_match_baseline(results);
for item in order_by.iter().rev() {
self.order_match_results(results, &item.expr, item.descending, params)?;
}
}
Ok(())
}
}
pub(in crate::collection::search::query) fn match_return_limit(
match_clause: &crate::velesql::MatchClause,
) -> Option<usize> {
match_clause
.return_clause
.limit
.map(|l| usize::try_from(l).unwrap_or(MAX_LIMIT).min(MAX_LIMIT))
}
fn sort_match_baseline(results: &mut [super::match_exec::MatchResult]) {
results.sort_unstable_by(|a, b| {
a.node_id
.cmp(&b.node_id)
.then_with(|| a.depth.cmp(&b.depth))
.then_with(|| a.path.cmp(&b.path))
});
}
fn extract_match_query_pattern(
match_clause: &crate::velesql::MatchClause,
) -> (Vec<String>, Vec<String>, Vec<PredicateType>) {
let mut labels: Vec<String> = match_clause
.patterns
.iter()
.flat_map(|p| p.nodes.iter())
.flat_map(|n| n.labels.iter())
.cloned()
.collect();
labels.sort_unstable();
labels.dedup();
let mut properties: Vec<String> = Vec::new();
let mut predicates: Vec<PredicateType> = Vec::new();
if let Some(ref cond) = match_clause.where_clause {
collect_condition_predicates(cond, &mut properties, &mut predicates);
}
properties.sort_unstable();
properties.dedup();
(labels, properties, predicates)
}
#[allow(unreachable_patterns)]
fn collect_condition_predicates(
cond: &Condition,
properties: &mut Vec<String>,
predicates: &mut Vec<PredicateType>,
) {
match cond {
Condition::Comparison(c) => {
properties.push(c.column.clone());
let pred = match c.operator {
CompareOp::Eq | CompareOp::NotEq => PredicateType::Equality,
CompareOp::Gt | CompareOp::Gte | CompareOp::Lt | CompareOp::Lte => {
PredicateType::Range
}
};
predicates.push(pred);
}
Condition::In(i) => {
properties.push(i.column.clone());
predicates.push(PredicateType::In);
}
Condition::Between(b) => {
properties.push(b.column.clone());
predicates.push(PredicateType::Range);
}
Condition::Like(l) => {
properties.push(l.column.clone());
predicates.push(PredicateType::Like);
}
Condition::And(lhs, rhs) | Condition::Or(lhs, rhs) => {
collect_condition_predicates(lhs, properties, predicates);
collect_condition_predicates(rhs, properties, predicates);
}
Condition::Not(inner) | Condition::Group(inner) => {
collect_condition_predicates(inner, properties, predicates);
}
_ => {}
}
}
fn merge_match_results(
graph_results: Vec<super::match_exec::MatchResult>,
vector_results: Vec<super::match_exec::MatchResult>,
higher_is_better: bool,
) -> Vec<super::match_exec::MatchResult> {
use std::collections::HashMap;
let mut by_node: HashMap<u64, Vec<super::match_exec::MatchResult>> =
HashMap::with_capacity(graph_results.len() + vector_results.len());
for row in graph_results {
by_node.entry(row.node_id).or_default().push(row);
}
for candidate in vector_results {
match by_node.entry(candidate.node_id) {
std::collections::hash_map::Entry::Occupied(mut group) => {
for row in group.get_mut() {
enrich_row(row, &candidate, higher_is_better);
}
}
std::collections::hash_map::Entry::Vacant(slot) => {
slot.insert(vec![candidate]);
}
}
}
let mut merged: Vec<super::match_exec::MatchResult> = by_node.into_values().flatten().collect();
sort_match_results_by_score(&mut merged, higher_is_better);
merged
}
fn enrich_row(
row: &mut super::match_exec::MatchResult,
candidate: &super::match_exec::MatchResult,
higher_is_better: bool,
) {
let worse_sentinel = if higher_is_better {
f32::NEG_INFINITY
} else {
f32::MAX
};
let candidate_score = candidate.score.unwrap_or(worse_sentinel);
let row_score = row.score.unwrap_or(worse_sentinel);
let candidate_wins = if higher_is_better {
candidate_score > row_score
} else {
candidate_score < row_score
};
if candidate_wins {
row.score = candidate.score;
}
merge_map(&mut row.projected, &candidate.projected, candidate_wins);
merge_map(&mut row.bindings, &candidate.bindings, candidate_wins);
merge_map(
&mut row.edge_bindings,
&candidate.edge_bindings,
candidate_wins,
);
merge_map(&mut row.edge_paths, &candidate.edge_paths, candidate_wins);
}
fn merge_map<V: Clone>(
target: &mut std::collections::HashMap<String, V>,
source: &std::collections::HashMap<String, V>,
source_wins: bool,
) {
for (key, value) in source {
if source_wins {
target.insert(key.clone(), value.clone());
} else {
target.entry(key.clone()).or_insert_with(|| value.clone());
}
}
}
fn sort_match_results_by_score(
merged: &mut [super::match_exec::MatchResult],
higher_is_better: bool,
) {
if higher_is_better {
merged.sort_unstable_by(|a, b| {
let sa = a.score.unwrap_or(f32::NEG_INFINITY);
let sb = b.score.unwrap_or(f32::NEG_INFINITY);
sb.total_cmp(&sa)
});
} else {
merged.sort_unstable_by(|a, b| {
let sa = a.score.unwrap_or(f32::MAX);
let sb = b.score.unwrap_or(f32::MAX);
sa.total_cmp(&sb)
});
}
}
#[cfg(test)]
#[path = "match_dispatch_tests.rs"]
mod tests;