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:
asepharyana committed 2026-08-24 18:51:23 +07:00
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",