import type { SessionStore } from './store.js'; import { tokenize } from './indexer.js'; export interface HistorySearchResult { sessionId: string; messageId: number; role: 'user' | 'assistant'; content: string; score: number; createdAt: number; } export interface HistorySearchConfig { limit: number; minScore: number; } export class SessionSearch { private readonly store: SessionStore; private readonly config: HistorySearchConfig; constructor(store: SessionStore, config: HistorySearchConfig) { this.store = store; this.config = config; } search(query: string, opts?: { limit?: number; sessionId?: string }): HistorySearchResult[] { const queryTokens = new Set(tokenize(query)); if (queryTokens.size === 0) { return []; } const rows = opts?.sessionId ? this.store.getMessagesWithMetadata(opts.sessionId) : this.store.getAllMessagesWithMetadata(); const now = Math.floor(Date.now() / 1000); const results: HistorySearchResult[] = []; for (const row of rows) { const keywords = row.metadata?.keywords ?? []; if (keywords.length === 0) { continue; } const overlapCount = keywords.filter(keyword => queryTokens.has(keyword)).length; if (overlapCount === 0) { continue; } const overlapScore = overlapCount / queryTokens.size; const ageSeconds = Math.max(0, now - row.createdAt); const recencyScore = Math.max(0, 1 - ageSeconds / (30 * 24 * 3600)) * 0.2; const totalScore = overlapScore + recencyScore; if (totalScore < this.config.minScore) { continue; } results.push({ sessionId: row.sessionId, messageId: row.id, role: row.role, content: row.content, score: totalScore, createdAt: row.createdAt, }); } results.sort((a, b) => b.score - a.score || b.createdAt - a.createdAt); return results.slice(0, opts?.limit ?? this.config.limit); } }