Web search builds one massive index shared by all users. Email search is different: every query is private to one user, and results depend on that user’s labels, read status, and folder structure. Gmail’s search index is not one index — it’s one index per user. Per-user search indexes are the right architecture when search results are private, personalized, and must reflect user-specific metadata.

Why Not a Shared Index#

A shared inverted index for email would store every user’s email together: word “invoice” points to all documents across all users containing “invoice.” Returning results for user A requires filtering by user_id at query time. At 1.5 billion Gmail users, that filter is across billions of documents. Even with user-ID sharding, the cross-user data mixing creates unnecessary coupling and security risk.

Per-user index: word “invoice” in user A’s index points only to user A’s emails. Query is purely within one index. No cross-user filtering needed. Indexes can be independently sized to the user’s mailbox.

Index Sharding#

User indexes are sharded by user ID. User A’s index lives on shard 3, user B’s on shard 7. A search request routes directly to the shard containing that user’s index. No scatter-gather across shards.

Index size varies: a power user with 500,000 emails has a large index; a new user has a tiny one. Shards are assigned to balance total index size, not user count. A single heavy user might share a shard with thousands of light users.

graph TD A[Search query: user A, from:boss invoice] --> B[Route to user A index shard] B --> C[Query user A inverted index: from:boss AND invoice] C --> D[Intersect posting lists] D --> E[Apply user A filters: not in trash, not spam] E --> F[Rank by recency and relevance] F --> G[Return results] style A fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style B fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style C fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style D fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style E fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style F fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style G fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff

Indexing on Delivery#

Email arrives, gets indexed immediately into the user’s shard. Indexing fields: subject, body, sender, recipients, attachment filenames. User-specific fields are stored with each document: labels, read/unread, thread ID. These fields are filterable at query time without joining to another table.

Label changes (user archives a message) update the document in the index, not just the database. The index is the primary serving layer; the database is the backup.

Attachment Text Extraction#

Attachments are indexed separately. PDFs and Word documents are processed through a text extraction pipeline asynchronously. The message is delivered and indexed immediately; attachment content appears in search results within seconds as extraction completes. This prevents large attachments from blocking email delivery.

At Oracle#

Salesforce Email Studio indexed customer campaigns per-account. Each Salesforce account had its own search index over sent emails, templates, and engagement data. Routing all search queries to account-specific index shards kept query latency under 50ms regardless of platform total data volume. Cross-account searches (for internal analytics) ran on a separate aggregated index built from the same source data, updated hourly.

What I’m Learning#

Per-user indexes trade storage overhead for query simplicity. You store some redundant data (every user has their own index structure), but queries are fast, isolation is structural, and there’s no risk of leaking one user’s data into another’s results.

Have you built or operated per-user or per-tenant search indexes, and how did you handle the operational complexity of millions of small indexes?