8af25def83
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>
40 lines
1.4 KiB
TypeScript
40 lines
1.4 KiB
TypeScript
/** 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');
|