feat: add Google code retrieval across desktop CLI and MCP
Core CI / core (macos-latest) (push) Canceled after 0s
Core CI / core (windows-latest) (push) Canceled after 0s
Core CI / desktop (macos-latest) (push) Canceled after 0s
Core CI / desktop (ubuntu-latest) (push) Canceled after 0s
Core CI / desktop (windows-latest) (push) Canceled after 0s
Core CI / core (ubuntu-latest) (push) Canceled after 2m23s
Core CI / core (macos-latest) (push) Canceled after 0s
Core CI / core (windows-latest) (push) Canceled after 0s
Core CI / desktop (macos-latest) (push) Canceled after 0s
Core CI / desktop (ubuntu-latest) (push) Canceled after 0s
Core CI / desktop (windows-latest) (push) Canceled after 0s
Core CI / core (ubuntu-latest) (push) Canceled after 2m23s
This commit is contained in:
1 parent
8b9b079469
commit
81846b3e98
32 files changed
+1303
-59
No files matched your search
@@ -54,6 +54,12 @@ pub trait EmbeddingProvider: Send {
|
||||
fn split_windows(&mut self, text: &str, title: &str) -> Result<Vec<TextWindow>>;
|
||||
fn embed_documents(&mut self, texts: &[&str], title: &str) -> Result<Vec<Vec<f32>>>;
|
||||
fn embed_query(&mut self, query: &str) -> Result<Vec<f32>>;
|
||||
fn embed_code_query(&mut self, _query: &str) -> Result<Vec<f32>> {
|
||||
anyhow::bail!("Provider does not support Google code retrieval")
|
||||
}
|
||||
fn embed_code_query_debug(&mut self, query: &str) -> Result<Vec<f32>> {
|
||||
self.embed_code_query(query)
|
||||
}
|
||||
fn embed_query_debug(&mut self, query: &str) -> Result<Vec<f32>> {
|
||||
self.embed_query(query)
|
||||
}
|
||||
@@ -90,6 +96,12 @@ impl EmbeddingProvider for GemmaEmbedder {
|
||||
fn embed_query_debug(&mut self, query: &str) -> Result<Vec<f32>> {
|
||||
GemmaEmbedder::embed_query_debug(self, query)
|
||||
}
|
||||
fn embed_code_query(&mut self, query: &str) -> Result<Vec<f32>> {
|
||||
GemmaEmbedder::embed_code_query(self, query)
|
||||
}
|
||||
fn embed_code_query_debug(&mut self, query: &str) -> Result<Vec<f32>> {
|
||||
GemmaEmbedder::embed_code_query_debug(self, query)
|
||||
}
|
||||
fn query_diagnostics(&self) -> std::collections::BTreeMap<String, u64> {
|
||||
GemmaEmbedder::query_diagnostics(self)
|
||||
}
|
||||
@@ -132,6 +144,11 @@ pub struct EmbeddingStatus {
|
||||
pub pending: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum QueryTask {
|
||||
Search,
|
||||
Code,
|
||||
}
|
||||
/// Shared local hybrid service. Google EmbeddingGemma 2 is the default and
|
||||
/// only shipped provider; no alternative model is silently substituted.
|
||||
pub struct HybridSearch {
|
||||
@@ -142,7 +159,7 @@ pub struct HybridSearch {
|
||||
// Real provider outputs only, scoped to this model session. Queries never
|
||||
// persist and results/filters are never cached here.
|
||||
query_cache_fingerprint: String,
|
||||
query_cache: VecDeque<(String, Vec<f32>)>,
|
||||
query_cache: VecDeque<(QueryTask, String, Vec<f32>)>,
|
||||
}
|
||||
|
||||
impl HybridSearch {
|
||||
@@ -396,7 +413,7 @@ impl HybridSearch {
|
||||
trace.timed("hybrid.identifier_priority", parent, |_, _| {
|
||||
promote_identifiers(query.trim(), &mut lexical)
|
||||
});
|
||||
let vector = self.query_vector(query, trace, parent)?;
|
||||
let vector = self.query_vector(query, options.code_only, trace, parent)?;
|
||||
let semantic =
|
||||
self.routed_semantic_traced(query, &vector, candidates, options, trace, parent)?;
|
||||
let results = trace.timed("hybrid.rank_fusion", parent, |trace, span| {
|
||||
@@ -419,13 +436,14 @@ impl HybridSearch {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut trace = TraceCollector::new(MetricsMode::Basic);
|
||||
let vector = self.query_vector(query, &mut trace, None)?;
|
||||
let vector = self.query_vector(query, options.code_only, &mut trace, None)?;
|
||||
self.routed_semantic(query, &vector, limit.min(1000), options)
|
||||
}
|
||||
|
||||
fn query_vector(
|
||||
&mut self,
|
||||
query: &str,
|
||||
code: bool,
|
||||
trace: &mut TraceCollector,
|
||||
parent: Option<SpanToken>,
|
||||
) -> Result<Vec<f32>> {
|
||||
@@ -435,6 +453,12 @@ impl HybridSearch {
|
||||
"Query must be nonempty and at most 4096 UTF-8 bytes"
|
||||
);
|
||||
let span = trace.begin("semantic.query_embedding", parent);
|
||||
let task = if code {
|
||||
QueryTask::Code
|
||||
} else {
|
||||
QueryTask::Search
|
||||
};
|
||||
trace.counter(span, "code_task", u64::from(code));
|
||||
if self.query_cache_fingerprint != self.embedder.fingerprint() {
|
||||
self.query_cache.clear();
|
||||
self.query_cache_fingerprint = self.embedder.fingerprint().to_owned();
|
||||
@@ -442,13 +466,13 @@ impl HybridSearch {
|
||||
if let Some(position) = self
|
||||
.query_cache
|
||||
.iter()
|
||||
.position(|(cached, _)| cached == query)
|
||||
.position(|(cached_task, cached, _)| *cached_task == task && cached == query)
|
||||
{
|
||||
let entry = self
|
||||
.query_cache
|
||||
.remove(position)
|
||||
.expect("Located cache entry");
|
||||
let vector = entry.1.clone();
|
||||
let vector = entry.2.clone();
|
||||
self.query_cache.push_back(entry);
|
||||
trace.counter(span, "query_cache_hit", 1);
|
||||
trace.counter(span, "query_cache_miss", 0);
|
||||
@@ -460,10 +484,11 @@ impl HybridSearch {
|
||||
trace.counter(span, "query_cache_hit", 0);
|
||||
trace.counter(span, "query_cache_miss", 1);
|
||||
let vector = (|| {
|
||||
let vector = if trace.is_debug() {
|
||||
self.embedder.embed_query_debug(query)?
|
||||
} else {
|
||||
self.embedder.embed_query(query)?
|
||||
let vector = match (code, trace.is_debug()) {
|
||||
(true, true) => self.embedder.embed_code_query_debug(query)?,
|
||||
(true, false) => self.embedder.embed_code_query(query)?,
|
||||
(false, true) => self.embedder.embed_query_debug(query)?,
|
||||
(false, false) => self.embedder.embed_query(query)?,
|
||||
};
|
||||
ensure!(
|
||||
vector.len() == self.embedder.dimensions()
|
||||
@@ -487,7 +512,7 @@ impl HybridSearch {
|
||||
self.query_cache.pop_front();
|
||||
}
|
||||
self.query_cache
|
||||
.push_back((query.to_owned(), vector.clone()));
|
||||
.push_back((task, query.to_owned(), vector.clone()));
|
||||
Ok(vector)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +116,11 @@ impl HybridSearch {
|
||||
parent: Option<SpanToken>,
|
||||
) -> Result<Vec<SearchResult>> {
|
||||
trace.timed("semantic.routing", parent, |trace, parent| {
|
||||
if let Some(origin) = preferred_origin(query) {
|
||||
if let Some(origin) = if options.code_only {
|
||||
None
|
||||
} else {
|
||||
preferred_origin(query)
|
||||
} {
|
||||
trace.counter(parent, "audio_preference", 1);
|
||||
let preferred = self.store.search_origin_traced(
|
||||
vector,
|
||||
@@ -157,7 +161,8 @@ impl HybridSearch {
|
||||
if query.trim().is_empty() || limit == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let vector = self.embedder.embed_query(query.trim())?;
|
||||
let mut trace = TraceCollector::new(MetricsMode::Basic);
|
||||
let vector = self.query_vector(query, options.code_only, &mut trace, None)?;
|
||||
self.store.search_origin(
|
||||
&vector,
|
||||
&self.generation_fingerprint,
|
||||
|
||||
@@ -28,6 +28,7 @@ struct FilterKey {
|
||||
min_duration: Option<u32>,
|
||||
max_duration: Option<u32>,
|
||||
source_type: Option<String>,
|
||||
code_only: bool,
|
||||
}
|
||||
|
||||
struct EligibilityCache {
|
||||
@@ -551,6 +552,7 @@ impl Store {
|
||||
min_duration: options.min_duration_ms,
|
||||
max_duration: options.max_duration_ms,
|
||||
source_type: source_type.map(str::to_owned),
|
||||
code_only: options.code_only,
|
||||
};
|
||||
trace.end(filter_span, Default::default());
|
||||
let eligibility_span = trace.begin("semantic.eligibility", parent);
|
||||
@@ -906,7 +908,12 @@ fn collect_eligibility(
|
||||
filter: &FilterKey,
|
||||
path: Option<&str>,
|
||||
) -> Result<EligibilityCache> {
|
||||
let mut statement=connection.prepare("SELECT c.rowid,c.passage_id,c.content_hash,p.text,c.path,c.source_type FROM semantic_chunks c
|
||||
let code = if filter.code_only {
|
||||
format!(" AND {}", searchi_core::code_file_predicate())
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let mut statement=connection.prepare(&format!("SELECT c.rowid,c.passage_id,c.content_hash,p.text,c.path,c.source_type FROM semantic_chunks c
|
||||
LEFT JOIN passages p ON c.source_type='text' AND p.rowid=c.passage_id AND p.file_path=c.path AND p.kind=c.kind AND p.number=c.number
|
||||
JOIN files f ON f.path=c.path WHERE c.generation=?1 AND ((c.source_type='text' AND p.rowid IS NOT NULL) OR (c.source_type!='text' AND c.file_size=f.size AND c.file_mtime=f.mtime))
|
||||
AND (?2 IS NULL OR substr(f.path,1,length(?2))=?2) AND (?3 IS NULL OR f.extension=?3)
|
||||
@@ -915,7 +922,7 @@ fn collect_eligibility(
|
||||
AND (?10 IS NULL OR EXISTS(SELECT 1 FROM media_metadata m WHERE m.path=f.path AND m.duration_ms>=?10))
|
||||
AND (?11 IS NULL OR EXISTS(SELECT 1 FROM media_metadata m WHERE m.path=f.path AND m.duration_ms<=?11))
|
||||
AND (?12 IS NULL OR c.source_type=?12)
|
||||
AND (?9 IS NULL OR c.path=?9) ORDER BY c.passage_id,c.ordinal LIMIT ?8")?;
|
||||
AND (?9 IS NULL OR c.path=?9) {code} ORDER BY c.passage_id,c.ordinal LIMIT ?8"))?;
|
||||
let mut rows = statement.query(params![
|
||||
generation,
|
||||
filter.folder,
|
||||
@@ -971,6 +978,78 @@ mod tests {
|
||||
use searchi_core::SearchIndex;
|
||||
const DIMENSIONS: usize = 768;
|
||||
|
||||
#[test]
|
||||
fn code_filter_precedes_ann_top_k_and_cache_distinguishes_scope() -> Result<()> {
|
||||
// Retrieval geometry only: production vectors come from Google.
|
||||
let directory = tempfile::tempdir()?;
|
||||
let root = directory.path().join("sources");
|
||||
std::fs::create_dir(&root)?;
|
||||
for n in 0..40 {
|
||||
std::fs::write(
|
||||
root.join(format!("document-{n}.md")),
|
||||
"parse JSON and retry requests",
|
||||
)?;
|
||||
}
|
||||
std::fs::write(
|
||||
root.join("network.rs"),
|
||||
"fn retry_request() { parse_json(); }",
|
||||
)?;
|
||||
std::fs::write(root.join("Makefile"), "build: compile_sources\n")?;
|
||||
let db = directory.path().join("index.db");
|
||||
let mut lexical = SearchIndex::open(&db)?;
|
||||
lexical.index_folder(&root)?;
|
||||
let mut store = Store::open(&db, DIMENSIONS)?;
|
||||
let generation = store.begin("code-filter-proof")?;
|
||||
let mut query = vec![0.; DIMENSIONS];
|
||||
query[0] = 1.;
|
||||
let mut weaker = query.clone();
|
||||
weaker[0] = 0.9;
|
||||
weaker[1] = (1.0_f32 - 0.9_f32.powi(2)).sqrt();
|
||||
for passage in lexical.passage_batch(0, 100)? {
|
||||
let vector = if searchi_core::is_code_path(Path::new(&passage.path)) {
|
||||
weaker.clone()
|
||||
} else {
|
||||
query.clone()
|
||||
};
|
||||
let window = TextWindow {
|
||||
text: passage.text.clone(),
|
||||
start_byte: 0,
|
||||
end_byte: passage.text.len(),
|
||||
};
|
||||
store.insert(generation, &passage, 0, &[window], &[vector])?;
|
||||
}
|
||||
store.activate(generation)?;
|
||||
assert!(
|
||||
store.search(&query, "code-filter-proof", 1, &SearchOptions::default())?[0]
|
||||
.filename
|
||||
.ends_with(".md")
|
||||
);
|
||||
let code = SearchOptions {
|
||||
code_only: true,
|
||||
..Default::default()
|
||||
};
|
||||
let hits = store.search(&query, "code-filter-proof", 2, &code)?;
|
||||
assert_eq!(hits.len(), 2);
|
||||
assert!(hits
|
||||
.iter()
|
||||
.all(|h| searchi_core::is_code_path(Path::new(&h.path))));
|
||||
let incompatible = SearchOptions {
|
||||
code_only: true,
|
||||
extension: Some("md".into()),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(store
|
||||
.search(&query, "code-filter-proof", 2, &incompatible)?
|
||||
.is_empty());
|
||||
std::fs::remove_file(root.join("network.rs"))?;
|
||||
lexical.index_folder(&root)?;
|
||||
assert_eq!(
|
||||
store.search(&query, "code-filter-proof", 2, &code)?.len(),
|
||||
1
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn many_video_windows_do_not_crowd_out_other_parent_files() -> Result<()> {
|
||||
// Geometry verifies grouped retrieval, not model inference or codecs.
|
||||
|
||||
Reference in new issue
Block a user