@phren/cli
Version:
Knowledge layer for AI agents - CLI, MCP server, and data layer
51 lines (50 loc) • 1.64 kB
JavaScript
import { STOP_WORDS } from "../utils.js";
import { queryDocRows } from "../shared/index.js";
/**
* Keyword overlap fallback for when FTS5 returns no results.
* Scans all docs (optionally filtered by project/type), scores each by
* how many query terms appear in its content, and returns top matches.
*
* Shared between the MCP search tool and CLI `phren search`.
*/
export function keywordFallbackSearch(db, query, opts) {
let fallbackSql = "SELECT project, filename, type, content, path FROM docs";
const fallbackParams = [];
const clauses = [];
if (opts.project) {
clauses.push("project = ?");
fallbackParams.push(opts.project);
}
if (opts.type) {
clauses.push("type = ?");
fallbackParams.push(opts.type);
}
if (clauses.length)
fallbackSql += " WHERE " + clauses.join(" AND ");
const allRows = queryDocRows(db, fallbackSql, fallbackParams);
if (!allRows)
return null;
const terms = query
.toLowerCase()
.replace(/[^\w\s-]/g, " ")
.split(/\s+/)
.filter(w => w.length > 1 && !STOP_WORDS.has(w));
if (terms.length === 0)
return null;
const scored = allRows
.map((row) => {
const content = row.content.toLowerCase();
let score = 0;
for (const term of terms) {
if (content.includes(term))
score++;
}
return { row, score };
})
.filter(r => r.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, opts.limit);
if (scored.length === 0)
return null;
return scored.map(s => s.row);
}