(_req: http.IncomingMessage, res: http.ServerResponse, url: URL)
| 985 | } |
| 986 | |
| 987 | private async serveSearch(_req: http.IncomingMessage, res: http.ServerResponse, url: URL): Promise<void> { |
| 988 | const q = url.searchParams.get("q") ?? ""; |
| 989 | if (!q.trim()) { this.jsonResponse(res, { results: [], query: q }); return; } |
| 990 | |
| 991 | const role = url.searchParams.get("role") ?? undefined; |
| 992 | const session = url.searchParams.get("session") ?? undefined; |
| 993 | const owner = url.searchParams.get("owner") ?? undefined; |
| 994 | const dateFrom = url.searchParams.get("dateFrom") ?? undefined; |
| 995 | const dateTo = url.searchParams.get("dateTo") ?? undefined; |
| 996 | |
| 997 | const passesFilter = (r: any): boolean => { |
| 998 | if (role && r.role !== role) return false; |
| 999 | if (session && r.session_key !== session) return false; |
| 1000 | if (owner && r.owner !== owner) return false; |
| 1001 | if (dateFrom && r.created_at < new Date(dateFrom).getTime()) return false; |
| 1002 | if (dateTo && r.created_at > new Date(dateTo).getTime()) return false; |
| 1003 | return true; |
| 1004 | }; |
| 1005 | |
| 1006 | const ftsFilters: string[] = []; |
| 1007 | const likeFilters: string[] = []; |
| 1008 | const sqlParams: any[] = []; |
| 1009 | if (session) { ftsFilters.push("c.session_key = ?"); likeFilters.push("session_key = ?"); sqlParams.push(session); } |
| 1010 | if (owner) { ftsFilters.push("c.owner = ?"); likeFilters.push("owner = ?"); sqlParams.push(owner); } |
| 1011 | const ftsWhere = ftsFilters.length > 0 ? " AND " + ftsFilters.join(" AND ") : ""; |
| 1012 | const likeWhere = likeFilters.length > 0 ? " AND " + likeFilters.join(" AND ") : ""; |
| 1013 | |
| 1014 | const db = (this.store as any).db; |
| 1015 | let ftsResults: any[] = []; |
| 1016 | try { |
| 1017 | ftsResults = db.prepare( |
| 1018 | `SELECT c.* FROM chunks_fts f JOIN chunks c ON f.rowid = c.rowid WHERE chunks_fts MATCH ?${ftsWhere} ORDER BY rank LIMIT 100`, |
| 1019 | ).all(q, ...sqlParams).filter(passesFilter); |
| 1020 | } catch { /* FTS syntax error, fall through */ } |
| 1021 | if (ftsResults.length === 0) { |
| 1022 | try { |
| 1023 | ftsResults = db.prepare( |
| 1024 | `SELECT * FROM chunks WHERE (content LIKE ? OR summary LIKE ?)${likeWhere} ORDER BY created_at DESC LIMIT 100`, |
| 1025 | ).all(`%${q}%`, `%${q}%`, ...sqlParams).filter(passesFilter); |
| 1026 | } catch (err) { |
| 1027 | this.log.warn(`LIKE search failed: ${err}`); |
| 1028 | } |
| 1029 | } |
| 1030 | |
| 1031 | const SEMANTIC_THRESHOLD = 0.64; |
| 1032 | const VECTOR_TIMEOUT_MS = 8000; |
| 1033 | let vectorResults: any[] = []; |
| 1034 | let scoreMap = new Map<string, number>(); |
| 1035 | try { |
| 1036 | const vecPromise = (async () => { |
| 1037 | const queryVec = await this.embedder.embedQuery(q); |
| 1038 | return vectorSearch(this.store, queryVec, 40); |
| 1039 | })(); |
| 1040 | const hits = await Promise.race([ |
| 1041 | vecPromise, |
| 1042 | new Promise<null>((resolve) => setTimeout(() => resolve(null), VECTOR_TIMEOUT_MS)), |
| 1043 | ]); |
| 1044 | if (hits) { |
no test coverage detected