One of the nastier Magento 2 search bugs leaves no error at all. A custom indexer pushes documents into an Elasticsearch index named <prefix>_product_0 — an index your storefront never reads — while the live <prefix>_product_1 index quietly goes stale. Sometimes you'll catch a no such index [<prefix>_product_0] or an index_not_found_exception in the logs, but more often Elasticsearch auto-creates the phantom index on the first bulk write and reports success. The result: customers see outdated prices, stock, and listings, and nothing in your monitoring flags it. Below we cover why the _0 suffix appears, how to confirm it, and how to point writes back at the index the storefront actually queries.
If this error is impacting live revenue, our senior Magento team can help you debug and deploy a fix safely.
Contact us →
Why Magento 2 writes to the wrong index
Per-store Elasticsearch indices follow the pattern <prefix>_product_<storeId>. The bug is almost always one line: a custom service builds that name from $storeManager->getStore()->getId() instead of an explicit store view. MagentoStoreModelStoreManagerInterface::getStore() returns the current store — and outside a storefront HTTP request, "current" resolves to the admin scope, whose store id is 0. Real store views start at 1, so the index name becomes a dead <prefix>_product_0.
- Using
getStore()to name a store-scoped ES index — it falls back to admin (id0) whenever no storefront request is in flight. - Indexing triggered from an admin save observer, a
bin/magentocommand, a cron job, or a queue consumer, where store emulation is never set. - Treating store_id
0as a storefront scope. In Magento,0is the admin/config scope — valid for reading configuration, never for store-scoped catalog indices. - Elasticsearch auto-creating the missing index on first write, so the wrong index "works" while the live one rots.
1. Confirm a stray admin-scope index exists
Start at the index list. A _product_0 index should not exist at all.
curl -s 'http://localhost:9200/_cat/indices?v' | grep -E '_product_(0|1)' && echo '--- doc counts ---' && curl -s 'http://localhost:9200/<prefix>_product_0/_count' ; curl -s 'http://localhost:9200/<prefix>_product_1/_count'
You should see a stray <prefix>_product_0, and/or a <prefix>_product_1 whose doc count never moves after the indexer runs. Either signal confirms writes are landing in the admin-scope index instead of the storefront one.
2. Find every store-id resolution in custom indexing code
Grep for the leak. We've seen this most often when indexing logic was lifted from a controller — where getStore() happened to return a real store view — into an observer or CLI command, where it no longer does.
grep -rn 'storeManager->getStore()' app/code | grep -iE 'index|elastic|search'
Each hit is a place where the ambient "current store" leaks into the index name. Those are the lines to change.
3. Resolve a real storefront store view
Swap the current-store call for one that can never return admin.
// Replace:
// $storeId = $storeId ?? (int) $storeManager->getStore()->getId();
// With a guaranteed storefront store view (never admin / id 0):
$storeId = $storeId ?? (int) ($storeManager->getDefaultStoreView()?->getId() ?? 1);
getDefaultStoreView() returns the default storefront view (id >= 1) and never the admin store, so the index name always points at something the storefront reads. The ?? 1 guards the rare unconfigured case. For per-store indexing, iterate $storeManager->getStores() — it excludes admin — rather than trusting getStore().
If this error is impacting live revenue, our senior Magento team can help you debug and deploy a fix safely.
Contact us →
4. Reindex into the correct index and drop the orphan
With the store id fixed, rebuild and clean up.
bin/magento indexer:reindex catalogsearch_fulltext && curl -s -X DELETE 'http://localhost:9200/<prefix>_product_0'
The <prefix>_product_1 count should rise to your full catalog total and the stray _product_0 should be gone. In a blue-green or multi-store setup, reindex per active store view and re-point aliases rather than deleting a live index.
5. Verify the storefront index stays fresh
Re-run the indexer from CLI, then check the count holds.
curl -s 'http://localhost:9200/<prefix>_product_1/_count' && curl -s 'http://localhost:9200/_cat/indices?v' | grep _product_0 || echo 'no _product_0 index — good'
A live count that matches the catalog, plus no _product_0, confirms documents now land in the storefront index from every execution context.
When this points to a deeper problem
Deleting the orphan index fixes today's symptom. The pattern that produced it usually runs deeper:
- Conflating config/admin scope (store_id
0) with a real store view —0is never a valid target for store-scoped catalog indices. - Indexing logic that depends on ambient request state instead of taking an explicit, validated store id as a parameter, so behavior silently differs between web, CLI, and cron.
- A silent-failure surface: Elasticsearch auto-creates missing indices and the indexer logs success, so a wrong-index bug surfaces only as "stale search."
The durable fix is an audit. Grep the whole codebase for getStore()->getId() and getStoreId() feeding any ES index name, and ask of each: can this run from an observer, CLI, cron, or queue? If yes, resolve the store explicitly. Add a guard rejecting store_id 0 for store-scoped writes, and a health check comparing the live index doc count against the catalog so a regression is caught immediately instead of as a customer complaint.
You now have everything you need to find the leak, point writes back at <prefix>_product_1, and keep them there. One grep and one line of code usually close it out for that execution path.