///|
/// 稠密向量索引(暴力检索,适合中小规模本地 RAG)。
pub struct VectorIndex {
items : Array[ScoredDoc]
vectors : Array[Array[Double]]
} derive(ToJson, FromJson)
///|
pub fn VectorIndex::new() -> VectorIndex {
{ items: [], vectors: [], }
}
///|
/// 添加一个带向量的文档/分块,返回更新后的索引。
pub fn VectorIndex::add(
self : VectorIndex,
id : String,
vector : Array[Double],
) -> Unit {
self.items.push({ id, score: 0.0, })
self.vectors.push(vector)
}
///|
/// 按余弦相似度检索 Top-K。
pub fn VectorIndex::search(
self : VectorIndex,
query : Array[Double],
top_k? : Int = 10,
) -> Array[ScoredDoc] {
let scored : Array[(String, Double)] = []
for i = 0; i < self.items.length(); i = i + 1 {
scored.push((self.items[i].id, cosine(query, self.vectors[i])))
}
scored.sort_by(fn(a, b) {
let cmp = b.1.compare(a.1)
if cmp != 0 {
cmp
} else {
a.0.compare(b.0)
}
})
let out : Array[ScoredDoc] = []
for i = 0; i < scored.length() && i < top_k; i = i + 1 {
let (id, score) = scored[i]
if score > 0.0 {
out.push({ id, score, })
}
}
out
}
///|
/// 向量数量。
pub fn VectorIndex::count(self : VectorIndex) -> Int {
self.items.length()
}
///|
/// 计算两个向量的余弦相似度。维度不一致或零向量返回 0。
pub fn cosine(a : Array[Double], b : Array[Double]) -> Double {
if a.length() != b.length() || a.is_empty() {
return 0.0
}
let mut dot = 0.0
let mut na = 0.0
let mut nb = 0.0
for i = 0; i < a.length(); i = i + 1 {
dot = dot + a[i] * b[i]
na = na + a[i] * a[i]
nb = nb + b[i] * b[i]
}
if na == 0.0 || nb == 0.0 {
0.0
} else {
dot / (@math.pow(na, 0.5) * @math.pow(nb, 0.5))
}
}
///|
/// 用 RRF(Reciprocal Rank Fusion)融合两路检索结果。
pub fn rrf_fuse(
a : Array[ScoredDoc],
b : Array[ScoredDoc],
k? : Int = 60,
top_k? : Int = 10,
) -> Array[ScoredDoc] {
let scores : Map[String, Double] = Map([])
let mut rank = 1
for item in a {
scores[item.id] = scores.get_or_default(item.id, 0.0) +
1.0 / (k.to_double() + rank.to_double())
rank = rank + 1
}
rank = 1
for item in b {
scores[item.id] = scores.get_or_default(item.id, 0.0) +
1.0 / (k.to_double() + rank.to_double())
rank = rank + 1
}
let arr : Array[(String, Double)] = []
for kv in scores.iter() {
arr.push(kv)
}
arr.sort_by(fn(a, b) {
let cmp = b.1.compare(a.1)
if cmp != 0 {
cmp
} else {
a.0.compare(b.0)
}
})
let out : Array[ScoredDoc] = []
for i = 0; i < arr.length() && i < top_k; i = i + 1 {
let (id, score) = arr[i]
out.push({ id, score, })
}
out
}