UNPKG

@adonis-agora/agent

Version:

Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.

91 lines • 3.73 kB
import { matchesFilter } from './filter.js'; /** * An in-memory BM25 lexical retriever — the keyword half of hybrid search. Feed it the SAME * {@link ChunkRecord}s you upsert into the vector store (from `chunkDocuments`) so their chunk ids * line up and {@link import('./hybrid-retriever.js').HybridRetriever} can fuse the two rankings. Full * BM25: idf, term-frequency saturation (`k1`), and document-length normalization (`b`). Honors the * same array-OR metadata {@link matchesFilter} as the vector store. Mirrors the reference exactly. */ export class KeywordRetriever { docs = new Map(); documentFrequency = new Map(); totalLength = 0; k1; b; constructor(options = {}) { this.k1 = options.k1 ?? 1.5; this.b = options.b ?? 0.75; } /** Index (or re-index) records. Re-adding an existing id replaces its posting and updates stats. */ add(records) { for (const record of records) { this.remove(record.id); const tokens = tokenize(record.text); const frequencies = new Map(); for (const token of tokens) { frequencies.set(token, (frequencies.get(token) ?? 0) + 1); } for (const term of frequencies.keys()) { this.documentFrequency.set(term, (this.documentFrequency.get(term) ?? 0) + 1); } this.docs.set(record.id, { record, frequencies, length: tokens.length }); this.totalLength += tokens.length; } } remove(id) { const existing = this.docs.get(id); if (existing === undefined) { return; } for (const term of existing.frequencies.keys()) { const next = (this.documentFrequency.get(term) ?? 1) - 1; if (next <= 0) { this.documentFrequency.delete(term); } else { this.documentFrequency.set(term, next); } } this.totalLength -= existing.length; this.docs.delete(id); } async retrieve(query, options = {}) { const total = this.docs.size; if (total === 0) { return []; } const averageLength = this.totalLength / total; const queryTerms = new Set(tokenize(query)); const scored = []; for (const { record, frequencies, length } of this.docs.values()) { if (options.filter !== undefined && !matchesFilter(record.metadata, options.filter)) { continue; } let score = 0; for (const term of queryTerms) { const termFrequency = frequencies.get(term); if (termFrequency === undefined) { continue; } const df = this.documentFrequency.get(term) ?? 0; const idf = Math.log(1 + (total - df + 0.5) / (df + 0.5)); const denominator = termFrequency + this.k1 * (1 - this.b + (this.b * length) / averageLength); score += idf * ((termFrequency * (this.k1 + 1)) / denominator); } if (score > 0) { scored.push({ id: record.id, text: record.text, score, ...(record.source !== undefined ? { source: record.source } : {}), ...(record.metadata !== undefined ? { metadata: record.metadata } : {}), }); } } return scored.sort((a, b) => b.score - a.score).slice(0, options.topK ?? 5); } } function tokenize(text) { return text.toLowerCase().match(/[a-z0-9]+/g) ?? []; } //# sourceMappingURL=keyword-retriever.js.map