Skip to content
ExponaBLOG
ELASTICSEARCH & SEARCH ENGINEERING · DAY 060 / 100

Multi-tenant search in Elasticsearch & Search Engineering

Many customers, one cluster.

Expona Admin6 min read

Multi-tenant search in Elasticsearch & Search Engineering

When you're running a SaaS platform or marketplace with hundreds or thousands of customers, they all need search to work reliably—but never see each other's data. Multi-tenant search in Elasticsearch & Search Engineering is about architecting your cluster so that one shared infrastructure serves many isolated customers without becoming a security nightmare or performance bottleneck. In this article, we'll walk through the main strategies we've seen work in production, from simple field-based filtering all the way to document-level security.

Why multi-tenancy matters

Running a dedicated Elasticsearch cluster per customer sounds clean but becomes expensive fast. When you have 50 or 500 tenants, operational overhead explodes: monitoring, backups, version upgrades, storage duplication, and debugging are all multiplied. A shared cluster with proper isolation lets you amortize infrastructure costs while keeping data leakage and unauthorized access off the table. The challenge is choosing the right isolation strategy for your scale and compliance requirements.

Tenant ID field with filters

This is where most teams start, and for good reason. Every document gets a tenant_id field. Every query includes a filter that restricts results to the current tenant. It's simple, flexible, and works well up to moderate scale.

json
{
  "mappings": {
    "properties": {
      "tenant_id": {
        "type": "keyword"
      },
      "title": {
        "type": "text"
      },
      "description": {
        "type": "text"
      },
      "price": {
        "type": "float"
      }
    }
  }
}

When a user searches, your application layer enforces the tenant filter:

json
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "title": "laptop"
          }
        }
      ],
      "filter": [
        {
          "term": {
            "tenant_id": "customer-456"
          }
        }
      ]
    }
  }
}

The advantage here is flexibility. Tenants can be added or removed instantly without index restructuring. Aggregations, facets, and complex queries just work. The disadvantage is that human error in your application code—forgetting to add the tenant filter on one endpoint—exposes data. We've also seen teams burn significant CPU cycles filtering documents that should never have been returned in the first place.

For smaller deployments or when you're still onboarding tenants rapidly, this approach is pragmatic. Just make sure your query builders are centralized and tested, or use a query DSL wrapper that automatically injects tenant context.

Filtered aliases per tenant

An Elasticsearch alias with a filter is an invisible, automatic constraint applied to every query. You create one alias per tenant that points to the same index but only sees documents matching that tenant's ID. It's a clever layer between the application and the index.

json
{
  "actions": [
    {
      "add": {
        "index": "products",
        "alias": "products_tenant_customer_456",
        "filter": {
          "term": {
            "tenant_id": "customer-456"
          }
        }
      }
    },
    {
      "add": {
        "index": "products",
        "alias": "products_tenant_customer_789",
        "filter": {
          "term": {
            "tenant_id": "customer-789"
          }
        }
      }
    }
  ]
}

Now, when tenant 456's API queries products_tenant_customer_456, Elasticsearch enforces the filter at the shard level before results are returned. The benefit is security by architecture rather than application discipline. Even if a developer forgets to add a tenant check, the alias ensures isolation. Aggregations and facets are also automatically scoped.

The trade-off is operational overhead. Every new tenant onboarding needs an alias creation. You have many more aliases to track and index lifecycle management becomes trickier. If you're indexing at high volume across many tenants, you're still filtering large result sets internally, which can use CPU and memory.

We've used this approach most successfully when tenant count is moderate (hundreds rather than thousands) and onboarding is relatively predictable. Combine it with monitoring and automation to keep alias management clean.

Index per tenant only for large tenants

A hybrid approach: small and medium tenants share a multi-tenant index with tenant-ID filtering, while large tenants with high query volume or strict compliance requirements get their own dedicated index.

Let's say you have 200 small SaaS customers generating a few thousand documents each, but two enterprise customers with millions of documents and very specific SLAs. You'd keep the small ones in products_shared and give the large ones products_enterprise_acme and products_enterprise_globex.

