use std::collections::hash_map::Entry;
use std::collections::{BinaryHeap, HashMap};
use anyhow::Result;
use reblessive::tree::Stk;
use super::args::Optional;
use crate::catalog::providers::DatabaseProvider;
use crate::ctx::FrozenContext;
use crate::dbs::Options;
use crate::doc::CursorDoc;
use crate::err::Error;
use crate::fnc::get_execution_context;
use crate::idx::ft::analyzer::Analyzer;
use crate::idx::ft::highlighter::HighlightParams;
use crate::val::{Array, Number, Object, Value};
pub async fn analyze(
(stk, ctx, opt): (&mut Stk, &FrozenContext, Option<&Options>),
(az, val): (Value, Value),
) -> Result<Value> {
if let (Some(opt), Value::String(az), Value::String(val)) = (opt, az, val) {
let (ns, db) = ctx.expect_ns_db_ids(opt).await?;
let az = ctx.tx().get_db_analyzer(ns, db, &az, opt.version).await?;
let az = Analyzer::new(ctx.get_index_stores(), az)?;
az.analyze(stk, ctx, opt, val).await
} else {
Ok(Value::None)
}
}
pub async fn score(
(ctx, doc): (&FrozenContext, Option<&CursorDoc>),
(match_ref,): (Value,),
) -> Result<Value> {
if let Some((exe, doc, thg)) = get_execution_context(ctx, doc) {
return exe.score(ctx, &match_ref, thg, doc.ir.as_ref()).await;
}
Ok(Value::None)
}
pub async fn highlight(
(ctx, doc): (&FrozenContext, Option<&CursorDoc>),
(prefix, suffix, match_ref, Optional(partial)): (Value, Value, Value, Optional<bool>),
) -> Result<Value> {
if let Some((exe, doc, thg)) = get_execution_context(ctx, doc) {
let hlp = HighlightParams {
prefix,
suffix,
match_ref,
partial: partial.unwrap_or(false),
};
return exe.highlight(ctx, thg, hlp, doc.doc.as_ref()).await;
}
Ok(Value::None)
}
pub async fn offsets(
(ctx, doc): (&FrozenContext, Option<&CursorDoc>),
(match_ref, Optional(partial)): (Value, Optional<bool>),
) -> Result<Value> {
if let Some((exe, _, thg)) = get_execution_context(ctx, doc) {
let partial = partial.unwrap_or(false);
return exe.offsets(ctx, thg, match_ref, partial).await;
}
Ok(Value::None)
}
struct ScoredDoc(f64, Value, Vec<Object>);
impl PartialEq for ScoredDoc {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl Eq for ScoredDoc {}
impl PartialOrd for ScoredDoc {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for ScoredDoc {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
other.0.partial_cmp(&self.0).unwrap_or(std::cmp::Ordering::Equal)
}
}
pub async fn rrf(
ctx: &FrozenContext,
(results, limit, rrf_constant): (Array, i64, Optional<i64>),
) -> Result<Value> {
let limit = if limit < 1 {
anyhow::bail!(Error::InvalidFunctionArguments {
name: "search::rrf".to_string(),
message: "limit must be at least 1".to_string(),
});
} else {
limit as usize
};
let rrf_constant = if let Some(rrf_constant) = rrf_constant.0 {
if rrf_constant < 0 {
anyhow::bail!(Error::InvalidFunctionArguments {
name: "search::rrf".to_string(),
message: "RRF constant must be at least 0".to_string(),
});
}
rrf_constant as f64
} else {
60.0
};
if results.is_empty() {
return Ok(Value::Array(Array::new()));
}
#[expect(clippy::mutable_key_type)]
let mut documents: HashMap<Value, (f64, Vec<Object>)> = HashMap::new();
let mut count = 0;
for result_list in results {
if let Value::Array(array) = result_list {
for (rank, doc) in array.into_iter().enumerate() {
if let Value::Object(mut obj) = doc {
if let Some(id_value) = obj.remove("id") {
let rrf_contribution = 1.0 / (rrf_constant + (rank + 1) as f64);
match documents.entry(id_value) {
Entry::Vacant(entry) => {
entry.insert((rrf_contribution, vec![obj]));
}
Entry::Occupied(e) => {
let (score, objects) = e.into_mut();
*score += rrf_contribution;
objects.push(obj);
}
}
}
}
if ctx.is_done(Some(count)).await? {
return Ok(Value::None);
}
count += 1;
}
}
}
let mut scored_docs = BinaryHeap::with_capacity(limit);
for (id, (score, objects)) in documents {
if scored_docs.len() < limit {
scored_docs.push(ScoredDoc(score, id, objects));
} else if let Some(ScoredDoc(heap_min_score, _, _)) = scored_docs.peek() {
if score > *heap_min_score {
scored_docs.pop(); scored_docs.push(ScoredDoc(score, id, objects)); }
}
if ctx.is_done(Some(count)).await? {
return Ok(Value::None);
}
count += 1;
}
let sorted_docs = scored_docs.into_sorted_vec();
let mut result_array = Array::with_capacity(sorted_docs.len());
for doc in sorted_docs {
let mut obj = Object::default();
for mut o in doc.2 {
obj.append(&mut o.0);
}
obj.insert("id", doc.1);
obj.insert("rrf_score", Value::Number(Number::Float(doc.0)));
result_array.push(Value::Object(obj));
if ctx.is_done(Some(count)).await? {
return Ok(Value::None);
}
count += 1;
}
Ok(Value::Array(result_array))
}
enum LinearNorm {
MinMax,
ZScore,
}
pub async fn linear(
ctx: &FrozenContext,
(results, weights, limit, norm): (Array, Array, i64, String),
) -> Result<Value> {
let limit = if limit < 1 {
anyhow::bail!(Error::InvalidFunctionArguments {
name: "search::linear".to_string(),
message: "Limit must be at least 1".to_string(),
});
} else {
limit as usize
};
if weights.len() != results.len() {
anyhow::bail!(Error::InvalidFunctionArguments {
name: "search::linear".to_string(),
message: "The results and the weights array should have the same length".to_string(),
});
}
for (i, weight) in weights.iter().enumerate() {
if !matches!(weight, Value::Number(_)) {
anyhow::bail!(Error::InvalidFunctionArguments {
name: "search::linear".to_string(),
message: format!("Weight at index {} must be a number", i),
});
}
}
let norm = match norm.as_str() {
"minmax" => LinearNorm::MinMax,
"zscore" => LinearNorm::ZScore,
_ => anyhow::bail!(Error::InvalidFunctionArguments {
name: "search::linear".to_string(),
message: "Norm must be 'minmax' or 'zscore'".to_string()
}),
};
if results.is_empty() {
return Ok(Value::Array(Array::new()));
}
let results_len = results.len();
#[expect(clippy::mutable_key_type)]
let mut documents: HashMap<Value, (Vec<f64>, Vec<Object>)> = HashMap::new();
let mut count = 0;
for (list_idx, result_list) in results.into_iter().enumerate() {
if let Value::Array(array) = result_list {
for doc in array {
if let Value::Object(mut obj) = doc {
if let Some(id_value) = obj.remove("id") {
let score = if let Some(Value::Number(n)) = obj.get("distance") {
1.0 / (1.0 + n.as_float())
} else if let Some(Value::Number(n)) = obj.get("ft_score") {
n.as_float()
} else if let Some(Value::Number(n)) = obj.get("score") {
n.as_float()
} else {
1.0 / (1.0 + count as f64)
};
match documents.entry(id_value) {
Entry::Vacant(entry) => {
let mut scores = vec![0.0; results_len];
scores[list_idx] = score;
entry.insert((scores, vec![obj]));
}
Entry::Occupied(e) => {
let (scores, objects) = e.into_mut();
scores[list_idx] = score;
objects.push(obj);
}
}
}
}
if ctx.is_done(Some(count)).await? {
return Ok(Value::None);
}
count += 1;
}
}
}
let mut all_scores_by_list: Vec<Vec<f64>> = vec![Vec::new(); results_len];
for (scores, _) in documents.values() {
for (list_idx, &score) in scores.iter().enumerate() {
if score > 0.0 {
all_scores_by_list[list_idx].push(score);
}
}
}
let mut normalized_params: Vec<(f64, f64)> = Vec::new();
for list_scores in &all_scores_by_list {
if list_scores.is_empty() {
normalized_params.push((0.0, 1.0));
continue;
}
match norm {
LinearNorm::MinMax => {
let min_score = list_scores.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let max_score = list_scores.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let range = max_score - min_score;
if range > 0.0 {
normalized_params.push((min_score, range));
} else {
normalized_params.push((min_score, 1.0));
}
}
LinearNorm::ZScore => {
let mean = list_scores.iter().sum::<f64>() / list_scores.len() as f64;
let variance = list_scores.iter().map(|&x| (x - mean).powi(2)).sum::<f64>()
/ list_scores.len() as f64;
let std_dev = variance.sqrt();
if std_dev > 0.0 {
normalized_params.push((mean, std_dev));
} else {
normalized_params.push((mean, 1.0));
}
}
}
}
let mut scored_docs = BinaryHeap::with_capacity(limit);
for (id, (scores, objects)) in documents {
let mut combined_score = 0.0;
for (list_idx, &score) in scores.iter().enumerate() {
if score > 0.0 {
let weight = if let Some(Value::Number(w)) = weights.get(list_idx) {
w.as_float()
} else {
1.0
};
let normalized_score = match norm {
LinearNorm::MinMax => {
let (min_val, range) = normalized_params[list_idx];
(score - min_val) / range
}
LinearNorm::ZScore => {
let (mean, std_dev) = normalized_params[list_idx];
(score - mean) / std_dev
}
};
combined_score += weight * normalized_score;
}
}
if scored_docs.len() < limit {
scored_docs.push(ScoredDoc(combined_score, id, objects));
} else if let Some(ScoredDoc(heap_min_score, _, _)) = scored_docs.peek()
&& combined_score > *heap_min_score
{
scored_docs.pop();
scored_docs.push(ScoredDoc(combined_score, id, objects));
}
if ctx.is_done(Some(count)).await? {
return Ok(Value::None);
}
count += 1;
}
let sorted_docs = scored_docs.into_sorted_vec();
let mut result_array = Array::with_capacity(sorted_docs.len());
for doc in sorted_docs {
let mut obj = Object::default();
for mut o in doc.2 {
obj.append(&mut o.0);
}
obj.insert("id", doc.1);
obj.insert("linear_score", Value::Number(Number::Float(doc.0)));
result_array.push(Value::Object(obj));
if ctx.is_done(Some(count)).await? {
return Ok(Value::None);
}
count += 1;
}
Ok(Value::Array(result_array))
}