Skip to content

Commit b4ac385

Browse files
committed
fix: smarter person search + suppress llama.cpp logs
- context_who uses FTS search to find person notes instead of exact filename matching. Checks People folder, 'person' tag, then fuzzy filename. Works with hyphens, underscores, any vault structure. - find_files_by_prefix added to Store for folder-scoped queries - find_file_by_basename normalizes hyphens/underscores/spaces - llama.cpp stderr logs suppressed via backend.void_logs()
1 parent 35fce33 commit b4ac385

3 files changed

Lines changed: 124 additions & 20 deletions

File tree

src/context.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,60 @@ fn resolve_file(
114114
params.store.find_file_by_basename(file_or_docid)
115115
}
116116

117+
/// Find a person note using engraph's full search pipeline (FTS + semantic + graph).
118+
/// Picks the best match from the People folder, or any note tagged "person".
119+
fn find_person_by_search(
120+
params: &ContextParams,
121+
name: &str,
122+
) -> Result<Option<crate::store::FileRecord>> {
123+
// Use FTS to find candidates (lightweight, no embedder needed).
124+
let fts_results = params.store.fts_search(name, 20).unwrap_or_default();
125+
126+
let people_folder = params
127+
.profile
128+
.and_then(|p| p.structure.folders.people.as_deref());
129+
let name_normalized = name.to_lowercase().replace(['-', '_'], " ");
130+
131+
// Pass 1: prefer People folder matches.
132+
if let Some(pf) = people_folder {
133+
for result in &fts_results {
134+
if let Some(file) = params.store.get_file_by_id(result.file_id)?
135+
&& file.path.starts_with(pf)
136+
{
137+
return Ok(Some(file));
138+
}
139+
}
140+
}
141+
142+
// Pass 2: any note tagged "person"/"people".
143+
for result in &fts_results {
144+
if let Some(file) = params.store.get_file_by_id(result.file_id)?
145+
&& file.tags.iter().any(|t| t == "person" || t == "people")
146+
{
147+
return Ok(Some(file));
148+
}
149+
}
150+
151+
// Pass 3: filename fuzzy match (handles hyphens, underscores, case).
152+
for result in &fts_results {
153+
if let Some(file) = params.store.get_file_by_id(result.file_id)? {
154+
let basename = file
155+
.path
156+
.rsplit('/')
157+
.next()
158+
.unwrap_or(&file.path)
159+
.trim_end_matches(".md")
160+
.to_lowercase()
161+
.replace(['-', '_'], " ");
162+
if basename == name_normalized {
163+
return Ok(Some(file));
164+
}
165+
}
166+
}
167+
168+
Ok(None)
169+
}
170+
117171
/// Split content into (frontmatter YAML, body) parts.
118172
fn split_frontmatter(content: &str) -> (String, String) {
119173
let trimmed = content.trim_start();
@@ -266,9 +320,13 @@ pub fn vault_map(params: &ContextParams) -> Result<VaultMap> {
266320

267321
/// Build a person context bundle: note content, mentions, wikilink connections.
268322
pub fn context_who(params: &ContextParams, name: &str) -> Result<PersonContext> {
323+
// Try to find the person note: exact resolve first, then search People folder.
269324
let (note, person_id) = if let Some(pf) = resolve_file(params, name)? {
270325
let n = context_read(params, &pf.path)?;
271326
(Some(n), Some(pf.id))
327+
} else if let Some(pf) = find_person_by_search(params, name)? {
328+
let n = context_read(params, &pf.path)?;
329+
(Some(n), Some(pf.id))
272330
} else {
273331
(None, None)
274332
};

src/llm.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ pub fn llama_backend() -> Result<&'static LlamaBackend> {
2828
if let Some(b) = BACKEND.get() {
2929
return Ok(b);
3030
}
31-
let backend =
31+
let mut backend =
3232
LlamaBackend::init().map_err(|e| anyhow::anyhow!("initializing llama backend: {e}"))?;
33+
// Suppress llama.cpp's noisy Metal/model loading logs to stderr.
34+
backend.void_logs();
3335
Ok(BACKEND.get_or_init(|| backend))
3436
}
3537

src/store.rs

Lines changed: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1086,24 +1086,13 @@ impl Store {
10861086
Ok(map)
10871087
}
10881088

1089-
/// Find a file by case-insensitive basename match. Returns first match (shortest path).
1090-
pub fn find_file_by_basename(&self, basename: &str) -> Result<Option<FileRecord>> {
1091-
let target = if basename.ends_with(".md") {
1092-
basename.to_string()
1093-
} else {
1094-
format!("{}.md", basename)
1095-
};
1096-
// Try exact path first
1097-
if let Some(f) = self.get_file(&target)? {
1098-
return Ok(Some(f));
1099-
}
1100-
// Basename match via SQL
1089+
/// Find all files whose path matches a LIKE pattern (e.g., "03-Resources/People/%").
1090+
pub fn find_files_by_prefix(&self, pattern: &str) -> Result<Vec<FileRecord>> {
11011091
let mut stmt = self.conn.prepare(
1102-
"SELECT id, path, content_hash, mtime, tags, indexed_at, docid, created_by FROM files
1103-
WHERE lower(path) LIKE '%/' || lower(?1) OR lower(path) = lower(?1)
1104-
ORDER BY length(path) ASC LIMIT 1",
1092+
"SELECT id, path, content_hash, mtime, tags, indexed_at, docid, created_by
1093+
FROM files WHERE path LIKE ?1",
11051094
)?;
1106-
let mut rows = stmt.query_map(params![target], |row| {
1095+
let rows = stmt.query_map(params![pattern], |row| {
11071096
Ok(FileRecord {
11081097
id: row.get(0)?,
11091098
path: row.get(1)?,
@@ -1115,10 +1104,65 @@ impl Store {
11151104
created_by: row.get(7)?,
11161105
})
11171106
})?;
1118-
match rows.next() {
1119-
Some(r) => Ok(Some(r?)),
1120-
None => Ok(None),
1107+
rows.collect::<rusqlite::Result<Vec<_>>>()
1108+
.map_err(|e| anyhow::anyhow!("find_files_by_prefix: {e}"))
1109+
}
1110+
1111+
/// Find a file by case-insensitive basename match. Returns first match (shortest path).
1112+
pub fn find_file_by_basename(&self, basename: &str) -> Result<Option<FileRecord>> {
1113+
let base = if basename.ends_with(".md") {
1114+
basename.to_string()
1115+
} else {
1116+
format!("{basename}.md")
1117+
};
1118+
1119+
// Try exact path first.
1120+
if let Some(f) = self.get_file(&base)? {
1121+
return Ok(Some(f));
1122+
}
1123+
1124+
// Build candidate names: exact, spaces→hyphens, hyphens→spaces, spaces→underscores.
1125+
let normalized = basename.replace(['-', '_'], " ");
1126+
let hyphenated = basename.replace(' ', "-");
1127+
let underscored = basename.replace(' ', "_");
1128+
let mut candidates = vec![base];
1129+
for v in [normalized, hyphenated, underscored] {
1130+
let c = if v.ends_with(".md") {
1131+
v
1132+
} else {
1133+
format!("{v}.md")
1134+
};
1135+
if !candidates.contains(&c) {
1136+
candidates.push(c);
1137+
}
11211138
}
1139+
1140+
// Try each candidate as a case-insensitive basename match.
1141+
for candidate in &candidates {
1142+
let mut stmt = self.conn.prepare(
1143+
"SELECT id, path, content_hash, mtime, tags, indexed_at, docid, created_by
1144+
FROM files
1145+
WHERE lower(path) LIKE '%/' || lower(?1) OR lower(path) = lower(?1)
1146+
ORDER BY length(path) ASC LIMIT 1",
1147+
)?;
1148+
let mut rows = stmt.query_map(params![candidate], |row| {
1149+
Ok(FileRecord {
1150+
id: row.get(0)?,
1151+
path: row.get(1)?,
1152+
content_hash: row.get(2)?,
1153+
mtime: row.get(3)?,
1154+
tags: parse_tags(&row.get::<_, String>(4)?),
1155+
indexed_at: row.get(5)?,
1156+
docid: row.get(6)?,
1157+
created_by: row.get(7)?,
1158+
})
1159+
})?;
1160+
if let Some(row) = rows.next() {
1161+
return Ok(Some(row?));
1162+
}
1163+
}
1164+
1165+
Ok(None)
11221166
}
11231167

11241168
/// Rename a file's path in the store, preserving its row ID (and thus edge integrity).

0 commit comments

Comments
 (0)