json
{
  "tenant_id": "enterprise_acme",
  "index_name": "products_enterprise_acme",
  "shard_count": 8,
  "replica_count": 2
}
json
{
  "tenant_id": "small_customer_001",
  "index_name": "products_shared",
  "filter": {
    "term": {
      "tenant_id": "small_customer_001"
    }
  }
}

This is cost-effective and gives you control where it matters. Large tenants get performance isolation and can tune shards, replicas, and refresh intervals independently. You're not forcing every customer into the same resource constraints. Your routing layer needs to know which tenant uses which index, but that's a small metadata table.

The downside is complexity. Your query code or proxy must decide which index to hit. Migrations become tricky: if a small tenant grows into a large one, moving them from shared to dedicated requires re-indexing. Operational tooling around backups, snapshots, and monitoring now spans multiple indices.

In e-commerce and SaaS platforms with clear tiers, we've found this hybrid model the sweet spot between simplicity and efficiency. Build the routing logic once, then it scales as your customer base does.

Document-level security for strict isolation

When you need cryptographic or regulatory guarantees that no query can breach tenant boundaries, Elasticsearch has a built-in feature: Document-Level Security (DLS) via X-Pack. DLS enforces access control at query time through role-based permissions.

You define roles and users, and each role has a query that limits what documents that user can see:

json
{
  "roles": [
    {
      "name": "customer_456_role",
      "indices": [
        {
          "names": ["products"],
          "privileges": ["read"],
          "query": {
            "term": {
              "tenant_id": "customer-456"
            }
          }
        }
      ]
    }
  ]
}

When a user with customer_456_role runs any query on the products index, Elasticsearch transparently applies the DLS filter. No document with a different tenant_id will ever be returned, even if their query explicitly requests it.

DLS is powerful for compliance-heavy industries (healthcare, finance, regulated SaaS). It removes the need to trust application-layer filtering. The isolation is enforced by the database itself. However, DLS has performance implications—each query evaluation includes the security filter, and it can't always be pushed down to the shard as efficiently as simple filtering.

DLS also requires X-Pack licensing and careful permission management. Misconfigured roles can still leak data. And for very high query volume, the added CPU overhead is measurable.

Use DLS when regulatory or contractual requirements demand it, or when your tenant model includes adversarial scenarios (e.g., reseller platforms where one partner might try to spy on another). For most SaaS platforms, it's probably overkill if you've chosen a solid architecture from the start.

Common mistakes

  • Forgetting the tenant filter in one query path: A typo or edge case in your query builder means data leaks to the wrong tenant. Always centralize query construction and test thoroughly.
  • Assuming Elasticsearch is enough: Even with aliases and DLS, application-layer bugs can expose data. Defense in depth: validate tenant access in your API layer too.
  • Not accounting for administrative queries: Internal reports, dashboards, and debugging queries often bypass tenant scoping. Create separate admin indices or strict monitoring for these.
  • Mixing tenant fields with other filters carelessly: If your query logic is convoluted, it's easy to accidentally apply a filter meant for one field to another. Keep tenant filtering separate and explicit.
  • Scaling to hundreds of aliases without automation: Manual alias management is error-prone. Automate alias creation, updates, and deletion as part of your provisioning pipeline.
  • Ignoring storage and shard overhead: Each tenant_id field adds metadata. Many small indices or aliases mean more overhead per shard. Model your scale assumptions early.

Key takeaways

  • Multi-tenant search in Elasticsearch & Search Engineering begins with a tenant_id field and consistent filtering, but scale and compliance requirements drive your architecture choice.
  • Filtered aliases add a security layer between your application and data, reducing the risk of forgotten tenant filters.
  • A hybrid model of shared indices for small tenants and dedicated indices for large ones balances cost and performance.
  • Document-Level Security via X-Pack enforces isolation at the database layer, valuable for regulated industries but with performance overhead.
  • Centralize your query construction, test tenant isolation thoroughly, and automate provisioning and monitoring to avoid data leaks as you scale.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles