use std::cmp::Ordering;
use std::collections::BinaryHeap;
use crate::store::StoredChunk;
use crate::types::RelativePath;
use super::cosine_similarity;
fn neighbor_rank(a: &Neighbor, b: &Neighbor) -> Ordering {
b.score
.partial_cmp(&a.score)
.unwrap_or(Ordering::Equal)
.then_with(|| a.file_path.cmp(&b.file_path))
.then_with(|| a.line_start.cmp(&b.line_start))
}
struct NeighborEntry(Neighbor);
impl PartialEq for NeighborEntry {
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == Ordering::Equal
}
}
impl Eq for NeighborEntry {}
impl PartialOrd for NeighborEntry {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for NeighborEntry {
fn cmp(&self, other: &Self) -> Ordering {
neighbor_rank(&self.0, &other.0)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Neighbor {
pub file_path: String,
pub line_start: u32,
pub line_end: u32,
pub name: Option<String>,
pub score: f32,
}
pub fn neighbors(
rows: &[StoredChunk],
query_vectors: &[Vec<f32>],
exclude_path: &RelativePath,
top_k: usize,
min_similarity: f32,
) -> Vec<Neighbor> {
if rows.is_empty() || query_vectors.is_empty() || top_k == 0 {
return Vec::new();
}
let mut best: std::collections::HashMap<&str, (f32, &StoredChunk)> =
std::collections::HashMap::new();
for row in rows {
if row.file_path == exclude_path.as_str() {
continue;
}
let row_score = query_vectors
.iter()
.map(|qv| cosine_similarity(qv, &row.vector).max(0.0))
.fold(0.0_f32, f32::max);
let entry = best.entry(row.file_path.as_str()).or_insert((0.0, row));
if row_score > entry.0 {
*entry = (row_score, row);
}
}
let mut heap: BinaryHeap<NeighborEntry> = BinaryHeap::with_capacity(top_k + 1);
for (score, row) in best.into_values() {
if score < min_similarity {
continue;
}
let candidate = Neighbor {
file_path: row.file_path.clone(),
line_start: row.line_start,
line_end: row.line_end,
name: row.name.clone(),
score,
};
if heap.len() == top_k {
if let Some(min_entry) = heap.peek()
&& neighbor_rank(&candidate, &min_entry.0) != Ordering::Less
{
continue;
}
heap.pop();
}
heap.push(NeighborEntry(candidate));
}
let mut candidates: Vec<Neighbor> = heap.into_iter().map(|e| e.0).collect();
candidates.sort_by(neighbor_rank);
candidates
}
#[cfg(test)]
mod tests {
use super::*;
use crate::store::StoredChunk;
fn make_row(file_path: &str, name: &str, vector: Vec<f32>) -> StoredChunk {
StoredChunk {
chunk_id: 0,
file_path: file_path.to_owned(),
language: "rust".into(),
kind: "function".into(),
name: Some(name.to_owned()),
line_start: 1,
line_end: 10,
byte_start: 0,
byte_end: 100,
file_hash: [0u8; 16],
content: format!("pub fn {name}() {{}}"),
vector,
}
}
#[test]
fn neighbors_excludes_edited_file() {
let rows = vec![
make_row("src/a.rs", "foo", vec![1.0, 0.0, 0.0, 0.0]),
make_row("src/b.rs", "bar", vec![0.9, 0.1, 0.0, 0.0]),
];
let query = vec![vec![1.0, 0.0, 0.0, 0.0]];
let exclude = RelativePath::new("src/a.rs");
let hits = neighbors(&rows, &query, &exclude, 5, 0.0);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].file_path, "src/b.rs");
}
#[test]
fn neighbors_respects_min_similarity_floor() {
let rows = vec![
make_row("src/close.rs", "similar", vec![1.0, 0.0, 0.0, 0.0]),
make_row("src/distant.rs", "unrelated", vec![0.0, 1.0, 0.0, 0.0]),
];
let query = vec![vec![1.0, 0.0, 0.0, 0.0]];
let exclude = RelativePath::new("src/edited.rs");
let hits = neighbors(&rows, &query, &exclude, 5, 0.65);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].file_path, "src/close.rs");
}
#[test]
fn neighbors_deduplicates_to_one_hit_per_file() {
let mut chunk1 = make_row("src/multi.rs", "alpha", vec![0.7, 0.3, 0.0, 0.0]);
let mut chunk2 = make_row("src/multi.rs", "beta", vec![0.9, 0.1, 0.0, 0.0]);
chunk1.line_start = 1;
chunk1.line_end = 10;
chunk2.line_start = 20;
chunk2.line_end = 30;
let rows = vec![chunk1, chunk2];
let query = vec![vec![1.0, 0.0, 0.0, 0.0]];
let exclude = RelativePath::new("src/edited.rs");
let hits = neighbors(&rows, &query, &exclude, 5, 0.0);
assert_eq!(
hits.len(),
1,
"two chunks from same file must collapse to one hit"
);
assert_eq!(hits[0].file_path, "src/multi.rs");
assert_eq!(hits[0].name.as_deref(), Some("beta"));
}
#[test]
fn neighbors_respects_top_k_limit() {
let rows = vec![
make_row("src/a.rs", "a", vec![0.9, 0.1, 0.0, 0.0]),
make_row("src/b.rs", "b", vec![0.8, 0.2, 0.0, 0.0]),
make_row("src/c.rs", "c", vec![0.7, 0.3, 0.0, 0.0]),
];
let query = vec![vec![1.0, 0.0, 0.0, 0.0]];
let exclude = RelativePath::new("src/edited.rs");
let hits = neighbors(&rows, &query, &exclude, 2, 0.0);
assert_eq!(hits.len(), 2);
assert_eq!(hits[0].file_path, "src/a.rs");
assert_eq!(hits[1].file_path, "src/b.rs");
}
#[test]
fn neighbors_returns_empty_when_nothing_above_floor() {
let rows = vec![make_row("src/b.rs", "bar", vec![0.0, 1.0, 0.0, 0.0])];
let query = vec![vec![1.0, 0.0, 0.0, 0.0]];
let exclude = RelativePath::new("src/edited.rs");
let hits = neighbors(&rows, &query, &exclude, 5, 0.65);
assert!(hits.is_empty());
}
#[test]
fn neighbors_tied_scores_yield_a_stable_set() {
let v = vec![1.0_f32, 0.0, 0.0, 0.0];
let rows = vec![
make_row("src/e.rs", "e", v.clone()),
make_row("src/b.rs", "b", v.clone()),
make_row("src/d.rs", "d", v.clone()),
make_row("src/a.rs", "a", v.clone()),
make_row("src/c.rs", "c", v.clone()),
];
let query = vec![v];
let exclude = RelativePath::new("src/edited.rs");
let expected = ["src/a.rs", "src/b.rs", "src/c.rs"];
for _ in 0..16 {
let hits = neighbors(&rows, &query, &exclude, 3, 0.0);
let paths: Vec<&str> = hits.iter().map(|h| h.file_path.as_str()).collect();
assert_eq!(paths, expected, "tied-score survivors must be stable");
}
}
#[test]
fn neighbors_uses_max_over_multiple_query_vectors() {
let rows = vec![make_row("src/b.rs", "bar", vec![0.0, 1.0, 0.0, 0.0])];
let query = vec![
vec![1.0, 0.0, 0.0, 0.0], vec![0.0, 1.0, 0.0, 0.0], ];
let exclude = RelativePath::new("src/edited.rs");
let hits = neighbors(&rows, &query, &exclude, 5, 0.65);
assert_eq!(
hits.len(),
1,
"file similar to any query vector must surface"
);
assert!((hits[0].score - 1.0).abs() < 1e-6);
}
}