// Pure lexical (keyword) ranking. Complements semantic search by catching exact // term matches the embedding model might under-weight (names, acronyms, codes). // No I/O, no platform deps — fully unit-testable. /** Lowercase + split on non-word chars, dropping empties. */ function tokenize(s: string): string[] { return s .toLowerCase() .split(/\W+/) .filter((t) => t.length > 0); } /** * Rank `rows` by how often the query's terms occur in each row's text. * * Scoring: sum, over each query term, of the number of times that term appears * as a token in the row's text. Rows scoring zero are dropped. Result is sorted * by score descending (ties keep input order, since Array.sort is stable). */ export function lexicalRank( rows: { id: string; text: string }[], query: string, ): { id: string; score: number }[] { const terms = tokenize(query); if (terms.length === 0) return []; const out: { id: string; score: number }[] = []; for (const row of rows) { const tokens = tokenize(row.text); if (tokens.length === 0) continue; // Count occurrences per token once, then sum the query terms' counts. const counts = new Map(); for (const tok of tokens) counts.set(tok, (counts.get(tok) ?? 0) + 1); let score = 0; for (const term of terms) score += counts.get(term) ?? 0; if (score > 0) out.push({ id: row.id, score }); } out.sort((a, b) => b.score - a.score); return out; }