refactor(ai): remove semantic embedding cache + Qdrant vector store

Hapus seluruh fitur embedding/Qdrant (tidak dipakai lagi):

- gateway: drop embeddingClient.ts, qdrantClient.ts, archiveEmbedder.ts
  dan tes qdrantEnsure.test.ts; moderationOrchestrator kembali ke
  exact-hash cache -> LLM (tanpa phase-2 semantic lookup); textCacheStore
  kehilangan findSimilarTextModeration / parseQdrantVerdict /
  isSemanticBandAccepted / upsertBareKeyToQdrant; cache-prune hanya
  menyapu Postgres.
- backend: drop embed.ts + qdrant.ts, endpoint messages.semanticSearch
  dan schema/type terkait; kolom embedding dilepas dari schema
  text_analysis_cache.
- frontend: hapus toggle EXACT/SEMANTIC, hook useSemanticSearch,
  API client + tipe SemanticSearchResult.
- config: buang AI_LLM_EMBEDDING_* dan QDRANT_* (env + .env.example).
- docs: ARCHITECTURE.md / AGENTS.md / README.md / diagram arsitektur
  disesuaikan (LLM caller - vision, cache = exact-hash saja).

Verifikasi: tsc 0 (backend, gateway, frontend); bun test 135 pass +
37 pass, 0 fail; biome 0 error.
This commit is contained in:
mytheclipsebotreview
2026-09-25 01:38:51 +07:00
parent fdc7f01c26
commit 2b6ec59286
30 changed files with 41 additions and 1987 deletions
@@ -11,7 +11,6 @@ import {
Paperclip,
Search,
ShieldAlert,
Sparkles,
} from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { ActivityHeatmap } from "@/components/ActivityHeatmap";
@@ -42,7 +41,6 @@ import {
useMessagesWsSync,
useRecentEdits,
useReviewWsSync,
useSemanticSearch,
} from "@/hooks";
import { useStaggerReveal } from "@/hooks/use-gsap-animation";
import { aiTone } from "@/lib/ai-status";
@@ -85,9 +83,6 @@ export function MessagesView({
const [channelId, setChannelId] = useState<string | null>(null);
const [selected, setSelected] = useState<string | null>(null);
const [query, setQuery] = useState("");
// Search mode: "exact" (substring match over captured messages) or
// "semantic" (vector similarity over the persistent Qdrant archive).
const [semanticMode, setSemanticMode] = useState(false);
// feed | timeline: "timeline" groups messages into date-grouped cards.
const [viewMode, setViewMode] = useState<"feed" | "timeline">("feed");
// Guard against loading the entire history on a long scroll: cap how many
@@ -123,15 +118,7 @@ export function MessagesView({
const loadMore = useLoadMore();
useMessagesWsSync(ws, guildId ?? "");
useReviewWsSync(ws);
const search = useMessageSearch(
query,
query.trim().length >= 2 && !semanticMode,
);
const semantic = useSemanticSearch(
query,
query.trim().length >= 2 && semanticMode,
guildId,
);
const search = useMessageSearch(query, query.trim().length >= 2);
const activity = useMessageActivity(30);
const edits = useRecentEdits(50, undefined, initialEdits);
const detail = useMessageDetail(selected);
@@ -161,8 +148,7 @@ export function MessagesView({
ambient.set(query ? "amber" : "signal", 0.3, query ? "search" : "messages");
}, [query, ambient]);
const searching = query.trim().length >= 2 && !semanticMode;
const semanticSearching = query.trim().length >= 2 && semanticMode;
const searching = query.trim().length >= 2;
const list = searching ? (search.data ?? []) : (messages ?? []);
// Discord-style order: oldest at the top, newest at the bottom. The backend
// returns DESC (newest first); reverse so the feed reads top→bottom like DC.
@@ -283,32 +269,6 @@ export function MessagesView({
onChange={(e) => setQuery(e.target.value)}
/>
</div>
<div className="flex items-center gap-1.5 rounded-[6px] border border-hairline bg-surface-2 p-0.5">
<button
type="button"
onClick={() => setSemanticMode(false)}
className={`rounded-[4px] px-2.5 py-1 font-mono text-[10px] font-medium transition-all ${
!semanticMode
? "bg-surface text-ink border border-hairline-focus shadow-xs"
: "text-ink-muted hover:text-ink"
}`}
>
EXACT
</button>
<button
type="button"
onClick={() => setSemanticMode(true)}
className={`flex items-center gap-1 rounded-[4px] px-2.5 py-1 font-mono text-[10px] font-medium transition-all ${
semanticMode
? "bg-signal/20 text-signal border border-signal/40 shadow-xs"
: "text-ink-muted hover:text-ink"
}`}
>
<Sparkles className="size-3" />
SEMANTIC
</button>
</div>
<div className="flex items-center gap-1.5 rounded-[6px] border border-hairline bg-surface-2 p-0.5">
<button
type="button"
@@ -336,76 +296,6 @@ export function MessagesView({
</GlassPanel>
<div className="grid gap-3 lg:grid-cols-5">
{semanticSearching && (
<GlassPanel className="lg:col-span-5">
<SectionHeader
eyebrow="semantic archive"
title={`Vector Matches for “${query}”`}
action={
<span className="mono text-xs text-[#8a8f98]">
{semantic.data?.length ?? 0} matches
</span>
}
/>
{semantic.isLoading ? (
<SkeletonRows rows={4} />
) : semantic.data && semantic.data.length > 0 ? (
<div className="max-h-[60vh] space-y-1.5 overflow-y-auto pr-1">
{semantic.data.map((r, i) => (
<div
key={r.message_id ?? i}
className="hud-card animate-stagger flex items-start gap-3 p-3"
style={staggerDelay(i)}
>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="font-mono text-[10px] font-semibold text-signal">
{(r.score * 100).toFixed(0)}% RELEVANCE
</span>
{r.username && (
<span className="font-mono text-[10px] font-medium text-ink">
{r.username}
</span>
)}
{r.thread_name && (
<span className="font-mono text-[10px] text-ink-muted">
▶ {r.thread_name}
</span>
)}
{!r.thread_name && r.channel_name && (
<span className="font-mono text-[10px] text-ink-muted">
#{r.channel_name}
</span>
)}
{!r.thread_name && !r.channel_name && r.channel_id && (
<span className="font-mono text-[10px] text-ink-faint">
#{r.channel_id}
</span>
)}
<span
className="ml-auto font-mono text-[10px] text-ink-muted"
suppressHydrationWarning
>
{formatRelativeTime(r.created_at)}
</span>
</div>
<div className="mt-1 text-xs text-ink-soft leading-relaxed">
{r.content}
</div>
</div>
</div>
))}
</div>
) : (
<EmptyState
icon={<Search className="size-7" />}
title="No semantic matches"
description="Try different phrasing — vector search inspects contextual semantics."
/>
)}
</GlassPanel>
)}
{/* Message Stream Deck */}
<GlassPanel className="lg:col-span-3">
<SectionHeader
-1
View File
@@ -25,7 +25,6 @@ export {
useRecentEdits,
useReview,
useReviewWsSync,
useSemanticSearch,
useTextChannels,
} from "./use-messages";
export {
@@ -8,7 +8,6 @@ import type {
EditHistoryRow,
MessageActivityBucket,
MessageRecord,
SemanticSearchResult,
} from "@/lib/types";
import type { WsHook } from "@/lib/ws-hook";
@@ -210,29 +209,6 @@ export function useMessageSearch(query: string, enabled: boolean) {
);
}
// ── Semantic Search (public archive, Qdrant) ──────
export function useSemanticSearch(
query: string,
enabled: boolean,
guildId?: string | null,
) {
return useSWR<SemanticSearchResult[]>(
enabled && query.trim().length >= 2
? ["semantic-search", query.trim(), guildId ?? ""]
: null,
async () => {
const res = await messagesApi.semanticSearch(
query.trim(),
10,
guildId ?? undefined,
);
return res.results;
},
{ keepPreviousData: true },
);
}
// ── WS sync helpers ──────────────────────────────
/**
-12
View File
@@ -6,7 +6,6 @@ import type {
Guild,
MessageActivityBucket,
MessageRecord,
SemanticSearchResult,
} from "@/lib/types";
export const messagesApi = {
@@ -76,17 +75,6 @@ export const messagesApi = {
results: MessageRecord[];
}>,
// Public semantic search over the persistent message archive (Qdrant).
semanticSearch: (query: string, limit?: number, guildId?: string) =>
orpc.messages.semanticSearch({
query,
limit,
...(guildId ? { guildId } : {}),
}) as unknown as Promise<{
results: SemanticSearchResult[];
nextCursor: null;
}>,
// Public, read-only activity heatmap data (per-hour volume by channel).
getActivity: (days = 30) =>
orpc.messages.activity({ days }) as unknown as Promise<
@@ -166,29 +166,9 @@ export interface AttachmentRecord {
uploaded_at?: number | null;
}
// ── Semantic Search (read-only public archive search) ──────────
export interface SemanticSearchResult {
message_id: string | null;
content: string;
score: number;
created_at: number;
username: string | null;
channel_id: string | null;
guild_id: string | null;
thread_id: string | null;
channel_name: string | null;
thread_name: string | null;
}
export interface MessageActivityBucket {
channelId: string;
channelName: string;
hour: number;
count: number;
}
export interface SemanticSearchResponse {
results: SemanticSearchResult[];
nextCursor: null;
}