use crate::collection::types::Collection;
use crate::error::Result;
use crate::point::SearchResult;
use crate::velesql::{ArithmeticExpr, ArithmeticOp};
use std::cmp::Ordering;
fn get_nested_payload<'a>(
payload: &'a serde_json::Value,
path: &str,
) -> Option<&'a serde_json::Value> {
let mut current = payload;
for segment in path.split('.') {
match current {
serde_json::Value::Object(map) => current = map.get(segment)?,
_ => return None,
}
}
Some(current)
}
#[must_use]
pub fn compare_json_values(
a: Option<&serde_json::Value>,
b: Option<&serde_json::Value>,
) -> Ordering {
match (a, b) {
(None, None) => Ordering::Equal,
(None, Some(_)) => Ordering::Less,
(Some(_), None) => Ordering::Greater,
(Some(va), Some(vb)) => {
let type_rank = |v: &serde_json::Value| -> u8 {
match v {
serde_json::Value::Null => 0,
serde_json::Value::Bool(_) => 1,
serde_json::Value::Number(_) => 2,
serde_json::Value::String(_) => 3,
serde_json::Value::Array(_) => 4,
serde_json::Value::Object(_) => 5,
}
};
let rank_a = type_rank(va);
let rank_b = type_rank(vb);
if rank_a != rank_b {
return rank_a.cmp(&rank_b);
}
match (va, vb) {
(serde_json::Value::Number(na), serde_json::Value::Number(nb)) => {
let fa = na.as_f64().unwrap_or(0.0);
let fb = nb.as_f64().unwrap_or(0.0);
fa.total_cmp(&fb) }
(serde_json::Value::String(sa), serde_json::Value::String(sb)) => sa.cmp(sb),
(serde_json::Value::Bool(ba), serde_json::Value::Bool(bb)) => ba.cmp(bb),
_ => Ordering::Equal,
}
}
}
}
impl Collection {
pub(crate) fn apply_order_by(
&self,
results: &mut [SearchResult],
order_by: &[crate::velesql::SelectOrderBy],
params: &std::collections::HashMap<String, serde_json::Value>,
) -> Result<()> {
self.apply_order_by_with_let(results, order_by, params, &[])
}
pub(crate) fn apply_order_by_with_let(
&self,
results: &mut [SearchResult],
order_by: &[crate::velesql::SelectOrderBy],
params: &std::collections::HashMap<String, serde_json::Value>,
per_result_let: &[Vec<(String, f32)>],
) -> Result<()> {
if order_by.is_empty() {
return Ok(());
}
let similarity_scores_map = self.precompute_similarity_scores(results, order_by, params)?;
let higher_is_better = self.storage.config.read().metric.higher_is_better();
let mut indices: Vec<usize> = (0..results.len()).collect();
indices.sort_unstable_by(|&i, &j| {
Self::compare_by_order_columns(
i,
j,
results,
order_by,
&similarity_scores_map,
higher_is_better,
per_result_let,
)
.then_with(|| results[i].point.id.cmp(&results[j].point.id))
});
let sorted_scores =
extract_sorted_similarity_scores(order_by, &similarity_scores_map, &indices);
apply_permutation_in_place(results, &mut indices);
if let Some(scores) = sorted_scores {
for (result, score) in results.iter_mut().zip(scores) {
result.score = score;
}
}
Ok(())
}
fn precompute_similarity_scores(
&self,
results: &[SearchResult],
order_by: &[crate::velesql::SelectOrderBy],
params: &std::collections::HashMap<String, serde_json::Value>,
) -> Result<std::collections::HashMap<usize, Vec<f32>>> {
use crate::velesql::OrderByExpr;
let mut map = std::collections::HashMap::new();
for (idx, ob) in order_by.iter().enumerate() {
match &ob.expr {
OrderByExpr::Similarity(sim) => {
let order_vec = Self::resolve_vector(&sim.vector, params)?;
let scores =
self.similarity_scores_for_field(results, &sim.field, &order_vec)?;
map.insert(idx, scores);
}
OrderByExpr::SimilarityBare => {
let scores: Vec<f32> = results.iter().map(|r| r.score).collect();
map.insert(idx, scores);
}
OrderByExpr::Field(_) | OrderByExpr::Aggregate(_) | OrderByExpr::Arithmetic(_) => {}
}
}
Ok(map)
}
fn similarity_scores_for_field(
&self,
results: &[SearchResult],
field: &str,
order_vec: &[f32],
) -> Result<Vec<f32>> {
let worst = if self.storage.config.read().metric.higher_is_better() {
f32::NEG_INFINITY
} else {
f32::INFINITY
};
results
.iter()
.map(|r| {
let vec: std::borrow::Cow<[f32]> = if field == "vector" {
std::borrow::Cow::Borrowed(&r.point.vector)
} else {
match self.get_vector_for_field(r.point.id, field)? {
Some(v) => std::borrow::Cow::Owned(v),
None => return Ok(worst),
}
};
if vec.len() != order_vec.len() || vec.is_empty() {
return Ok(worst);
}
Ok(self.compute_metric_score(&vec, order_vec))
})
.collect()
}
#[allow(clippy::too_many_arguments)]
fn compare_by_order_columns(
i: usize,
j: usize,
results: &[SearchResult],
order_by: &[crate::velesql::SelectOrderBy],
similarity_scores: &std::collections::HashMap<usize, Vec<f32>>,
higher_is_better: bool,
per_result_let: &[Vec<(String, f32)>],
) -> Ordering {
use crate::velesql::OrderByExpr;
for (idx, ob) in order_by.iter().enumerate() {
let cmp = match &ob.expr {
OrderByExpr::Similarity(_) | OrderByExpr::SimilarityBare => similarity_scores
.get(&idx)
.map_or(Ordering::Equal, |scores| scores[i].total_cmp(&scores[j])),
OrderByExpr::Field(field_name) => {
Self::compare_field_expr(field_name, i, j, results, per_result_let)
}
OrderByExpr::Aggregate(_) => Ordering::Equal,
OrderByExpr::Arithmetic(expr) => {
Self::compare_arithmetic(expr, i, j, results, per_result_let)
}
};
let is_similarity = matches!(
&ob.expr,
OrderByExpr::Similarity(_) | OrderByExpr::SimilarityBare
);
let directed_cmp =
Self::apply_sort_direction(cmp, ob.descending, is_similarity, higher_is_better);
if directed_cmp != Ordering::Equal {
return directed_cmp;
}
}
Ordering::Equal
}
fn compare_payload_field(
field_name: &str,
i: usize,
j: usize,
results: &[SearchResult],
) -> Ordering {
let val_i = results[i]
.point
.payload
.as_ref()
.and_then(|p| get_nested_payload(p, field_name));
let val_j = results[j]
.point
.payload
.as_ref()
.and_then(|p| get_nested_payload(p, field_name));
compare_json_values(val_i, val_j)
}
fn compare_field_or_let(
field_name: &str,
i: usize,
j: usize,
results: &[SearchResult],
per_result_let: &[Vec<(String, f32)>],
) -> Ordering {
if let (Some(let_i), Some(let_j)) = (per_result_let.get(i), per_result_let.get(j)) {
if let Some(vi) = let_i.iter().find(|(k, _)| k == field_name).map(|(_, v)| *v) {
let vj = let_j
.iter()
.find(|(k, _)| k == field_name)
.map_or(0.0, |(_, v)| *v);
return vi.total_cmp(&vj);
}
}
Self::compare_payload_field(field_name, i, j, results)
}
fn compare_field_expr(
field_name: &str,
i: usize,
j: usize,
results: &[SearchResult],
per_result_let: &[Vec<(String, f32)>],
) -> Ordering {
if is_builtin_score_variable(field_name) {
Self::compare_score_variable(field_name, i, j, results, per_result_let)
} else {
Self::compare_field_or_let(field_name, i, j, results, per_result_let)
}
}
fn compare_score_variable(
name: &str,
i: usize,
j: usize,
results: &[SearchResult],
per_result_let: &[Vec<(String, f32)>],
) -> Ordering {
let (ctx_i, ctx_j) = Self::score_context_pair(i, j, results, per_result_let);
ctx_i
.resolve_variable(name)
.total_cmp(&ctx_j.resolve_variable(name))
}
fn compare_arithmetic(
expr: &crate::velesql::ArithmeticExpr,
i: usize,
j: usize,
results: &[SearchResult],
per_result_let: &[Vec<(String, f32)>],
) -> Ordering {
let (ctx_i, ctx_j) = Self::score_context_pair(i, j, results, per_result_let);
let val_i = evaluate_arithmetic(expr, &ctx_i);
let val_j = evaluate_arithmetic(expr, &ctx_j);
val_i.total_cmp(&val_j)
}
fn score_context_pair<'a>(
i: usize,
j: usize,
results: &'a [SearchResult],
per_result_let: &'a [Vec<(String, f32)>],
) -> (ScoreContext<'a>, ScoreContext<'a>) {
let ctx_i = ScoreContext::with_let_bindings(
results[i].score,
results[i].point.payload.as_ref(),
results[i].component_scores.as_deref(),
per_result_let.get(i).map(Vec::as_slice),
);
let ctx_j = ScoreContext::with_let_bindings(
results[j].score,
results[j].point.payload.as_ref(),
results[j].component_scores.as_deref(),
per_result_let.get(j).map(Vec::as_slice),
);
(ctx_i, ctx_j)
}
fn apply_sort_direction(
cmp: Ordering,
descending: bool,
is_similarity: bool,
higher_is_better: bool,
) -> Ordering {
if descending {
if is_similarity && !higher_is_better {
cmp
} else {
cmp.reverse()
}
} else if is_similarity && !higher_is_better {
cmp.reverse()
} else {
cmp
}
}
}
fn is_builtin_score_variable(name: &str) -> bool {
matches!(
name,
"vector_score" | "graph_score" | "bm25_score" | "sparse_score" | "fused_score"
)
}
fn apply_permutation_in_place<T>(slice: &mut [T], perm: &mut [usize]) {
debug_assert_eq!(slice.len(), perm.len());
for i in 0..perm.len() {
let mut j = i;
while perm[j] != i {
let k = perm[j];
slice.swap(j, k);
perm[j] = j;
j = k;
}
perm[j] = j;
}
}
fn extract_sorted_similarity_scores(
order_by: &[crate::velesql::SelectOrderBy],
similarity_scores_map: &std::collections::HashMap<usize, Vec<f32>>,
indices: &[usize],
) -> Option<Vec<f32>> {
use crate::velesql::OrderByExpr;
let sim_idx = order_by
.iter()
.enumerate()
.find(|(_, ob)| {
matches!(
ob.expr,
OrderByExpr::Similarity(_) | OrderByExpr::SimilarityBare
)
})
.map(|(idx, _)| idx)?;
let scores = similarity_scores_map.get(&sim_idx)?;
Some(indices.iter().map(|&orig| scores[orig]).collect())
}
pub(crate) struct ScoreContext<'a> {
search_score: f32,
payload: Option<&'a serde_json::Value>,
component_scores: Option<&'a [(&'static str, f32)]>,
let_bindings: Option<&'a [(String, f32)]>,
}
impl<'a> ScoreContext<'a> {
#[allow(dead_code)] pub(crate) fn new(search_score: f32, payload: Option<&'a serde_json::Value>) -> Self {
Self {
search_score,
payload,
component_scores: None,
let_bindings: None,
}
}
#[allow(dead_code)] pub(crate) fn with_components(
search_score: f32,
payload: Option<&'a serde_json::Value>,
component_scores: Option<&'a [(&'static str, f32)]>,
) -> Self {
Self {
search_score,
payload,
component_scores,
let_bindings: None,
}
}
pub(crate) fn with_let_bindings(
search_score: f32,
payload: Option<&'a serde_json::Value>,
component_scores: Option<&'a [(&'static str, f32)]>,
let_bindings: Option<&'a [(String, f32)]>,
) -> Self {
Self {
search_score,
payload,
component_scores,
let_bindings,
}
}
fn resolve_variable(&self, name: &str) -> f32 {
if let Some(val) = self.lookup_let_binding(name) {
return val;
}
match name {
"fused_score" | "similarity" => self.search_score,
"vector_score" | "graph_score" | "bm25_score" | "sparse_score" => self
.lookup_component(name)
.unwrap_or_else(|| self.absent_component_default()),
_ => self.resolve_payload_variable(name),
}
}
fn absent_component_default(&self) -> f32 {
if self.component_scores.is_some() {
0.0
} else {
self.search_score
}
}
fn lookup_let_binding(&self, name: &str) -> Option<f32> {
self.let_bindings?
.iter()
.find(|(k, _)| k == name)
.map(|(_, v)| *v)
}
fn lookup_component(&self, name: &str) -> Option<f32> {
self.component_scores?
.iter()
.find(|(k, _)| *k == name)
.map(|(_, v)| *v)
}
fn resolve_payload_variable(&self, name: &str) -> f32 {
self.payload
.and_then(|p| get_nested_payload(p, name))
.and_then(serde_json::Value::as_f64)
.map_or(0.0, |v| {
#[allow(clippy::cast_possible_truncation)]
{
v as f32
}
})
}
}
pub(crate) fn evaluate_let_bindings(
bindings: &[crate::velesql::LetBinding],
search_score: f32,
payload: Option<&serde_json::Value>,
component_scores: Option<&[(&'static str, f32)]>,
) -> Vec<(String, f32)> {
let mut evaluated: Vec<(String, f32)> = Vec::with_capacity(bindings.len());
for binding in bindings {
let ctx = ScoreContext::with_let_bindings(
search_score,
payload,
component_scores,
Some(&evaluated),
);
let value = evaluate_arithmetic(&binding.expr, &ctx);
evaluated.push((binding.name.clone(), value));
}
evaluated
}
const MAX_ARITHMETIC_DEPTH: u8 = 64;
pub(crate) fn evaluate_arithmetic(expr: &ArithmeticExpr, ctx: &ScoreContext<'_>) -> f32 {
evaluate_arithmetic_inner(expr, ctx, 0)
}
fn evaluate_arithmetic_inner(expr: &ArithmeticExpr, ctx: &ScoreContext<'_>, depth: u8) -> f32 {
if depth >= MAX_ARITHMETIC_DEPTH {
return 0.0;
}
match expr {
ArithmeticExpr::Literal(v) => {
#[allow(clippy::cast_possible_truncation)]
{
*v as f32
}
}
ArithmeticExpr::Variable(name) => ctx.resolve_variable(name),
ArithmeticExpr::Similarity(_) => ctx.search_score,
ArithmeticExpr::BinaryOp { left, op, right } => {
let l = evaluate_arithmetic_inner(left, ctx, depth + 1);
let r = evaluate_arithmetic_inner(right, ctx, depth + 1);
match op {
ArithmeticOp::Add => l + r,
ArithmeticOp::Sub => l - r,
ArithmeticOp::Mul => l * r,
ArithmeticOp::Div => {
if r.abs() > f32::EPSILON {
l / r
} else {
0.0
}
}
}
}
}
}