use std::cmp::Ordering;
use serde_json::{json, Value};
use crate::local::value;
use crate::pipeline::vector::VectorSpec;
pub const MAX_VECTOR_SCAN_ROWS: usize = 100_000;
pub fn parse_stage(arg: &Value) -> Result<VectorSpec, String> {
crate::pipeline::vector::parse_stage(None, arg)
}
pub fn score(metric: &str, a: &[f64], b: &[f64]) -> Result<f64, String> {
if a.len() != b.len() {
return Err(format!(
"向量维度不一致:查询 {} 维,行 {} 维(禁止截断或补零)",
a.len(),
b.len()
));
}
let r = match metric {
"cosine" => {
let (na, nb) = (norm(a), norm(b));
if na == 0.0 || nb == 0.0 {
return Err(format!("零向量不参与 cosine 打分(维度 {})", a.len()));
}
dot(a, b) / (na * nb)
}
"l2" => {
let sum: f64 = a.iter().zip(b.iter()).map(|(x, y)| (x - y) * (x - y)).sum();
1.0 / (1.0 + sum.sqrt())
}
"ip" => dot(a, b),
other => {
return Err(format!("不支持的向量度量 {other}(仅 cosine/l2/ip)"));
}
};
if !r.is_finite() {
return Err(format!("向量打分结果非有限值(metric={metric})"));
}
Ok(r)
}
pub fn search(rows: Vec<Value>, spec: &VectorSpec) -> Result<Vec<Value>, String> {
if rows.len() > MAX_VECTOR_SCAN_ROWS {
return Err(format!(
"vectorScanOverflow:候选行数 {} 超过上限 {}(禁止静默截断,请先缩小过滤条件)",
rows.len(),
MAX_VECTOR_SCAN_ROWS
));
}
let mut skipped_zero = 0_usize;
let mut scored: Vec<Value> = Vec::with_capacity(rows.len().min(spec.limit as usize));
for mut row in rows {
let raw = value::get_path(&row, &spec.field)
.ok_or_else(|| format!("候选行缺少向量字段 {}(禁止拿空列打分)", spec.field))?;
let b = row_vector(raw, &spec.field)?;
if norm(&b) == 0.0 {
skipped_zero += 1;
continue;
}
let s = score(&spec.metric, &spec.vector, &b)?;
value::set_path(&mut row, "_score", json!(s));
scored.push(row);
}
if skipped_zero > 0 {
eprintln!(
"vector_skipped_zero: 字段 {} 有 {skipped_zero} 行零向量未参与排序",
spec.field
);
}
scored.sort_by(|a, b| {
let sa = a.get("_score").and_then(Value::as_f64).unwrap_or(0.0);
let sb = b.get("_score").and_then(Value::as_f64).unwrap_or(0.0);
sb.partial_cmp(&sa)
.unwrap_or(Ordering::Equal)
.then_with(|| match (a.get("_id"), b.get("_id")) {
(Some(x), Some(y)) => value::compare(x, y),
(None, Some(_)) => Ordering::Less,
(Some(_), None) => Ordering::Greater,
(None, None) => Ordering::Equal,
})
});
if let Some(min) = spec.min_score {
scored.retain(|r| r.get("_score").and_then(Value::as_f64).unwrap_or(0.0) >= min);
}
scored.truncate(spec.limit as usize);
Ok(scored)
}
fn row_vector(raw: &Value, field: &str) -> Result<Vec<f64>, String> {
let arr = raw
.as_array()
.ok_or_else(|| format!("字段 {field} 的值不是数值数组(禁止拿空列打分)"))?;
let mut out = Vec::with_capacity(arr.len());
for (i, v) in arr.iter().enumerate() {
let n = v
.as_f64()
.ok_or_else(|| format!("字段 {field} 第 {i} 个元素不是数值,实际 {v}"))?;
if !n.is_finite() {
return Err(format!("字段 {field} 第 {i} 个元素不是有限数值,实际 {v}"));
}
out.push(n);
}
Ok(out)
}
fn dot(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}
fn norm(v: &[f64]) -> f64 {
v.iter().map(|x| x * x).sum::<f64>().sqrt()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pipeline::vector::build_stages;
use serde_json::json;
fn spec(limit: i64) -> VectorSpec {
VectorSpec {
field: "embedding".into(),
vector: vec![1.0, 0.0],
limit,
min_score: None,
metric: "cosine".into(),
filter: None,
residual: None,
index: "vector_index".into(),
}
}
fn rows() -> Vec<Value> {
vec![
json!({"_id": "a", "embedding": [1.0, 0.0]}),
json!({"_id": "b", "embedding": [0.0, 1.0]}),
json!({"_id": "c", "embedding": [3.0, 4.0]}),
json!({"_id": "d", "embedding": [0.5, 0.0]}),
]
}
#[test]
fn topk_matches_brute_force() {
let out = search(rows(), &spec(3)).unwrap();
let ids: Vec<&str> = out.iter().map(|r| r["_id"].as_str().unwrap()).collect();
assert_eq!(ids, vec!["a", "d", "c"]);
let scores: Vec<f64> = out.iter().map(|r| r["_score"].as_f64().unwrap()).collect();
assert!((scores[0] - 1.0).abs() < 1e-12);
assert!((scores[1] - 1.0).abs() < 1e-12);
assert!((scores[2] - 0.6).abs() < 1e-12);
}
#[test]
fn zero_vector_row_skipped_without_nan() {
let mut input = rows();
input.push(json!({"_id": "z", "embedding": [0.0, 0.0]}));
let out = search(input, &spec(10)).unwrap();
assert!(out.iter().all(|r| r["_id"] != "z"));
assert!(out
.iter()
.all(|r| r["_score"].as_f64().map(f64::is_finite) == Some(true)));
}
#[test]
fn dimension_mismatch_errors() {
let input = vec![json!({"_id": "x", "embedding": [1.0, 0.0, 0.0]})];
let err = search(input, &spec(1)).unwrap_err();
assert!(err.contains("维度"), "err = {err}");
assert!(score("cosine", &[1.0, 0.0], &[1.0]).is_err());
}
#[test]
fn l2_and_ip_scores_match_formulas() {
assert!((score("l2", &[1.0, 1.0], &[1.0, 3.0]).unwrap() - 1.0 / 3.0).abs() < 1e-12);
assert!((score("ip", &[2.0, 1.0], &[3.0, 4.0]).unwrap() - 10.0).abs() < 1e-12);
}
#[test]
fn min_score_filters_after_sort() {
let mut s = spec(10);
s.min_score = Some(0.9);
let out = search(rows(), &s).unwrap();
let ids: Vec<&str> = out.iter().map(|r| r["_id"].as_str().unwrap()).collect();
assert_eq!(ids, vec!["a", "d"]);
}
#[test]
fn pipeline_vector_search_scores_and_meta_is_noop() {
let collections = json!({ "artworks": rows() });
let stages = build_stages(&spec(2));
let out = crate::local::pipeline::run(&collections, "artworks", &stages).unwrap();
let ids: Vec<&str> = out.iter().map(|r| r["_id"].as_str().unwrap()).collect();
assert_eq!(ids, vec!["a", "d"]);
assert!((out[0]["_score"].as_f64().unwrap() - 1.0).abs() < 1e-12);
let mut later = stages.clone();
later.push(json!({ "$match": { "_id": "d" } }));
let out = crate::local::pipeline::run(&collections, "artworks", &later).unwrap();
assert_eq!(out.len(), 1);
assert_eq!(out[0]["_id"], "d");
}
}