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:
2026-07-23 13:57:52 +05:30
parent 23c43acf8f
commit 8af25def83
10 changed files with 386 additions and 0 deletions
@@ -0,0 +1,24 @@
import { Module } from '@nestjs/common';
import { OutboxModule } from '../outbox/outbox.module';
import { SearchService } from './search.service';
import { SearchProjector } from './search.projector';
import { SearchController } from './search.controller';
import { MESSAGE_SEARCH_PORT } from './message-search.port';
import { messageSearchFromEnv } from './meili.adapter';
/**
* Message search (Meilisearch). The engine binds from MEILI_URL; unset → a no-op port so search
* returns empty rather than erroring. The projector indexes on `message.sent`; the service enforces
* the permission fence (scope + caller's threads). SessionVerifier + ActorResolver are global.
*/
@Module({
imports: [OutboxModule],
controllers: [SearchController],
providers: [
SearchService,
SearchProjector,
{ provide: MESSAGE_SEARCH_PORT, useFactory: () => messageSearchFromEnv() },
],
exports: [SearchService],
})
export class SearchModule {}