Files
flynn/src/session/search.ts
T
2026-02-12 22:47:22 -08:00

74 lines
2.0 KiB
TypeScript

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);
}
}