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
@@ -68,6 +68,9 @@ enum Command {
|
||||
/// Restrict results to this folder and descendants.
|
||||
#[arg(long)]
|
||||
folder: Option<PathBuf>,
|
||||
/// Search indexed source-code files only; supports symbols and semantic code queries.
|
||||
#[arg(long)]
|
||||
code_only: bool,
|
||||
/// File extension, with or without the leading dot.
|
||||
#[arg(long)]
|
||||
extension: Option<String>,
|
||||
@@ -166,6 +169,7 @@ fn main() -> Result<()> {
|
||||
performance,
|
||||
debug_performance,
|
||||
folder,
|
||||
code_only,
|
||||
extension,
|
||||
min_size,
|
||||
max_size,
|
||||
@@ -183,6 +187,7 @@ fn main() -> Result<()> {
|
||||
require_existing_index(&db)?;
|
||||
let options = SearchOptions {
|
||||
folder,
|
||||
code_only,
|
||||
extension,
|
||||
min_size,
|
||||
max_size,
|
||||
|
||||
@@ -38,6 +38,9 @@ struct SearchInput {
|
||||
limit: u16,
|
||||
|
||||
folder: Option<String>,
|
||||
/// Restrict all retrieval lanes to indexed source-code files.
|
||||
#[serde(default)]
|
||||
code_only: bool,
|
||||
|
||||
extension: Option<String>,
|
||||
|
||||
@@ -168,6 +171,7 @@ impl AgentServer {
|
||||
let options = SearchOptions {
|
||||
|
||||
folder: input.folder.map(PathBuf::from),
|
||||
code_only: input.code_only,
|
||||
|
||||
extension: input.extension,
|
||||
|
||||
@@ -272,6 +276,7 @@ impl AgentServer {
|
||||
Ok(json!({
|
||||
|
||||
"mode": mode,
|
||||
"code_only": input.code_only,
|
||||
|
||||
"semantic_available": mode == "hybrid",
|
||||
|
||||
|
||||
@@ -157,3 +157,55 @@ fn exclusions_and_filters_change_real_results() {
|
||||
let stats: serde_json::Value = serde_json::from_slice(&stats.stdout).unwrap();
|
||||
assert_eq!(stats["files"], 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_only_filters_real_results_and_finds_exact_symbols() {
|
||||
let fixture = Fixture::new();
|
||||
fs::create_dir(fixture.0.join("code")).unwrap();
|
||||
fs::write(
|
||||
fixture.0.join("code/retry.rs"),
|
||||
"pub fn refreshAccessToken() { /* retry failed requests with backoff */ }",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
fixture.0.join("code/notes.txt"),
|
||||
"refreshAccessToken retry failed requests with backoff",
|
||||
)
|
||||
.unwrap();
|
||||
let indexed = fixture.run(&["index", "--db", "index.db", "--root", "code"]);
|
||||
assert!(
|
||||
indexed.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&indexed.stderr)
|
||||
);
|
||||
let all = fixture.run(&["search", "--db", "index.db", "refreshAccessToken", "--json"]);
|
||||
assert_eq!(
|
||||
serde_json::from_slice::<serde_json::Value>(&all.stdout)
|
||||
.unwrap()
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.len(),
|
||||
2
|
||||
);
|
||||
let code = fixture.run(&[
|
||||
"search",
|
||||
"--db",
|
||||
"index.db",
|
||||
"refreshAccessToken",
|
||||
"--code-only",
|
||||
"--json",
|
||||
]);
|
||||
assert!(
|
||||
code.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&code.stderr)
|
||||
);
|
||||
let results: serde_json::Value = serde_json::from_slice(&code.stdout).unwrap();
|
||||
assert_eq!(results.as_array().unwrap().len(), 1);
|
||||
assert_eq!(results[0]["filename"], "retry.rs");
|
||||
assert!(results[0]["snippet"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains("refreshAccessToken"));
|
||||
assert_eq!(results[0]["source"]["kind"], "line");
|
||||
}
|
||||
@@ -75,6 +75,16 @@ fn actual_mcp_handshake_search_and_guarded_sources() {
|
||||
"Termination clause: sixty days written notice to end the supplier partnership.",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
folder.join("retry.rs"),
|
||||
"pub fn refreshAccessToken() { /* retry requests */ }",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
folder.join("symbol-note.txt"),
|
||||
"refreshAccessToken retry requests",
|
||||
)
|
||||
.unwrap();
|
||||
let db = fixture.path().join("index.sqlite");
|
||||
let indexed = Command::new(env!("CARGO_BIN_EXE_searchi"))
|
||||
.args(["index", "--db"])
|
||||
@@ -154,6 +164,21 @@ fn actual_mcp_handshake_search_and_guarded_sources() {
|
||||
.unwrap()
|
||||
.contains("local path"));
|
||||
}
|
||||
let code = server.request(
|
||||
40,
|
||||
"tools/call",
|
||||
json!({"name":"search","arguments":{"query":"refreshAccessToken","code_only":true}}),
|
||||
);
|
||||
let code_results = code["result"]["structuredContent"]["results"]
|
||||
.as_array()
|
||||
.unwrap();
|
||||
assert_eq!(code_results.len(), 1);
|
||||
assert_eq!(code_results[0]["filename"], "retry.rs");
|
||||
assert_eq!(code_results[0]["source"]["kind"], "line");
|
||||
assert!(code_results[0]["snippet"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains("refreshAccessToken"));
|
||||
let mutation = server.request(7, "tools/call", json!({"name":"index","arguments":{}}));
|
||||
assert!(mutation["error"].is_object());
|
||||
std::fs::write(
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
//! Shared code-file classification for extraction and pre-ranking scope filters.
|
||||
use std::{path::Path, sync::OnceLock};
|
||||
|
||||
pub const CODE_EXTENSIONS: &[&str] = &[
|
||||
"rs", "py", "pyi", "js", "jsx", "mjs", "cjs", "ts", "tsx", "mts", "cts", "c", "h", "cpp", "cc",
|
||||
"cxx", "hpp", "hxx", "hh", "go", "java", "kt", "kts", "cs", "swift", "m", "mm", "rb", "php",
|
||||
"phtml", "scala", "sc", "sql", "sh", "bash", "zsh", "fish", "ps1", "psm1", "psd1", "bat",
|
||||
"cmd", "lua", "r", "pl", "pm", "ex", "exs", "erl", "hrl", "hs", "lhs", "clj", "cljs", "cljc",
|
||||
"dart", "vue", "svelte", "graphql", "gql", "tf", "tfvars", "proto", "zig", "vb", "vbs", "f",
|
||||
"for", "f90", "f95", "f03", "pas", "d", "cmake",
|
||||
];
|
||||
const CODE_FILENAMES: &[&str] = &[
|
||||
"makefile",
|
||||
"gnumakefile",
|
||||
"dockerfile",
|
||||
"cmakelists.txt",
|
||||
"justfile",
|
||||
];
|
||||
|
||||
pub fn is_code_path(path: &Path) -> bool {
|
||||
let extension = path
|
||||
.extension()
|
||||
.and_then(|value| value.to_str())
|
||||
.unwrap_or("")
|
||||
.to_ascii_lowercase();
|
||||
let filename = path
|
||||
.file_name()
|
||||
.and_then(|value| value.to_str())
|
||||
.unwrap_or("")
|
||||
.to_ascii_lowercase();
|
||||
CODE_EXTENSIONS.contains(&extension.as_str()) || CODE_FILENAMES.contains(&filename.as_str())
|
||||
}
|
||||
|
||||
/// Static trusted SQL predicate for the shared internal `files f` alias. No
|
||||
/// caller-controlled query, path or extension is interpolated into this SQL.
|
||||
pub fn code_file_predicate() -> &'static str {
|
||||
static SQL: OnceLock<String> = OnceLock::new();
|
||||
SQL.get_or_init(|| {
|
||||
let literals = |values: &[&str]| {
|
||||
values
|
||||
.iter()
|
||||
.map(|value| format!("'{value}'"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
};
|
||||
format!(
|
||||
"(f.extension IN ({}) OR lower(f.filename) IN ({}))",
|
||||
literals(CODE_EXTENSIONS),
|
||||
literals(CODE_FILENAMES)
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -33,27 +33,30 @@ pub(crate) fn extract(path: &Path) -> Result<Option<Vec<Passage>>> {
|
||||
docx(&bytes).map(Some)
|
||||
};
|
||||
}
|
||||
if !matches!(
|
||||
extension.as_str(),
|
||||
"txt"
|
||||
| "md"
|
||||
| "rs"
|
||||
| "py"
|
||||
| "js"
|
||||
| "ts"
|
||||
| "tsx"
|
||||
| "jsx"
|
||||
| "json"
|
||||
| "toml"
|
||||
| "yaml"
|
||||
| "yml"
|
||||
| "csv"
|
||||
| "html"
|
||||
| "css"
|
||||
| "xml"
|
||||
| "log"
|
||||
| "sh"
|
||||
) {
|
||||
let code = crate::is_code_path(path);
|
||||
if !code
|
||||
&& !matches!(
|
||||
extension.as_str(),
|
||||
"txt"
|
||||
| "md"
|
||||
| "rs"
|
||||
| "py"
|
||||
| "js"
|
||||
| "ts"
|
||||
| "tsx"
|
||||
| "jsx"
|
||||
| "json"
|
||||
| "toml"
|
||||
| "yaml"
|
||||
| "yml"
|
||||
| "csv"
|
||||
| "html"
|
||||
| "css"
|
||||
| "xml"
|
||||
| "log"
|
||||
| "sh"
|
||||
)
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
let file = File::open(path)?;
|
||||
@@ -69,6 +72,26 @@ pub(crate) fn extract(path: &Path) -> Result<Option<Vec<Passage>>> {
|
||||
return Ok(None);
|
||||
};
|
||||
let lines = text.split_inclusive('\n').collect::<Vec<_>>();
|
||||
if code {
|
||||
let mut passages = Vec::new();
|
||||
for start in (0..lines.len()).step_by(96) {
|
||||
if passages.len() >= MAX_PASSAGES {
|
||||
return Ok(None);
|
||||
}
|
||||
let end = (start + 128).min(lines.len());
|
||||
passages.push(Passage {
|
||||
text: lines[start..end].concat(),
|
||||
source: SourceLocation {
|
||||
kind: "line".into(),
|
||||
number: (start + 1) as u32,
|
||||
},
|
||||
});
|
||||
if end == lines.len() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Ok(Some(passages));
|
||||
}
|
||||
Ok(Some(
|
||||
lines
|
||||
.chunks(64)
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
//! Reconciliation is atomic. Incomplete directory walks never delete records.
|
||||
//! Text and document extraction have byte limits, but still run in this process.
|
||||
//! Worker isolation remains a production milestone.
|
||||
mod code;
|
||||
mod extraction;
|
||||
pub use code::{code_file_predicate, is_code_path, CODE_EXTENSIONS};
|
||||
pub mod metrics;
|
||||
use anyhow::{bail, Context, Result};
|
||||
pub use metrics::{MetricsMode, SearchMetrics, SearchResponse, SearchSpan};
|
||||
@@ -19,7 +21,7 @@ use std::{
|
||||
};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
const SCHEMA_VERSION: i64 = 10;
|
||||
const SCHEMA_VERSION: i64 = 11;
|
||||
const MAX_RESULTS: usize = 1000;
|
||||
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
@@ -50,6 +52,8 @@ impl Default for IndexOptions {
|
||||
/// The folder is canonicalized and includes descendants, with a path-boundary check.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct SearchOptions {
|
||||
/// Restrict every retrieval lane to source/build-script files before top-K.
|
||||
pub code_only: bool,
|
||||
pub folder: Option<PathBuf>,
|
||||
pub extension: Option<String>,
|
||||
pub min_size: Option<u64>,
|
||||
@@ -242,6 +246,16 @@ impl SearchIndex {
|
||||
CREATE TRIGGER IF NOT EXISTS files_media_delete AFTER DELETE ON files BEGIN DELETE FROM media_metadata WHERE path=old.path; END;
|
||||
CREATE TRIGGER IF NOT EXISTS files_media_update AFTER UPDATE ON files WHEN new.path!=old.path OR new.size!=old.size OR new.mtime!=old.mtime BEGIN DELETE FROM media_metadata WHERE path=old.path; END;
|
||||
PRAGMA user_version=10;")?;
|
||||
if version < 11 {
|
||||
initialization.execute(
|
||||
&format!(
|
||||
"UPDATE files AS f SET mtime=-1 WHERE {}",
|
||||
code_file_predicate()
|
||||
),
|
||||
[],
|
||||
)?;
|
||||
}
|
||||
initialization.execute_batch("PRAGMA user_version=11;")?;
|
||||
initialization.commit()?;
|
||||
}
|
||||
Ok(Self {
|
||||
@@ -620,12 +634,16 @@ impl SearchIndex {
|
||||
trace.end(normalization, Default::default());
|
||||
let planning = trace.begin("lexical.candidate_plan", parent);
|
||||
let limit = limit.min(MAX_RESULTS);
|
||||
let filters = " AND (?3 IS NULL OR substr(f.path,1,length(?3))=?3)
|
||||
let mut filters = String::from(" AND (?3 IS NULL OR substr(f.path,1,length(?3))=?3)
|
||||
AND (?4 IS NULL OR f.extension=?4)
|
||||
AND (?5 IS NULL OR f.size>=?5) AND (?6 IS NULL OR f.size<=?6)
|
||||
AND (?7 IS NULL OR f.mtime/1000000>=?7) AND (?8 IS NULL OR f.mtime/1000000<=?8)
|
||||
AND (?10 IS NULL OR EXISTS(SELECT 1 FROM media_metadata m WHERE m.file_id=f.id AND m.duration_ms>=?10))
|
||||
AND (?11 IS NULL OR EXISTS(SELECT 1 FROM media_metadata m WHERE m.file_id=f.id AND m.duration_ms<=?11))";
|
||||
AND (?11 IS NULL OR EXISTS(SELECT 1 FROM media_metadata m WHERE m.file_id=f.id AND m.duration_ms<=?11))");
|
||||
if options.code_only {
|
||||
filters.push_str(" AND ");
|
||||
filters.push_str(code_file_predicate());
|
||||
}
|
||||
let filter_values = [
|
||||
folder_prefix.map_or(Value::Null, Value::Text),
|
||||
extension.map_or(Value::Null, Value::Text),
|
||||
@@ -639,6 +657,7 @@ impl SearchIndex {
|
||||
.map_or(Value::Null, Value::Integer),
|
||||
];
|
||||
let has_filters = filter_values.iter().any(|value| *value != Value::Null)
|
||||
|| options.code_only
|
||||
|| options.min_duration_ms.is_some()
|
||||
|| options.max_duration_ms.is_some();
|
||||
let mut results = Vec::new();
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
use anyhow::Result;
|
||||
use searchi_core::{SearchIndex, SearchOptions, SourceLocation};
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn code_scope_filters_before_limit_and_extracts_build_files() -> Result<()> {
|
||||
let root = tempfile::tempdir()?;
|
||||
for number in 0..100 {
|
||||
fs::write(
|
||||
root.path().join(format!("early{number}.txt")),
|
||||
"refreshAccessToken",
|
||||
)?;
|
||||
}
|
||||
fs::write(
|
||||
root.path().join("last.go"),
|
||||
"func refreshAccessToken() {}\n",
|
||||
)?;
|
||||
fs::write(
|
||||
root.path().join("Makefile"),
|
||||
"refreshAccessToken:\n\techo ready\n",
|
||||
)?;
|
||||
fs::write(root.path().join("large.rs"), "\n".repeat(1_000_000))?;
|
||||
let mut index = SearchIndex::open(":memory:")?;
|
||||
index.index_folder(root.path())?;
|
||||
assert_eq!(index.search("large.rs", 1)?[0].filename, "large.rs");
|
||||
let options = SearchOptions {
|
||||
code_only: true,
|
||||
..Default::default()
|
||||
};
|
||||
let results = index.search_with_options("refreshAccessToken", 2, &options)?;
|
||||
assert_eq!(results.len(), 2);
|
||||
assert!(results
|
||||
.iter()
|
||||
.all(|r| r.filename == "last.go" || r.filename == "Makefile"));
|
||||
assert!(results
|
||||
.iter()
|
||||
.all(|r| r.source.is_some() && r.snippet.contains("refreshAccessToken")));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overlapping_source_passages_preserve_routine_and_actual_lines() -> Result<()> {
|
||||
let root = tempfile::tempdir()?;
|
||||
let file = root.path().join("module.swift");
|
||||
let mut lines = vec!["// filler\n"; 166];
|
||||
lines[116] = "func importantRoutine() {\n";
|
||||
lines[136] = " return finalValue\n";
|
||||
lines[137] = "}\n";
|
||||
fs::write(&file, lines.concat())?;
|
||||
let mut index = SearchIndex::open(":memory:")?;
|
||||
index.index_folder(root.path())?;
|
||||
let path = fs::canonicalize(file)?.to_string_lossy().into_owned();
|
||||
let passage = index
|
||||
.source_passage(
|
||||
&path,
|
||||
&SourceLocation {
|
||||
kind: "line".into(),
|
||||
number: 97,
|
||||
},
|
||||
8192,
|
||||
)?
|
||||
.unwrap();
|
||||
assert_eq!(passage.text, lines[96..].concat());
|
||||
assert!(passage.text.contains("importantRoutine") && passage.text.contains("finalValue"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn old_code_index_reextracts_unchanged_source_on_upgrade() -> Result<()> {
|
||||
let root = tempfile::tempdir()?;
|
||||
let files = root.path().join("files");
|
||||
fs::create_dir(&files)?;
|
||||
fs::write(files.join("module.java"), "class Evidence {}")?;
|
||||
fs::write(files.join("notes.txt"), "ordinary evidence")?;
|
||||
let db = root.path().join("index.db");
|
||||
let mut index = SearchIndex::open(&db)?;
|
||||
index.index_folder(&files)?;
|
||||
drop(index);
|
||||
let connection = rusqlite::Connection::open(&db)?;
|
||||
connection.execute_batch("PRAGMA user_version=10;")?;
|
||||
drop(connection);
|
||||
let mut index = SearchIndex::open(&db)?;
|
||||
assert_eq!(index.search("module.java", 1)?[0].filename, "module.java");
|
||||
let report = index.index_folder(&files)?;
|
||||
assert_eq!(report.indexed, 1);
|
||||
assert_eq!(report.unchanged, 1);
|
||||
assert!(index
|
||||
.search("Evidence", 10)?
|
||||
.iter()
|
||||
.any(|r| r.filename == "module.java" && r.source.is_some()));
|
||||
Ok(())
|
||||
}
|
||||
@@ -197,11 +197,50 @@ public:
|
||||
require(!text.empty() && text.size() <= max_passage_bytes, "Passage exceeds 1 MiB or is empty");
|
||||
auto tokens = tokenize(text,false);
|
||||
require(!tokens.empty(), "Passage has no tokens");
|
||||
// Preserve actual source offsets, rather than inventing locations from
|
||||
// a lossy decoded representation. Unexpected normalization is an error.
|
||||
require(decode(tokens,tokens.size()) == text, "Tokenizer roundtrip changed source text; cannot verify source offsets");
|
||||
size_t prompt = tokenize(document_prefix(title),true).size();
|
||||
require(prompt + 132 < size_t(context_tokens), "Document prefix exceeds token budget");
|
||||
// SentencePiece may normalize source characters. In that case decoded
|
||||
// token lengths are not source offsets: split the original UTF-8 bytes
|
||||
// and verify each complete prefixed fragment against the real tokenizer.
|
||||
if (decode(tokens,tokens.size()) != text) {
|
||||
const auto prefix = document_prefix(title);
|
||||
json result = json::array();
|
||||
if (prefix.size()+text.size() <= max_text_bytes && tokenize(prefix+text,true).size() <= size_t(context_tokens)) {
|
||||
return json::array({{{"text",text},{"start_byte",0},{"end_byte",text.size()}}});
|
||||
}
|
||||
std::vector<size_t> boundaries;
|
||||
for (size_t i=0;i<text.size();++i) if ((uint8_t(text[i]) & 0xc0) != 0x80) boundaries.push_back(i);
|
||||
boundaries.push_back(text.size());
|
||||
size_t start_index = 0;
|
||||
while (start_index+1 < boundaries.size()) {
|
||||
const size_t start = boundaries[start_index];
|
||||
require(result.size() < max_windows, "Passage exceeds 256 windows");
|
||||
size_t low = start_index;
|
||||
size_t high = size_t(std::upper_bound(boundaries.begin(),boundaries.end(),std::min(text.size(),start+max_text_bytes-prefix.size()))-boundaries.begin())-1;
|
||||
size_t best_index = start_index;
|
||||
while (low < high) {
|
||||
size_t candidate_index = low + (high-low+1)/2;
|
||||
size_t candidate = boundaries[candidate_index];
|
||||
if (tokenize(prefix+text.substr(start,candidate-start),true).size() <= size_t(context_tokens)-4) {
|
||||
best_index = candidate_index;
|
||||
low = candidate_index;
|
||||
} else {
|
||||
high = candidate_index-1;
|
||||
}
|
||||
}
|
||||
size_t best = boundaries[best_index];
|
||||
require(best > start, "Cannot fit source fragment in model context");
|
||||
auto fragment = text.substr(start,best-start);
|
||||
require(tokenize(prefix+fragment,true).size() <= size_t(context_tokens), "Source fragment exceeds context budget");
|
||||
result.push_back({{"text",fragment},{"start_byte",start},{"end_byte",best}});
|
||||
if (best == text.size()) break;
|
||||
// Bounded original-byte overlap; always advance, even for
|
||||
// unusual Unicode/token expansion. No decoded text is indexed.
|
||||
size_t next = best - std::min(size_t(128),(best-start)/2);
|
||||
start_index = size_t(std::lower_bound(boundaries.begin(),boundaries.end(),next)-boundaries.begin());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
size_t budget = size_t(context_tokens) - prompt - 4;
|
||||
size_t stride = budget - 128;
|
||||
json result = json::array();
|
||||
@@ -253,10 +292,11 @@ public:
|
||||
return {{"vectors",json::array({embed_bitmap(bitmap.get())})}};
|
||||
}
|
||||
if (op == "windows") return {{"windows",windows(request.at("text").get<std::string>(),request.value("title",std::string()))}};
|
||||
if (op == "query") {
|
||||
if (op == "query" || op == "code_query") {
|
||||
bool debug = request.value("debug_performance",false);
|
||||
json timings;
|
||||
auto vector = embed("task: search result | query: "+request.at("text").get<std::string>(),512,debug ? &timings : nullptr);
|
||||
auto prefix = op == "code_query" ? "task: code retrieval | query: " : "task: search result | query: ";
|
||||
auto vector = embed(prefix+request.at("text").get<std::string>(),512,debug ? &timings : nullptr);
|
||||
json response = {{"vectors",json::array({vector})}};
|
||||
if (debug) response["performance"] = timings;
|
||||
return response;
|
||||
@@ -289,7 +329,7 @@ int main(int argc, char ** argv) {
|
||||
bool force_cpu = std::getenv("SEARCHI_FORCE_CPU") && std::string(std::getenv("SEARCHI_FORCE_CPU")) == "1";
|
||||
Encoder encoder(path,threads,device == "auto" && !force_cpu);
|
||||
std::cout << json({{"ready",true},{"model","google/embeddinggemma-2"},{"dimensions",dimensions},
|
||||
{"device",encoder.device},{"threads",threads},{"runtime",SEARCHI_LLAMA_TAG},{"media_supported",true}}).dump() << '\n' << std::flush;
|
||||
{"device",encoder.device},{"threads",threads},{"runtime",SEARCHI_LLAMA_TAG},{"media_supported",true},{"code_query_supported",true}}).dump() << '\n' << std::flush;
|
||||
for (;;) {
|
||||
std::string line; char c;
|
||||
while (std::cin.get(c) && c != '\n') {
|
||||
|
||||
@@ -48,6 +48,13 @@ pub struct WorkerInfo {
|
||||
pub runtime: String,
|
||||
#[serde(default)]
|
||||
pub media_supported: bool,
|
||||
#[serde(default)]
|
||||
pub code_query_supported: bool,
|
||||
}
|
||||
|
||||
fn query_operation(info: &WorkerInfo, code: bool) -> Result<&'static str> {
|
||||
ensure!(!code || info.code_query_supported,"Installed embedding worker does not support code retrieval; repair/rebuild the native runtime");
|
||||
Ok(if code { "code_query" } else { "query" })
|
||||
}
|
||||
|
||||
/// Persistent isolated native worker. No HTTP or remote model fallback.
|
||||
@@ -317,15 +324,24 @@ impl GemmaEmbedder {
|
||||
|
||||
pub fn embed_query(&mut self, query: &str) -> Result<Vec<f32>> {
|
||||
self.query_profile = None;
|
||||
self.embed_query_inner(query, false)
|
||||
self.embed_query_inner(query, false, false)
|
||||
}
|
||||
|
||||
/// Opt-in diagnostics contain only durations and counts, never query text.
|
||||
pub fn embed_query_debug(&mut self, query: &str) -> Result<Vec<f32>> {
|
||||
self.query_profile = Some(BTreeMap::new());
|
||||
self.embed_query_inner(query, true)
|
||||
self.embed_query_inner(query, true, false)
|
||||
}
|
||||
|
||||
/// Google code retrieval task; unavailable workers fail explicitly.
|
||||
pub fn embed_code_query(&mut self, query: &str) -> Result<Vec<f32>> {
|
||||
self.query_profile = None;
|
||||
self.embed_query_inner(query, false, true)
|
||||
}
|
||||
pub fn embed_code_query_debug(&mut self, query: &str) -> Result<Vec<f32>> {
|
||||
self.query_profile = Some(BTreeMap::new());
|
||||
self.embed_query_inner(query, true, true)
|
||||
}
|
||||
pub fn query_diagnostics(&self) -> BTreeMap<String, u64> {
|
||||
self.query_profile.clone().unwrap_or_default()
|
||||
}
|
||||
@@ -337,13 +353,13 @@ impl GemmaEmbedder {
|
||||
}
|
||||
}
|
||||
|
||||
fn embed_query_inner(&mut self, query: &str, debug: bool) -> Result<Vec<f32>> {
|
||||
fn embed_query_inner(&mut self, query: &str, debug: bool, code: bool) -> Result<Vec<f32>> {
|
||||
ensure!(
|
||||
!query.trim().is_empty() && query.len() <= 4096,
|
||||
"Query must be nonempty and at most 4096 UTF-8 bytes"
|
||||
);
|
||||
let response =
|
||||
self.request(json!({"op":"query","text":query,"debug_performance":debug}))?;
|
||||
let op = query_operation(&self.info, code)?;
|
||||
let response = self.request(json!({"op":op,"text":query,"debug_performance":debug}))?;
|
||||
if debug {
|
||||
if let Some(profile) = &mut self.query_profile {
|
||||
for name in [
|
||||
@@ -655,6 +671,12 @@ mod tests {
|
||||
let mut handshake = json!({"ready":true,"model":"google/embeddinggemma-2","dimensions":768,"device":"cpu","threads":1,"runtime":"b11461-no-logits-v1"});
|
||||
let old: WorkerInfo = serde_json::from_value(handshake.clone()).unwrap();
|
||||
assert!(!old.media_supported);
|
||||
assert!(!old.code_query_supported);
|
||||
assert!(query_operation(&old, true).is_err());
|
||||
assert_eq!(query_operation(&old, false).unwrap(), "query");
|
||||
let mut new = old.clone();
|
||||
new.code_query_supported = true;
|
||||
assert_eq!(query_operation(&new, true).unwrap(), "code_query");
|
||||
handshake["media_supported"] = json!(true);
|
||||
assert!(
|
||||
serde_json::from_value::<WorkerInfo>(handshake)
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
use anyhow::Result;
|
||||
use searchi_semantic::{Device, GemmaEmbedder};
|
||||
|
||||
#[test]
|
||||
#[ignore = "Requires explicitly provisioned CPU Google tokenizer and worker"]
|
||||
fn normalized_code_tokenization_preserves_original_source_windows() -> Result<()> {
|
||||
let models = std::env::var_os("SEARCHI_GEMMA_MODEL_DIR").expect("Set model directory");
|
||||
let worker = std::env::var_os("SEARCHI_GEMMA_WORKER").expect("Set CPU worker");
|
||||
let mut embedder = GemmaEmbedder::load_with_device(models, worker, 1, Device::Cpu)?;
|
||||
// Genuine frozen TypeScript fixture that does not roundtrip through the
|
||||
// tokenizer. Do not replace source bytes with normalized decoded tokens.
|
||||
let code = "export function process(rows: {category: string; amount: number}[]) {\n const totals = new Map<string, number>();\n for (const row of rows) totals.set(row.category, (totals.get(row.category) ?? 0) + row.amount);\n return totals;\n}\n";
|
||||
for text in [
|
||||
code.to_owned(),
|
||||
format!("{code}// Grüße 日本語 🦀\n").repeat(160),
|
||||
] {
|
||||
let windows = embedder.split_windows(&text, "c.ts")?;
|
||||
let mut covered = 0;
|
||||
for window in &windows {
|
||||
assert!(window.start_byte <= covered);
|
||||
assert!(window.end_byte > covered);
|
||||
assert_eq!(window.text, text[window.start_byte..window.end_byte]);
|
||||
covered = window.end_byte;
|
||||
}
|
||||
assert_eq!(covered, text.len());
|
||||
assert!(windows.len() <= 256);
|
||||
if text.len() > 1000 {
|
||||
assert!(windows.len() > 1);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "Requires explicitly provisioned CPU Google model and rebuilt code-capable worker"]
|
||||
fn actual_code_task_differs_from_search_and_debug_preserves_vectors() -> Result<()> {
|
||||
let models = std::env::var_os("SEARCHI_GEMMA_MODEL_DIR").expect("Set model directory");
|
||||
let worker = std::env::var_os("SEARCHI_GEMMA_WORKER").expect("Set CPU worker");
|
||||
let mut embedder = GemmaEmbedder::load_with_device(models, worker, 1, Device::Cpu)?;
|
||||
assert!(embedder.info.code_query_supported);
|
||||
assert_eq!(embedder.info.device, "cpu");
|
||||
let query = "parse a JSON response and retry a failed network request";
|
||||
let ordinary = embedder.embed_query(query)?;
|
||||
let code = embedder.embed_code_query(query)?;
|
||||
let cosine = |a: &[f32], b: &[f32]| {
|
||||
a.iter()
|
||||
.zip(b)
|
||||
.map(|(a, b)| f64::from(*a) * f64::from(*b))
|
||||
.sum::<f64>()
|
||||
};
|
||||
assert!(
|
||||
cosine(&ordinary, &code) < 0.999999,
|
||||
"Task-specific prompt must not silently reuse generic search embeddings"
|
||||
);
|
||||
let debug = embedder.embed_code_query_debug(query)?;
|
||||
assert!(cosine(&code, &debug) > 0.999999);
|
||||
assert!(embedder
|
||||
.query_diagnostics()
|
||||
.contains_key("worker_token_count"));
|
||||
let ordinary_again = embedder.embed_query(query)?;
|
||||
assert!(
|
||||
cosine(&ordinary, &ordinary_again) > 0.999999,
|
||||
"Previous code query must not contaminate a retained encoder context"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -48,6 +48,9 @@ impl EmbeddingProvider for CountingProvider {
|
||||
}
|
||||
Ok(vector)
|
||||
}
|
||||
fn embed_code_query(&mut self, query: &str) -> Result<Vec<f32>> {
|
||||
self.embed_query(&format!("code-task:{query}"))
|
||||
}
|
||||
fn query_diagnostics(&self) -> BTreeMap<String, u64> {
|
||||
BTreeMap::from([("worker_inference_us".into(), 123)])
|
||||
}
|
||||
@@ -63,6 +66,37 @@ fn count(calls: &Calls) -> usize {
|
||||
calls.queries.lock().unwrap().len()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query_task_scopes_cache_and_code_debug_never_replays_old_worker_counters() -> Result<()> {
|
||||
let (_directory, mut service, calls) = fixture()?;
|
||||
let normal = SearchOptions::default();
|
||||
let code = SearchOptions {
|
||||
code_only: true,
|
||||
..Default::default()
|
||||
};
|
||||
service.search("parse JSON", 5, &normal)?;
|
||||
service.search("parse JSON", 5, &code)?;
|
||||
assert_eq!(
|
||||
*calls.queries.lock().unwrap(),
|
||||
vec!["parse JSON", "code-task:parse JSON"]
|
||||
);
|
||||
let cached = service.search_measured("parse JSON", 5, &code, true)?;
|
||||
assert_eq!(count(&calls), 2);
|
||||
let embedding = cached
|
||||
.metrics
|
||||
.trace
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.find(|s| s.name == "semantic.query_embedding")
|
||||
.unwrap();
|
||||
assert_eq!(embedding.counters["code_task"], 1);
|
||||
assert_eq!(embedding.counters["query_cache_hit"], 1);
|
||||
assert!(!embedding.counters.contains_key("worker_inference_us"));
|
||||
service.search("parse JSON", 5, &normal)?;
|
||||
assert_eq!(count(&calls), 2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_trimmed_queries_reuse_vectors_but_case_and_model_fingerprint_do_not() -> Result<()> {
|
||||
let (_directory, mut service, calls) = fixture()?;
|
||||
|
||||
Reference in new issue
Block a user