feat(archive): rich metadata in semantic search — username/channel/guild context + guild filter

Archive payload now stores username, channel_id, guild_id, thread_id and the
real message created_at (not embed time). Backend searchArray accepts an
optional guildId and applies a Qdrant payload filter so results can be scoped
to the guild being viewed. API/frontend expose the new fields and the
semantic results card shows who said it, in which channel, and when —
turning bare text blobs into contextual results. Old points fall back to
analyzed_at and omit the new fields gracefully.
This commit is contained in:
asepharyana
2026-09-01 18:56:14 +07:00
parent c704fbf7a5
commit 26a690943d
8 changed files with 84 additions and 8 deletions
@@ -10,6 +10,11 @@ export interface ArchiveHit {
content_hash?: string;
analyzed_at: number;
expires_at: number;
username?: string;
channel_id?: string;
guild_id?: string;
thread_id?: string | null;
created_at?: number;
};
}
@@ -61,6 +66,7 @@ export async function searchArchive(
vector: number[],
limit: number,
scoreThreshold: number,
guildId?: string,
): Promise<ArchiveHit[]> {
if (!config.QDRANT_URL) return [];
try {
@@ -72,6 +78,17 @@ export async function searchArchive(
limit,
score_threshold: scoreThreshold,
with_payload: true,
// Optional scope: only return vectors from a specific guild's archive.
// Old points (embedded before rich metadata) have no guild_id payload —
// the `must` match simply excludes them, which is the correct behavior
// for a guild-scoped search.
...(guildId
? {
filter: {
must: [{ key: "guild_id", match: { value: guildId } }],
},
}
: {}),
},
)) as {
result?: Array<{