From 13b1c7612fe9d2ef479dc0ff105cf5a71704e559 Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Wed, 14 Oct 2026 17:46:27 +0900 Subject: [PATCH 1/3] fix(index): restrict find_related to the seed chunk's language (#113) Mirror upstream SembleIndex.find_related: when the seed chunk has a language, pass a same-language selector to the semantic query so related results no longer mix languages. Seeds without a language still search the whole index. --- crates/csp/src/indexing/index.rs | 13 +++++- crates/csp/src/indexing/index/tests.rs | 60 ++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/crates/csp/src/indexing/index.rs b/crates/csp/src/indexing/index.rs index 5570218..e284a77 100644 --- a/crates/csp/src/indexing/index.rs +++ b/crates/csp/src/indexing/index.rs @@ -255,17 +255,26 @@ impl CspIndex { ) } - /// Find chunks similar to a seed, excluding the seed itself. + /// Find chunks similar to a seed, excluding the seed itself. Candidates are + /// limited to the seed's language when it has one. pub fn find_related(&self, seed: &Chunk, options: &QueryOptions) -> Vec { let top_k = options.top_k.unwrap_or(DEFAULT_TOP_K); if top_k == 0 || self.chunks.is_empty() { return Vec::new(); } + // Mirror upstream: restrict candidates to the seed's language when it has one. + let selector = seed.language.as_ref().and_then(|lang| { + self.build_selector(&QueryOptions { + filter_languages: Some(vec![lang.clone()]), + ..Default::default() + }) + }); + let query_embedding = self.model.encode(std::slice::from_ref(&seed.content)); let batch = self .semantic_index - .query(&query_embedding, top_k + 1, None) + .query(&query_embedding, top_k + 1, selector.as_deref()) .unwrap_or_default(); let Some(first) = batch.into_iter().next() else { return Vec::new(); diff --git a/crates/csp/src/indexing/index/tests.rs b/crates/csp/src/indexing/index/tests.rs index 97d5377..f67c855 100644 --- a/crates/csp/src/indexing/index/tests.rs +++ b/crates/csp/src/indexing/index/tests.rs @@ -121,6 +121,66 @@ fn find_related_excludes_seed() { assert!(results.len() <= 5); } +#[test] +fn find_related_restricts_to_seed_language() { + let chunks = vec![ + make_chunk( + "a.rs", + 1, + 3, + Some("rust"), + "fn add_numbers(a: i32, b: i32) -> i32 { a + b }", + ), + make_chunk( + "b.rs", + 1, + 3, + Some("rust"), + "fn add_values(a: i32, b: i32) -> i32 { a + b }", + ), + make_chunk( + "c.py", + 1, + 3, + Some("python"), + "def add_numbers(a, b): return a + b", + ), + make_chunk( + "d.py", + 1, + 3, + Some("python"), + "def add_values(a, b): return a + b", + ), + ]; + let idx = build_index(chunks.clone()); + let opts = QueryOptions { + top_k: Some(5), + ..Default::default() + }; + let results = idx.find_related(&chunks[0], &opts); + assert_eq!(results.len(), 1); + assert_eq!(results[0].chunk.file_path, "b.rs"); +} + +#[test] +fn find_related_without_seed_language_searches_whole_index() { + let chunks = vec![ + make_chunk("a.txt", 1, 3, None, "seed"), + make_chunk("b.rs", 1, 3, Some("rust"), "fn b() {}"), + make_chunk("c.py", 1, 3, Some("python"), "def c(): pass"), + ]; + let idx = build_index(chunks.clone()); + let opts = QueryOptions { + top_k: Some(5), + ..Default::default() + }; + let results = idx.find_related(&chunks[0], &opts); + let mut paths: Vec<_> = results.iter().map(|r| r.chunk.file_path.as_str()).collect(); + paths.sort_unstable(); + assert_eq!(paths, vec!["b.rs", "c.py"]); +} + #[test] fn save_load_roundtrip() { let chunks = vec![ From 15fcd84e877fbc5d9a2c7f7a2c2903ffe445c1c0 Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Wed, 14 Oct 2026 18:12:59 +0900 Subject: [PATCH 2/3] fix(index): treat an empty seed language as unrestricted in find_related --- crates/csp/src/indexing/index.rs | 19 ++++++++++++------- crates/csp/src/indexing/index/tests.rs | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/crates/csp/src/indexing/index.rs b/crates/csp/src/indexing/index.rs index e284a77..7901f07 100644 --- a/crates/csp/src/indexing/index.rs +++ b/crates/csp/src/indexing/index.rs @@ -263,13 +263,18 @@ impl CspIndex { return Vec::new(); } - // Mirror upstream: restrict candidates to the seed's language when it has one. - let selector = seed.language.as_ref().and_then(|lang| { - self.build_selector(&QueryOptions { - filter_languages: Some(vec![lang.clone()]), - ..Default::default() - }) - }); + // Mirror upstream (`if target.language`): an absent or empty language is + // falsy there, so the search stays unrestricted. + let selector = seed + .language + .as_ref() + .filter(|lang| !lang.is_empty()) + .and_then(|lang| { + self.build_selector(&QueryOptions { + filter_languages: Some(vec![lang.clone()]), + ..Default::default() + }) + }); let query_embedding = self.model.encode(std::slice::from_ref(&seed.content)); let batch = self diff --git a/crates/csp/src/indexing/index/tests.rs b/crates/csp/src/indexing/index/tests.rs index f67c855..c3acb8b 100644 --- a/crates/csp/src/indexing/index/tests.rs +++ b/crates/csp/src/indexing/index/tests.rs @@ -181,6 +181,24 @@ fn find_related_without_seed_language_searches_whole_index() { assert_eq!(paths, vec!["b.rs", "c.py"]); } +#[test] +fn find_related_with_empty_seed_language_searches_whole_index() { + let chunks = vec![ + make_chunk("a.txt", 1, 3, Some(""), "seed"), + make_chunk("b.rs", 1, 3, Some("rust"), "fn b() {}"), + make_chunk("c.py", 1, 3, Some("python"), "def c(): pass"), + ]; + let idx = build_index(chunks.clone()); + let opts = QueryOptions { + top_k: Some(5), + ..Default::default() + }; + let results = idx.find_related(&chunks[0], &opts); + let mut paths: Vec<_> = results.iter().map(|r| r.chunk.file_path.as_str()).collect(); + paths.sort_unstable(); + assert_eq!(paths, vec!["b.rs", "c.py"]); +} + #[test] fn save_load_roundtrip() { let chunks = vec![ From 5900043f2a3bf066d1afee38beeb3eea7a917412 Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Wed, 14 Oct 2026 18:39:44 +0900 Subject: [PATCH 3/3] perf(index): skip embedding in find_related when the language selector is empty --- crates/csp/src/indexing/index.rs | 3 +++ crates/csp/src/indexing/index/tests.rs | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/crates/csp/src/indexing/index.rs b/crates/csp/src/indexing/index.rs index 7901f07..61d1d28 100644 --- a/crates/csp/src/indexing/index.rs +++ b/crates/csp/src/indexing/index.rs @@ -275,6 +275,9 @@ impl CspIndex { ..Default::default() }) }); + if selector.as_ref().is_some_and(Vec::is_empty) { + return Vec::new(); + } let query_embedding = self.model.encode(std::slice::from_ref(&seed.content)); let batch = self diff --git a/crates/csp/src/indexing/index/tests.rs b/crates/csp/src/indexing/index/tests.rs index c3acb8b..cf4c178 100644 --- a/crates/csp/src/indexing/index/tests.rs +++ b/crates/csp/src/indexing/index/tests.rs @@ -199,6 +199,18 @@ fn find_related_with_empty_seed_language_searches_whole_index() { assert_eq!(paths, vec!["b.rs", "c.py"]); } +#[test] +fn find_related_returns_empty_when_seed_language_is_absent_from_index() { + let chunks = vec![make_chunk("a.rs", 1, 3, Some("rust"), "fn a() {}")]; + let idx = build_index(chunks); + let seed = make_chunk("b.py", 1, 3, Some("python"), "def b(): pass"); + let opts = QueryOptions { + top_k: Some(5), + ..Default::default() + }; + assert!(idx.find_related(&seed, &opts).is_empty()); +} + #[test] fn save_load_roundtrip() { let chunks = vec![