perf(ai-moderation): naikkan cache hit dgn guard akurasi
- Fase-1 exact-cache lookup: N query serial -> SATU query ANY($1::text[]) - Global reuse utk bare key legacy, HANYA verdict non-actionable (clean/flagless/action=none, conf>=0.85, umur<=72h) — flagged/warn tetap context-scoped - Semantic cache dua-band: clean band 0.92 default, actionable tetap 0.97; di antara band -> LLM (fail-open ke akurasi) - hit_count kini di-increment (bulk UPDATE per batch) -> hit-rate terukur - Cache hasil wikipediaSearch di Redis (6h, hanya hasil non-kosong) - Memoize fetchUrlSafely utk type=text (LRU 30m + in-flight dedupe) - makeImageCacheKey strip query CDN Discord (?ex/is/hm, format/width) -> attachment sama = satu key vision, skip re-download+re-vision Spec: .hermes/plans/2026-08-24-ai-analysis-cache-optimization.md Tests: +33 (cacheGuards, discordImageKeyNormalize, cacheBatchLookup)
This commit is contained in:
1 parent
440ec41da8
commit
1accfd9390
11 files changed
+1297
-112
No files matched your search
@@ -21,6 +21,7 @@
|
||||
import { createChildLogger } from "@/shared/logger/index";
|
||||
import { createAbortControllerWithTimeout } from "@/shared/utils/index";
|
||||
import { config } from "../../shared/config/config.js";
|
||||
import { cacheGet, cacheSet, makeCacheKey } from "./cacheStore.js";
|
||||
|
||||
const log = createChildLogger("wikipedia-client");
|
||||
|
||||
@@ -57,10 +58,19 @@ function stripHtml(snippet: string): string {
|
||||
.trim();
|
||||
}
|
||||
|
||||
/** Redis TTL for cached search results (6h — articles change slowly). */
|
||||
const SEARCH_CACHE_TTL_SECONDS = 6 * 60 * 60;
|
||||
|
||||
/**
|
||||
* Search Wikipedia for a query and return up to MAX_RESULTS structured hits.
|
||||
* Uses the Action API `list=search` (srsearch) which is stable and returns
|
||||
* title + HTML snippet. Graceful: returns [] on any failure.
|
||||
*
|
||||
* Cached in the shared Redis store: the same query recurs across batches
|
||||
* (repeat slang, recurring topics), and an uncached re-search per batch was
|
||||
* pure latency + Wikipedia rate-limit pressure. Only NON-EMPTY results are
|
||||
* cached — an empty result may be a transient limiter/network blip, so it is
|
||||
* retried on a later batch instead of being pinned for 6 hours.
|
||||
*/
|
||||
export async function wikipediaSearch(
|
||||
query: string,
|
||||
@@ -69,6 +79,32 @@ export async function wikipediaSearch(
|
||||
const q = query.trim();
|
||||
if (!q) return [];
|
||||
|
||||
const cacheKey = makeCacheKey("wikisearch", q);
|
||||
const cached = await cacheGet(cacheKey);
|
||||
if (cached) {
|
||||
try {
|
||||
const parsed = JSON.parse(cached) as SearchResult[];
|
||||
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||
log.debug({ query: q }, "Wikipedia search cache HIT");
|
||||
return parsed;
|
||||
}
|
||||
} catch {
|
||||
// Malformed entry — fall through to live fetch.
|
||||
}
|
||||
}
|
||||
|
||||
const mapped = await wikipediaSearchLive(q, timeoutMs);
|
||||
if (mapped.length > 0) {
|
||||
cacheSet(cacheKey, JSON.stringify(mapped), SEARCH_CACHE_TTL_SECONDS);
|
||||
}
|
||||
return mapped;
|
||||
}
|
||||
|
||||
/** Live (uncached) Action API search. Returns [] on any failure. */
|
||||
async function wikipediaSearchLive(
|
||||
q: string,
|
||||
timeoutMs: number,
|
||||
): Promise<SearchResult[]> {
|
||||
const params = new URLSearchParams({
|
||||
action: "query",
|
||||
list: "search",
|
||||
|
||||
Reference in new issue
Block a user