// src/day29_cache.js
// 簡單快取 (Map 存 {key: {data, expire}})
const cache = new Map();
/**
* 生成快取 key
*/
function makeKey({ tenant, ns, query, strategy }) {
return `${tenant}:${ns}:${strategy}:${query}`;
}
/**
* 讀取快取
*/
export function getCache({ tenant, ns, query, strategy }) {
const key = makeKey({ tenant, ns, query, strategy });
const entry = cache.get(key);
if (!entry) return null;
if (Date.now() > entry.expire) {
cache.delete(key);
return null;
}
return entry.data;
}
/**
* 寫入快取
*/
export function setCache({ tenant, ns, query, strategy, data, ttlMs=10*60*1000 }) {
const key = makeKey({ tenant, ns, query, strategy });
cache.set(key, { data, expire: Date.now() + ttlMs });
}
/**
* 清除全部快取
*/
export function clearCache() {
cache.clear();
}
在每個策略進入檢索前加快取判斷:
import { getCache, setCache } from "../../../../../src/day29_cache.js";
export const POST = withAuth(async (req, ctx) => {
const { tenant, ns } = ctx.params;
const { q, highlight = true, strategy = "default" } = await req.json();
if (!q || !q.trim()) return NextResponse.json({ ok:false, error:"q 必填" }, { status:400 });
+ // 先檢查快取
+ const cached = getCache({ tenant, ns, query: q, strategy });
+ if (cached) {
+ return NextResponse.json({ ok:true, strategy, cached:true, ...cached });
+ }
if (strategy === "default") {
...
const out = await answerWithRAGHighlighted({ tenant, ns, query: q, topK: 4 });
+ setCache({ tenant, ns, query: q, strategy, data: out });
return NextResponse.json({ ok:true, strategy, ...out });
}
if (strategy === "section") {
...
+ setCache({ tenant, ns, query: q, strategy, data: { answer, answerHtml: aligned.html, sources, spans } });
return NextResponse.json({ ok:true, strategy:"section", answer, answerHtml:aligned.html, ... });
}
if (strategy === "qrewrite") {
...
+ setCache({ tenant, ns, query: q, strategy, data: { answer, sources: chunks } });
return NextResponse.json({ ok:true, strategy:"qrewrite", answer, sources: chunks });
}
if (strategy === "hybrid") {
...
+ setCache({ tenant, ns, query: q, strategy, data: { answer, sources: chunks } });
return NextResponse.json({ ok:true, strategy:"hybrid", answer, sources: chunks });
}
if (strategy === "rerank") {
...
+ setCache({ tenant, ns, query: q, strategy, data: { answer, sources: chunks } });
return NextResponse.json({ ok:true, strategy:"rerank", answer, sources: chunks });
}
if (strategy === "xlingual") {
...
+ setCache({ tenant, ns, query: q, strategy, data: { answer, sources: chunks } });
return NextResponse.json({ ok:true, strategy:"xlingual", answer, sources: chunks });
}
}, ["viewer","editor","admin"]);
在回答顯示快取提示:
const [cached, setCached] = useState(false);
...
const j = await r.json();
if (!j.ok) throw new Error(j.error);
setAnswer(j.answer);
setAnswerHtml(j.answerHtml || "");
setSources(j.sources || []);
+setCached(j.cached || false);
...
<div className="mt-2 text-sm opacity-70">
{strategy.toUpperCase()} 完成
+ {cached && <span className="ml-2 text-green-600">⚡ 來自快取</span>}
</div>
▶️ 驗收流程
問「退貨要多久?」 → 第一次花 1-2 秒。
再問一次同樣問題 → 秒回,顯示「⚡來自快取」。
過了 10 分鐘 TTL 再問 → 會重新跑檢索。