Loading learning content...
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:
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.
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.
When you query an alias:
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.
Aliases can have associated properties that modify their behavior:
| 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.
The _aliases API allows multiple alias modifications in a single atomic operation. This is the foundation of zero-downtime index switching.
When you submit multiple actions in a single _aliases request:
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.
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.
Effective use of aliases requires a consistent index versioning scheme. Several patterns have emerged as best practices.
The simplest scheme: append a version number.
products_v1 → products_v2 → products_v3
Advantages:
Disadvantages:
Include creation timestamp in the name.
products_20240115_103045 → products_20240122_142030
Advantages:
Disadvantages:
Combine semantic versioning with timestamps.
products_v3_20240115 → products_v3_20240122 (minor updates)
products_v4_20240201 (major schema change)
Advantages:
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 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.
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.
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.
Filtered aliases provide a level of data isolation, but they're not a complete security boundary:
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.
A sophisticated pattern for managing index lifecycles is separating read and write operations through different aliases.
Create two aliases for each logical index:
products_read): Points to the index(es) queries should searchproducts_write): Points to the index receiving new documentsDuring 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.
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.
Aliases can point to multiple indexes simultaneously, enabling powerful patterns for time-series data, data lifecycle management, and horizontal scaling.
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.
Elasticsearch's ILM works seamlessly with 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.
Years of production experience have distilled these alias best practices. Following them prevents common operational issues and enables smooth index management.
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.
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.
When migrating existing systems to use aliases:
| 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 |
Index aliases are the cornerstone of maintainable search infrastructure. They provide the indirection that makes zero-downtime operations possible and operational management tractable.
What's next:
We've covered aliases as the mechanism for managing index versions. The final piece is putting it all together with blue-green indexing—a deployment pattern that uses everything we've learned to achieve truly risk-free search deployments.
You now understand how index aliases enable flexible, zero-downtime index management. This capability is the foundation for all advanced deployment patterns in search systems. Next, we'll explore blue-green indexing as the capstone deployment strategy.