| 179 | } |
| 180 | |
| 181 | async function benchmarkCrossSession(): Promise<CrossSessionResult[]> { |
| 182 | const { observations } = generateDataset(); |
| 183 | const results: CrossSessionResult[] = []; |
| 184 | |
| 185 | const bm25 = new SearchIndex(); |
| 186 | const kv = mockKV(); |
| 187 | const vector = new VectorIndex(); |
| 188 | const dims = 384; |
| 189 | |
| 190 | for (const obs of observations) { |
| 191 | bm25.add(obs); |
| 192 | const text = [obs.title, obs.narrative, ...obs.concepts].join(" "); |
| 193 | vector.add(obs.id, obs.sessionId, deterministicEmbedding(text, dims)); |
| 194 | await kv.set(`mem:obs:${obs.sessionId}`, obs.id, obs); |
| 195 | } |
| 196 | |
| 197 | const mockEmbed: any = { |
| 198 | name: "deterministic", dimensions: dims, |
| 199 | embed: async (t: string) => deterministicEmbedding(t, dims), |
| 200 | embedBatch: async (ts: string[]) => ts.map(t => deterministicEmbedding(t, dims)), |
| 201 | }; |
| 202 | const hybrid = new HybridSearch(bm25, vector, mockEmbed, kv as never, 0.4, 0.6, 0); |
| 203 | |
| 204 | const crossQueries: Array<{ |
| 205 | query: string; |
| 206 | targetConcepts: string[]; |
| 207 | targetSessionRange: [number, number]; |
| 208 | currentSession: number; |
| 209 | }> = [ |
| 210 | { query: "How did we set up OAuth providers?", targetConcepts: ["oauth", "nextauth"], targetSessionRange: [5, 9], currentSession: 29 }, |
| 211 | { query: "What was the N+1 query fix?", targetConcepts: ["n+1", "eager-loading"], targetSessionRange: [10, 14], currentSession: 28 }, |
| 212 | { query: "PostgreSQL full-text search setup", targetConcepts: ["full-text-search", "tsvector"], targetSessionRange: [10, 14], currentSession: 27 }, |
| 213 | { query: "bcrypt password hashing configuration", targetConcepts: ["bcrypt", "password-hashing"], targetSessionRange: [5, 9], currentSession: 25 }, |
| 214 | { query: "Vitest unit testing setup", targetConcepts: ["vitest", "unit-testing"], targetSessionRange: [20, 24], currentSession: 29 }, |
| 215 | { query: "webhook retry exponential backoff", targetConcepts: ["webhooks", "exponential-backoff"], targetSessionRange: [15, 19], currentSession: 29 }, |
| 216 | { query: "ESLint flat config migration", targetConcepts: ["eslint", "linting"], targetSessionRange: [0, 4], currentSession: 29 }, |
| 217 | { query: "Kubernetes HPA autoscaling configuration", targetConcepts: ["hpa", "autoscaling", "kubernetes"], targetSessionRange: [25, 29], currentSession: 29 }, |
| 218 | { query: "Prisma database seed script", targetConcepts: ["seeding", "faker", "prisma"], targetSessionRange: [10, 14], currentSession: 26 }, |
| 219 | { query: "API cursor-based pagination", targetConcepts: ["cursor-based", "pagination"], targetSessionRange: [15, 19], currentSession: 29 }, |
| 220 | { query: "CSRF protection double-submit cookie", targetConcepts: ["csrf", "cookies"], targetSessionRange: [5, 9], currentSession: 29 }, |
| 221 | { query: "blue-green deployment rollback", targetConcepts: ["blue-green", "rollback", "zero-downtime"], targetSessionRange: [25, 29], currentSession: 29 }, |
| 222 | ]; |
| 223 | |
| 224 | for (const cq of crossQueries) { |
| 225 | const targetObs = observations.filter(o => |
| 226 | o.concepts.some(c => cq.targetConcepts.includes(c)) |
| 227 | ); |
| 228 | const targetIds = new Set(targetObs.map(o => o.id)); |
| 229 | |
| 230 | const start = performance.now(); |
| 231 | const bm25Results = bm25.search(cq.query, 20); |
| 232 | const hybridResults = await hybrid.search(cq.query, 20); |
| 233 | const latency = performance.now() - start; |
| 234 | |
| 235 | const bm25Rank = bm25Results.findIndex(r => targetIds.has(r.obsId)); |
| 236 | const hybridRank = hybridResults.findIndex(r => targetIds.has(r.observation.id)); |
| 237 | |
| 238 | const builtinLines = 200; |