Search Indexing Strategies
Index Aliases
Read aloud is unavailable in this browser.
The Indirection That Changes Everything
Every major search system—from Google's internal infrastructure to Elasticsearch clusters powering Netflix—relies on a deceptively simple concept: index aliases.
An alias is just a name that points to one or more indexes. Instead of querying products_v23 directly, applications query the alias products. When you need to switch to a new index version, you update the alias, not the application code.
This indirection enables capabilities that would otherwise be impossible or extremely risky:
- Zero-downtime deployments: Switch to a new index atomically
- A/B testing: Route portions of traffic to experimental indexes
- Gradual rollouts: Shift traffic incrementally from old to new
- Time-based views: Automatically span indexes for different time periods
- Read/write separation: Direct writes to one index, reads to another
If you're not using aliases, you're making search infrastructure harder than it needs to be. This page will show you how aliases work, how to use them effectively, and the advanced patterns that make production search systems robust and maintainable.
By the end of this page, you will understand how index aliases work internally, master alias management operations, implement versioning schemes, and apply advanced patterns like filtered aliases, routing aliases, and multi-index aliases.
Understanding Index Aliases
At its core, an index alias is a symbolic name that resolves to one or more concrete index names. The search engine maintains a mapping from alias names to index names, and this mapping can be modified atomically.
How Aliases Work
When you query an alias:
- The search engine receives the query targeting the alias
- It looks up the alias in its metadata to find the concrete index(es)
- The query is executed against those concrete indexes
- Results are returned as if you'd queried directly
The key insight: applications never need to know the concrete index names. They work with stable alias names, while operators manage the underlying index mapping.
Alias Properties
Aliases can have associated properties that modify their behavior:
- Filtering: An alias can include a query filter, so queries against it automatically apply that filter
- Routing: An alias can include routing values, directing queries to specific shards
- Write index: When an alias points to multiple indexes, one can be designated as the write target
- Is hidden: Aliases can be hidden from certain API responses
Alias vs. Index Names
| Aspect | Concrete Index | Alias |
|---|---|---|
| Naming | Immutable after creation | Can point to different indexes |
| Storage | Contains actual data | Contains no data, just a pointer |
| Creation | Heavy operation (shards, replicas) | Lightweight metadata update |
| Atomic switch | Cannot swap names | Can atomically move between indexes |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
// Create an index with an alias in one operationPUT /products_v1{ "aliases": { "products": {} }, "mappings": { "properties": { "name": { "type": "text" }, "price": { "type": "float" } } }} // Add alias to existing indexPOST /_aliases{ "actions": [ { "add": { "index": "products_v1", "alias": "products" } } ]} // Query using the alias (identical to querying the index)GET /products/_search{ "query": { "match": { "name": "headphones" } }} // Index a document using the aliasPOST /products/_doc{ "name": "Wireless Headphones", "price": 79.99} // Check what indexes an alias points toGET /_alias/products// Response:{ "products_v1": { "aliases": { "products": {} } }} // Check all aliases for an indexGET /products_v1/_alias// Response:{ "products_v1": { "aliases": { "products": {}, "products_read": {}, "electronics": {} } }}Always use aliases in application code, never concrete index names. This isn't just about future-proofing—it's about operational sanity. When you inevitably need to reindex, migrate, or split indexes, applications won't need any changes.
Atomic Alias Operations
The _aliases API allows multiple alias modifications in a single atomic operation. This is the foundation of zero-downtime index switching.
The Atomic Guarantee
When you submit multiple actions in a single _aliases request:
- All actions succeed together, or all fail together
- No query sees a partial state (e.g., alias pointing to both old and new indexes unintentionally)
- The switch is instantaneous from the client's perspective
This atomicity is crucial. Without it, there would be a moment during switching where queries might hit the old index, the new index, both, or neither—causing inconsistent results or errors.
Common Atomic Operations
Index Version Switch: The most common operation—moving an alias from one index version to another.
Multi-Alias Update: Updating multiple aliases together (e.g., read and write aliases).
Alias Rename: Removing from one name and adding to another.
Filter Update: Changing the filter on a filtered alias.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
// ATOMIC OPERATION 1: Simple version switch// This is the core operation for zero-downtime reindexingPOST /_aliases{ "actions": [ { "remove": { "index": "products_v1", "alias": "products" } }, { "add": { "index": "products_v2", "alias": "products" } } ]}// Both actions happen atomically - no moment where 'products' points to nothing or both // ATOMIC OPERATION 2: Multiple alias update// Switch read alias and update write alias togetherPOST /_aliases{ "actions": [ // Move read alias to new index { "remove": { "index": "products_v1", "alias": "products_read" } }, { "add": { "index": "products_v2", "alias": "products_read" } }, // Move write alias to new index { "remove": { "index": "products_v1", "alias": "products_write" } }, { "add": { "index": "products_v2", "alias": "products_write" } } ]} // ATOMIC OPERATION 3: Wildcard-based switch// Useful when you have multiple indexes to updatePOST /_aliases{ "actions": [ { "remove": { "index": "products_*", "alias": "products" } }, { "add": { "index": "products_v2", "alias": "products" } } ]}// Removes 'products' alias from ALL indexes matching products_* // ATOMIC OPERATION 4: Conditional switch (using filter)// Create distinct aliases for different product categoriesPOST /_aliases{ "actions": [ { "add": { "index": "products_v2", "alias": "electronics", "filter": { "term": { "category": "electronics" } } } }, { "add": { "index": "products_v2", "alias": "clothing", "filter": { "term": { "category": "clothing" } } } } ]} // ATOMIC OPERATION 5: Add to multiple indexes// Alias spanning multiple indexes for time-based dataPOST /_aliases{ "actions": [ { "add": { "index": "logs-2024-01", "alias": "logs-recent" } }, { "add": { "index": "logs-2024-02", "alias": "logs-recent" } }, { "add": { "index": "logs-2024-03", "alias": "logs-recent" } } ]}// Queries to 'logs-recent' now search across all three indexesWhile operations within a single _aliases call are atomic, the order of actions matters in some cases. If you're adding and removing the same alias, ensure the logic is clear. Elasticsearch processes actions in order, but the atomicity means external observers see only the final state.
Index Versioning Schemes
Effective use of aliases requires a consistent index versioning scheme. Several patterns have emerged as best practices.
Numeric Versioning
The simplest scheme: append a version number.
products_v1 → products_v2 → products_v3
Advantages:
- Easy to understand
- Clear upgrade path
- Simple to track in deployment scripts
Disadvantages:
- Version numbers grow indefinitely
- Doesn't encode when the index was created
- Multiple parallel versions can confuse (which is newer: v23 or v24-hotfix?)
Timestamp Versioning
Include creation timestamp in the name.
products_20240115_103045 → products_20240122_142030
Advantages:
- Self-documenting (you know when each was created)
- Natural ordering by time
- No coordination needed for unique names
Disadvantages:
- Longer names
- Harder to reference in automation
- Doesn't indicate relationship between versions
Hybrid Versioning
Combine semantic versioning with timestamps.
products_v3_20240115 → products_v3_20240122 (minor updates)
products_v4_20240201 (major schema change)
Advantages:
- Major version indicates schema compatibility
- Timestamp provides creation context
- Supports both routine updates and major changes
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
/** * Index Version Manager * * Provides structured index naming and lifecycle management. */ interface IndexVersion { baseName: string; // e.g., "products" majorVersion: number; // Schema version minorVersion: number; // Data refresh version timestamp: Date;} class IndexVersionManager { /** * Generate a new index name following the versioning scheme. * * Format: {baseName}_v{major}_{minor}_{YYYYMMDD_HHmmss} */ generateIndexName( baseName: string, majorVersion: number, minorVersion: number ): string { const timestamp = new Date(); const dateStr = timestamp.toISOString() .replace(/[-:T]/g, '') .slice(0, 15); return `${baseName}_v${majorVersion}_${minorVersion}_${dateStr}`; } /** * Parse an index name into its version components. */ parseIndexName(indexName: string): IndexVersion | null { const pattern = /^(.+)_v(d+)_(d+)_(d{8}_d{6})$/; const match = indexName.match(pattern); if (!match) return null; const [, baseName, major, minor, timestamp] = match; return { baseName, majorVersion: parseInt(major), minorVersion: parseInt(minor), timestamp: this.parseTimestamp(timestamp), }; } /** * Get the current production index for an alias. */ async getCurrentIndex(alias: string): Promise<string | null> { const response = await this.esClient.indices.getAlias({ name: alias }); const indexes = Object.keys(response); if (indexes.length === 0) return null; if (indexes.length === 1) return indexes[0]; // Multiple indexes - return the most recent return this.getMostRecent(indexes); } /** * List all historical versions of an index family. */ async listVersions(baseName: string): Promise<IndexVersion[]> { const response = await this.esClient.cat.indices({ index: `${baseName}_v*`, format: 'json', s: 'index:desc', // Sort by name descending (newest first) }); return response .map((idx: { index: string }) => this.parseIndexName(idx.index)) .filter((v: IndexVersion | null): v is IndexVersion => v !== null); } /** * Clean up old index versions, keeping the N most recent. */ async pruneOldVersions( baseName: string, keepCount: number = 3 ): Promise<string[]> { const versions = await this.listVersions(baseName); // Current index pointed by alias should never be deleted const currentIndex = await this.getCurrentIndex(baseName); const toDelete = versions .slice(keepCount) // Skip the most recent N .filter(v => this.versionToIndexName(v) !== currentIndex); const deletedNames: string[] = []; for (const version of toDelete) { const indexName = this.versionToIndexName(version); await this.esClient.indices.delete({ index: indexName }); deletedNames.push(indexName); } return deletedNames; } /** * Atomic version switch with safety checks. */ async switchVersion( alias: string, fromIndex: string, toIndex: string ): Promise<void> { // Verify target index exists and is healthy const health = await this.esClient.cluster.health({ index: toIndex, wait_for_status: 'green', timeout: '30s', }); if (health.status !== 'green') { throw new Error( `Target index ${toIndex} is not healthy: ${health.status}` ); } // Perform atomic switch await this.esClient.indices.updateAliases({ body: { actions: [ { remove: { index: fromIndex, alias } }, { add: { index: toIndex, alias } }, ], }, }); console.log(`Switched alias '${alias}': ${fromIndex} → ${toIndex}`); } private versionToIndexName(version: IndexVersion): string { const dateStr = version.timestamp.toISOString() .replace(/[-:T]/g, '') .slice(0, 15); return `${version.baseName}_v${version.majorVersion}_${version.minorVersion}_${dateStr}`; } private parseTimestamp(ts: string): Date { // Parse "20240115_103045" format const year = ts.slice(0, 4); const month = ts.slice(4, 6); const day = ts.slice(6, 8); const hour = ts.slice(9, 11); const minute = ts.slice(11, 13); const second = ts.slice(13, 15); return new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}Z`); } private getMostRecent(indexes: string[]): string { return indexes .map(name => ({ name, version: this.parseIndexName(name) })) .filter(item => item.version !== null) .sort((a, b) => b.version!.timestamp.getTime() - a.version!.timestamp.getTime() )[0]?.name ?? indexes[0]; }}Filtered Aliases: Virtual Indexes
Filtered aliases are one of the most powerful yet underutilized features in search systems. They allow you to create virtual views of your data without physical duplication.
How Filtered Aliases Work
A filtered alias includes a query filter that is automatically applied to every query against that alias. If you create an alias electronics with a filter {"term": {"category": "electronics"}}, all queries against electronics will only return electronics products.
Use Cases
Multi-Tenancy: Create per-tenant aliases with tenant_id filters. Each tenant's application queries their alias, unable to see other tenants' data.
Data Partitioning: Logical partitions without physical index separation. Hot/cold data, active/archived records.
Access Control: Teams see only data they're authorized for. Finance sees financial data, HR sees employee data.
A/B Testing: Create aliases with different filters for A and B cohorts.
Security Considerations
Filtered aliases provide a level of data isolation, but they're not a complete security boundary:
- Users with direct index access can bypass the filter
- Aggregate queries might leak information about filtered-out documents
- For true multi-tenancy, consider separate indexes and/or external authorization
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
// Multi-tenancy example: Create per-tenant aliases// All tenants share one index, but each sees only their data // Create filtered aliases for each tenantPOST /_aliases{ "actions": [ { "add": { "index": "orders", "alias": "orders_tenant_acme", "filter": { "term": { "tenant_id": "acme-corp" } } } }, { "add": { "index": "orders", "alias": "orders_tenant_globex", "filter": { "term": { "tenant_id": "globex-inc" } } } } ]} // ACME's application queries their alias - sees only their dataGET /orders_tenant_acme/_search{ "query": { "range": { "order_date": { "gte": "2024-01-01" } } }}// The tenant_id filter is automatically applied - impossible to see Globex's orders // Category-based aliases for e-commercePOST /_aliases{ "actions": [ { "add": { "index": "products", "alias": "electronics", "filter": { "term": { "category": "electronics" } } } }, { "add": { "index": "products", "alias": "in_stock_products", "filter": { "range": { "inventory": { "gt": 0 } } } } }, { "add": { "index": "products", "alias": "premium_products", "filter": { "bool": { "filter": [ { "range": { "price": { "gte": 100 } } }, { "range": { "rating": { "gte": 4.0 } } } ] } } } } ]} // Time-based filtering for logsPOST /_aliases{ "actions": [ { "add": { "index": "logs-2024", "alias": "logs-last-30-days", "filter": { "range": { "@timestamp": { "gte": "now-30d/d", "lte": "now" } } } } }, { "add": { "index": "logs-2024", "alias": "error-logs", "filter": { "term": { "level": "ERROR" } } } } ]} // Combining filtered alias with routing for performancePOST /_aliases{ "actions": [ { "add": { "index": "orders", "alias": "orders_region_us", "filter": { "term": { "region": "us" } }, "routing": "us" // Also route to specific shard } } ]}Filtered alias filters are applied as additional query clauses. They benefit from the same optimizations as regular queries—ensure filtered fields are indexed appropriately. For high-cardinality filters (like tenant_id), keyword fields with doc_values work well.
Read/Write Alias Separation
A sophisticated pattern for managing index lifecycles is separating read and write operations through different aliases.
The Pattern
Create two aliases for each logical index:
- Read Alias (
products_read): Points to the index(es) queries should search - Write Alias (
products_write): Points to the index receiving new documents
Why Separate Read and Write?
During Reindexing: Write alias continues pointing to the old index while you build the new one. Reads can switch to the new index before writes do, allowing verification.
Rolling Indexes: For time-series data, the write alias always points to the current time bucket while the read alias spans all relevant buckets.
Blue-Green Deployments: Read from stable "blue" index while writing to experimental "green" index.
Write Index Designation
When an alias points to multiple indexes (common for read aliases spanning time periods), you must designate one as the write index:
{
"add": {
"index": "products_v2",
"alias": "products",
"is_write_index": true
}
}
Only the write index receives new documents; searches go to all indexes behind the alias.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
// Initial setup: Both read and write point to v1POST /_aliases{ "actions": [ { "add": { "index": "products_v1", "alias": "products_read" } }, { "add": { "index": "products_v1", "alias": "products_write" } } ]} // During reindexing: Write still goes to v1, we're building v2// No changes needed - just index to v2 directly during bulk load // After reindex complete: Switch reads to v2, writes still to v1// This allows verifying v2 with real production queriesPOST /_aliases{ "actions": [ { "remove": { "index": "products_v1", "alias": "products_read" } }, { "add": { "index": "products_v2", "alias": "products_read" } } // products_write still points to v1 ]} // After verification: Switch writes to v2 as wellPOST /_aliases{ "actions": [ { "remove": { "index": "products_v1", "alias": "products_write" } }, { "add": { "index": "products_v2", "alias": "products_write" } } ]} // Now both aliases point to v2, can clean up v1 // =========================================================// Time-series example: Rolling monthly indexes// ========================================================= // Setup: Read spans last 3 months, write goes to current monthPOST /_aliases{ "actions": [ // Read alias spans multiple indexes { "add": { "index": "logs-2024-01", "alias": "logs_read" } }, { "add": { "index": "logs-2024-02", "alias": "logs_read" } }, { "add": { "index": "logs-2024-03", "alias": "logs_read" } }, // Write alias points to current month only { "add": { "index": "logs-2024-03", "alias": "logs_write" } } ]} // Monthly rollover: Add new month, remove old, update write targetPOST /_aliases{ "actions": [ // Add April to read alias { "add": { "index": "logs-2024-04", "alias": "logs_read" } }, // Remove January from read alias (rolling window) { "remove": { "index": "logs-2024-01", "alias": "logs_read" } }, // Update write alias to April { "remove": { "index": "logs-2024-03", "alias": "logs_write" } }, { "add": { "index": "logs-2024-04", "alias": "logs_write" } } ]} // =========================================================// Multi-index write index pattern// ========================================================= // When one alias points to multiple indexes, designate write targetPOST /_aliases{ "actions": [ { "add": { "index": "logs-2024-03", "alias": "logs", "is_write_index": false } }, { "add": { "index": "logs-2024-04", "alias": "logs", "is_write_index": true // New documents go here } } ]} // Now writes to 'logs' go to logs-2024-04// Reads from 'logs' search both indexesFor critical systems, use a proxy layer to gradually shift read traffic: start with 1% to new index, monitor, increase to 10%, 50%, then 100%. This catches issues that verification alone might miss. The alias switch becomes the final step after traffic has been fully validated.
Multi-Index Aliases
Aliases can point to multiple indexes simultaneously, enabling powerful patterns for time-series data, data lifecycle management, and horizontal scaling.
Time-Series Data Management
For logs, events, and metrics, data is typically organized by time:
logs-2024-01 → logs-2024-02 → logs-2024-03 → ...
A multi-index alias provides a unified search interface:
logs-recent → [logs-2024-01, logs-2024-02, logs-2024-03]
Queries against logs-recent automatically search all three months.
Data Lifecycle with Index Lifecycle Management (ILM)
Elasticsearch's ILM works seamlessly with aliases:
- Hot phase: New data writes to current index via write alias
- Rollover: When index meets size/age criteria, new index is created and becomes write target
- Warm phase: Older indexes are moved to warm nodes, read alias still includes them
- Cold/Delete phase: Eventually removed from alias and deleted
Cross-Cluster Aliases
With Cross-Cluster Search (CCS), aliases can span clusters:
GET /cluster_a:logs,cluster_b:logs/_search
This enables globally distributed search while maintaining regional data locality.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
// =========================================================// Time-series alias management// ========================================================= // Create rolling 7-day alias for recent dataPOST /_aliases{ "actions": [ { "add": { "index": "metrics-2024.01.08", "alias": "metrics-last-7-days" } }, { "add": { "index": "metrics-2024.01.09", "alias": "metrics-last-7-days" } }, { "add": { "index": "metrics-2024.01.10", "alias": "metrics-last-7-days" } }, { "add": { "index": "metrics-2024.01.11", "alias": "metrics-last-7-days" } }, { "add": { "index": "metrics-2024.01.12", "alias": "metrics-last-7-days" } }, { "add": { "index": "metrics-2024.01.13", "alias": "metrics-last-7-days" } }, { "add": { "index": "metrics-2024.01.14", "alias": "metrics-last-7-days" } } ]} // Daily rollover: Add today, remove 8 days agoPOST /_aliases{ "actions": [ { "add": { "index": "metrics-2024.01.15", "alias": "metrics-last-7-days" } }, { "remove": { "index": "metrics-2024.01.08", "alias": "metrics-last-7-days" } } ]} // =========================================================// Index Lifecycle Management (ILM) with aliases// ========================================================= // Create ILM policyPUT /_ilm/policy/logs_policy{ "policy": { "phases": { "hot": { "min_age": "0ms", "actions": { "rollover": { "max_size": "50gb", "max_age": "1d" } } }, "warm": { "min_age": "7d", "actions": { "shrink": { "number_of_shards": 1 }, "forcemerge": { "max_num_segments": 1 } } }, "cold": { "min_age": "30d", "actions": { "searchable_snapshot": { "snapshot_repository": "my-repo" } } }, "delete": { "min_age": "90d", "actions": { "delete": {} } } } }} // Create index template with ILM and aliasPUT /_index_template/logs_template{ "index_patterns": ["logs-*"], "template": { "settings": { "index.lifecycle.name": "logs_policy", "index.lifecycle.rollover_alias": "logs" } }} // Bootstrap first index with aliasPUT /logs-000001{ "aliases": { "logs": { "is_write_index": true } }} // =========================================================// Tiered aliases for access patterns// ========================================================= // Different aliases for different query patternsPOST /_aliases{ "actions": [ // Real-time dashboards - only hot data { "add": { "index": "events-hot-*", "alias": "events-realtime" } }, // Historical analysis - includes warm data { "add": { "index": "events-hot-*", "alias": "events-analysis" } }, { "add": { "index": "events-warm-*", "alias": "events-analysis" } }, // Compliance - all data including cold { "add": { "index": "events-*", "alias": "events-compliance" } } ]}Queries against aliases spanning many indexes have overhead: query coordination, result merging, and memory for aggregations across all shards. For read-heavy workloads spanning hundreds of indexes, consider time-range pre-filtering or dedicated summary indexes.
Index Alias Best Practices
Years of production experience have distilled these alias best practices. Following them prevents common operational issues and enables smooth index management.
Naming Conventions
Be consistent: Establish and document a naming convention for your organization.
Encode purpose: Names like products_read, products_write, products_staging communicate intent.
Avoid special characters: Stick to lowercase alphanumeric and underscores/hyphens.
Don't encode versions in aliases: The alias is stable; version numbers belong in index names.
Operational Recommendations
Never query concrete index names in applications: Always use aliases. This is the most important rule.
Document alias purposes: Maintain a registry of aliases and their intended use.
Automate alias management: Use scripts or tools for routine operations like rollover.
Monitor alias state: Alert if production aliases point to unexpected indexes.
Test alias changes in staging: Practice atomic switches before production.
Migration Patterns
When migrating existing systems to use aliases:
- Create alias pointing to current index (no application changes yet)
- Update application to query alias instead of index
- Verify everything works
- Now you can reindex/switch freely
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Hardcoded index names | Can't switch without code deploy | Always use aliases in application code |
| No versioning scheme | Hard to track index history | Adopt consistent naming: {name}v{N}{timestamp} |
| Manual alias updates | Error-prone, audit trail missing | Automate with scripts, log all changes |
| Alias with same name as potential index | Conflicts when creating indexes | Use distinct naming: 'products' alias, 'products_v1' index |
| Too many aliases to one index | Confusing, hard to reason about | Consolidate to read/write pattern plus filters |
| Unbounded multi-index aliases | Query performance degrades | Limit time ranges, use rollover policies |
- •All application queries use aliases, never concrete index names
- •Separate read and write aliases for flexibility during reindexing
- •Versioned index names allow tracking history and enable rollback
- •Alias registry documented with purpose and ownership for each alias
- •Automated rollover for time-series data with ILM policies
- •Monitoring in place for alias-to-index mappings
- •Runbooks defined for common alias operations
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.