How does a search system index 10B+ messages with near-real-time indexing?
How does a search system index 10B+ messages with <5s indexing latency from send to searchable, and return full-text results in <100ms across the entire message corpus?
What the system must do · full-text search with permission enforcement and near-real-time indexing
Quality constraints for search at 10B+ document scale
| Property | Target | Why It Matters / Design Impact |
|---|---|---|
| Indexing latency | <5s from message send to searchable | Users expect to find a message they just sent. Longer lag causes confusion and re-sends. |
| Query latency | P99 <100ms for full-text search | Search must feel instant. Slow search drives users to scroll manually instead. |
| Throughput | 200K+ msgs/sec indexed, 10K+ queries/sec served | Must keep up with message ingestion rate without falling behind. |
| Corpus size | 10B+ documents indexed | Full message history must be searchable · not just recent messages. |
| Availability | 99.9% for search (degraded = stale results, not outage) | Search unavailability is tolerable briefly if results are stale rather than missing entirely. |
| Consistency | Eventually consistent (~5s lag acceptable) | Near-real-time indexing is sufficient · strong consistency would kill throughput. |
| Security | Permission-aware | Never show messages from channels user can't access. Filter at query time, not index time. |
Deriving cluster sizing from message volume and query patterns
| Step | Calculation | Result |
|---|---|---|
| 1 | 10B msgs/day · 86400 | ~115K msgs/sec to index |
| 2 | Avg message 500 bytes → 115K · 500B | ~57MB/sec ingestion |
| 3 | ES cluster sizing: 10B docs · 1KB indexed | ~10TB index storage |
| 4 | Shards: 10TB · 50GB/shard | ~200 shards across cluster |
| 5 | Query load: 10M users · 2 searches/day · 86400 | ~230 queries/sec avg |
| 6 | Permission filter: avg user in 50 channels | Terms filter with 50 channel_ids per query |
Kafka consumer indexes messages into Elasticsearch sharded by workspace_id · permission filtering at query time
Balancing indexing speed, query latency, and permission correctness
| Decision | Choice | Why | Alternative Rejected |
|---|---|---|---|
| Search Engine | Elasticsearch | Purpose-built for full-text search. BM25 scoring, highlighting, aggregations. Proven at 10B+ doc scale. | MySQL FULLTEXT: doesn't scale. Solr: operationally heavier. Meilisearch: not proven at this scale. |
| Indexing Pipeline | Kafka consumer → ES bulk API | Decouples indexing from message delivery. Batch 500 docs per bulk request for throughput. Consumer lag = indexing delay, not message delay. | Synchronous index on write: adds latency to message send path. CDC from DB: more complex, same result. |
| Shard Strategy | Route by workspace_id | Queries never cross workspace boundaries. Data isolation for enterprise. Hot/warm tiering per workspace. | Shard by channel_id: cross-channel search requires scatter-gather across all shards. |
| Permission Model | Query-time filter (channel_id IN user's channels) | Channel membership changes instantly reflected. No re-indexing needed when user joins/leaves channel. | Index-time ACL: requires re-indexing all messages when membership changes · too expensive. |
| Ranking | BM25 + recency decay | Text relevance as primary signal, with exponential decay favoring recent messages. Tunable per workspace. | Pure recency: ignores relevance. Pure BM25: old messages dominate for common terms. |
Search must be eventually consistent with the source of truth (Vitess) and handle permission changes correctly
| # | Failure / Edge Case | What Breaks | How It's Handled |
|---|---|---|---|
| 1 | Indexer consumer lag spike | Messages not searchable for minutes instead of seconds | Auto-scale indexer consumers. Alert on lag >10s. Kafka retains messages · no data loss, just delay. |
| 2 | User removed from channel | Should no longer see messages from that channel in search | Query-time permission filter uses live channel membership. Removal is instant · no re-indexing needed. |
| 3 | Message edited or deleted | Search returns stale content or deleted messages | Edit/delete events flow through same Kafka topic → indexer updates/deletes ES document. Eventual consistency (~5s). |
| 4 | ES cluster node failure | Queries to affected shards fail | Each shard has 1 primary + 2 replicas. Replica promoted automatically. Query routes to healthy replica. |
| 5 | Hot workspace (10M+ messages) | Single shard too large, queries slow | Split into time-based indices (messages-W123-2025-01). Query across recent indices first, paginate to older. |
Each component chosen for a specific role in the search pipeline
| Component | Technology | Why This | Why Not X |
|---|---|---|---|
| Search Engine | Elasticsearch (Lucene) | BM25 scoring, inverted index, highlighting, aggregations. Proven at 10B+ docs. Near-real-time refresh. | Solr: same engine, harder ops. Typesense/Meilisearch: not proven at this scale. |
| Indexing Pipeline | Kafka → Custom Consumer | Decoupled from write path. Batch bulk indexing (500 docs/request). Replay from offset on failure. | Logstash: less control over batching/error handling. Direct DB polling: higher latency, more load on Vitess. |
| Permission Cache | Redis (user → channel set) | Sub-ms lookup of user's accessible channels. TTL 60s, refreshed on membership change event. | Query Vitess on every search: adds 5-10ms per query. Embed in ES doc: re-index on membership change. |
| Source of Truth | Vitess/MySQL | Durable message store. ES index can be rebuilt from Vitess at any time. Single source of truth. | ES as primary: not designed for transactional writes, no strong consistency guarantees. |
| Query Routing | Custom Search API (Go) | Builds ES query with permission filter, handles pagination, caches frequent queries. | Direct ES access from client: exposes internal schema, no permission enforcement. |
The 5 things an interviewer wants to hear about chat search at scale
terms filter in ES query. Channel membership changes are instantly reflected · no re-indexing. This is the key insight: permissions at query time, not index time.