feat(search): Meilisearch message search — indexer + permission-scoped query
New search module: a message index (Meilisearch, MEILI_URL-gated; no-op when unset), a SearchProjector that indexes on message.sent, and a SearchService whose permission fence runs server-side — a query only touches the caller's own scope AND the threads they currently belong to (resolved live from participant rows, so membership changes reflect immediately). Every conversation kind (chat/mail/sms) is a message with a TEXT part, so all are searchable through one index. POST /v1/search + /reindex (backfill). 5 fence tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
/** One indexed message document. `at` is epoch-ms so Meilisearch can sort recency-first. */
|
||||
export interface SearchDoc {
|
||||
id: string; // interactionId
|
||||
scopeId: string;
|
||||
threadId: string;
|
||||
text: string;
|
||||
subject: string | null;
|
||||
source: string | null; // opaque app source tag (e.g. crm-messenger / crm-mail) — drives the surface
|
||||
actorId: string | null;
|
||||
at: number;
|
||||
}
|
||||
|
||||
/** A search hit with a highlighted snippet. */
|
||||
export interface SearchHit {
|
||||
id: string;
|
||||
threadId: string;
|
||||
text: string;
|
||||
subject: string | null;
|
||||
source: string | null;
|
||||
at: number;
|
||||
/** The match with `<em>…</em>` around the query terms (from the engine's highlighter). */
|
||||
snippet: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The message-search seam. IIOS owns egress-style search: a message index + a permission-scoped
|
||||
* query. The concrete engine (Meilisearch) sits behind this; an unconfigured deployment binds a
|
||||
* no-op so search degrades to empty rather than erroring.
|
||||
*/
|
||||
export interface MessageSearchPort {
|
||||
/** True only when a real engine is configured (else index/search are inert). */
|
||||
ready(): boolean;
|
||||
index(docs: SearchDoc[]): Promise<void>;
|
||||
remove(ids: string[]): Promise<void>;
|
||||
/** Search WITHIN the given scope and thread set only (the permission fence — enforced here). */
|
||||
search(scopeId: string, threadIds: string[], query: string, limit: number): Promise<SearchHit[]>;
|
||||
}
|
||||
|
||||
export const MESSAGE_SEARCH_PORT = Symbol('MESSAGE_SEARCH_PORT');
|
||||
Reference in New Issue
Block a user