search: cache results

This commit is contained in:
2026-05-30 17:57:56 -07:00
parent 5b7ed25522
commit 600bc3befc
8 changed files with 148 additions and 11 deletions

View File

@@ -12,6 +12,7 @@ import { getModelCatalogSnapshot } from "./llm/model-catalog.js";
import { openaiClient } from "./llm/providers.js";
import { serializeProviderFields, toPrismaProvider } from "./llm/provider-ids.js";
import { exaClient } from "./search/exa.js";
import { isFreshSearchCacheHit, normalizeSearchQuery } from "./search-cache.js";
import type { ChatAttachment } from "./llm/types.js";
const ProviderSchema = z.enum(["openai", "anthropic", "xai", "hermes-agent"]);
@@ -375,7 +376,7 @@ function serializeChatLike<T extends Record<string, any>>(chat: T) {
}
function serializeSearchLike<T extends Record<string, any>>(search: T) {
const { projectItems: _projectItems, ...rest } = search;
const { projectItems: _projectItems, queryNormalized: _queryNormalized, ...rest } = search;
return {
...rest,
...serializeStarFields(search),
@@ -649,6 +650,7 @@ async function executeSearchRunStream(searchId: string, body: SearchRunRequest,
where: { id: searchId },
data: {
query,
queryNormalized: normalizeSearchQuery(query),
title: normalizedTitle,
requestId: searchResponse?.requestId ?? null,
rawResponse: searchResponse as any,
@@ -686,6 +688,7 @@ async function executeSearchRunStream(searchId: string, body: SearchRunRequest,
where: { id: searchId },
data: {
query,
queryNormalized: normalizeSearchQuery(query),
title: normalizedTitle,
latencyMs: Math.round(performance.now() - startedAt),
error: message,
@@ -877,18 +880,51 @@ export async function registerRoutes(app: FastifyInstance) {
app.post("/v1/searches", async (req) => {
requireAdmin(req);
const Body = z.object({ title: z.string().optional(), query: z.string().optional() });
const Body = z.object({
title: z.string().optional(),
query: z.string().optional(),
reuseByQuery: z.boolean().optional(),
});
const body = Body.parse(req.body ?? {});
const title = body.title?.trim() || body.query?.trim()?.slice(0, 80);
const query = body.query?.trim() || null;
const queryNormalized = normalizeSearchQuery(query);
if (body.reuseByQuery && queryNormalized) {
const existing = await prisma.search.findFirst({
where: { queryNormalized },
orderBy: { updatedAt: "desc" },
select: {
...searchSummarySelect,
answerText: true,
_count: { select: { results: true } },
},
});
if (existing) {
const { _count, answerText: _answerText, ...search } = existing;
return {
search: serializeSearchLike(search),
reused: true,
cacheHit: isFreshSearchCacheHit({
updatedAt: existing.updatedAt,
resultCount: _count.results,
answerText: existing.answerText,
isActive: activeSearchStreams.has(existing.id),
}),
};
}
}
const search = await prisma.search.create({
data: {
title: title || null,
query,
queryNormalized,
},
select: searchSummarySelect,
});
return { search: serializeSearchLike(search) };
return { search: serializeSearchLike(search), reused: false, cacheHit: false };
});
app.patch("/v1/searches/:searchId/star", async (req) => {
@@ -1032,6 +1068,7 @@ export async function registerRoutes(app: FastifyInstance) {
where: { id: searchId },
data: {
query,
queryNormalized: normalizeSearchQuery(query),
title: normalizedTitle,
requestId: searchResponse?.requestId ?? null,
rawResponse: searchResponse as any,