ElasticPress is a WordPress plugin that routes WP_Query through Elasticsearch by adding 'ep_integrate' => true to the query args, so the query hits Elasticsearch instead of MySQL. The flag is one line, but the surrounding plumbing matters: which post types are indexable, which fields get indexed and weighted, how to reindex after bulk imports without taking the site down, and the question almost no tutorial answers honestly: does Elasticsearch actually beat MySQL FULLTEXT for your data, or are you adding a moving part for no real benefit? Below: the full setup walkthrough, the production patterns that keep the index from rotting, faceted search with aggregations, and the cost/benefit honest answer.
How do I use ElasticPress with WP_Query?
Install ElasticPress (composer require 10up/elasticpress or via plugin upload), define EP_HOST in wp-config.php to point at your Elasticsearch endpoint, activate the plugin, and run wp elasticpress index --setup from WP-CLI to build the initial index. Then add 'ep_integrate' => true to any WP_Query arguments to route that query through Elasticsearch instead of MySQL. For just search forms, enable the "Search" feature in the ElasticPress admin and queries with a non-empty s parameter automatically use ES. The ep_indexable_post_types filter controls which post types get indexed; the ep_search_fields filter controls which fields are queried. For the underlying PHP runtime considerations, see PHP Memory Limit: How to Fix 'Allowed Memory Size Exhausted'. For bulk-import-and-reindex patterns, see wp_insert_post Consuming Large Amounts of Memory.
Set your Elasticsearch host, WordPress table prefix, and the post type you index most. WP-CLI and curl examples below update so you can copy and run.
Jump to:
- Install ElasticPress and an Elasticsearch host
- Configure EP_HOST and indexable post types
- Build the initial index with wp-cli
- Route queries through ES with ep_integrate
- Faceted search with aggregations
- Reindex after bulk imports
- Common errors and how to debug
- Cost vs benefit: does ES actually beat MySQL FULLTEXT?
- Common pitfalls
- What to do next
- FAQ
Install ElasticPress and an Elasticsearch host
Two install paths:
Plugin (the WordPress-native way):
Download from the ElasticPress plugin page or install from the admin Plugins screen.
Composer (for project-managed WordPress sites):
composer require 10up/elasticpressThen symlink or copy vendor/10up/elasticpress into wp-content/plugins/.
For the Elasticsearch host you have three sensible options in 2026:
| Host | Cost | Notes |
|---|---|---|
| Self-hosted Elasticsearch 8.x on the same box | free | Lowest latency but you're now operating a JVM in production |
| Elastic Cloud (managed) | $$ from ~$95/month | Zero ops, one-click upgrades |
| Bonsai (managed, ElasticPress-friendly) | $ from $10/month | Cheapest managed option, designed for ElasticPress |
ElasticPress officially supports Elasticsearch, not OpenSearch. If you are weighing OpenSearch or AWS OpenSearch Service for the backend, see Elasticsearch vs OpenSearch for ElasticPress for the real compatibility status before committing.
For a typical content site with under 100k posts, a single small managed Elasticsearch instance is plenty. For an e-commerce site with 500k+ products and faceted filtering, plan for at least a 3-node cluster with 4 to 8 GB RAM per node.
Configure EP_HOST and indexable post types
In wp-config.php:
define('EP_HOST', 'https://es-username:es-password@search.example.com:9243');For Elastic Cloud, the host is provided as a single URL with credentials embedded.
Configure which post types get indexed via a filter. By default ElasticPress indexes post and page:
// In a custom plugin or theme functions.php
add_filter('ep_indexable_post_types', function ($post_types) {
$post_types[] = 'product'; // WooCommerce
$post_types[] = 'event';
unset($post_types['attachment']); // exclude
return $post_types;
});Configure which fields get searched:
add_filter('ep_search_fields', function ($search_fields) {
return [
'post_title^10', // boost title matches 10x
'post_content',
'post_excerpt^3',
'meta.sku.value^5', // custom field with 5x boost
];
});The ^N boost syntax tells Elasticsearch to weight matches in that field N times more heavily. Title and SKU matches almost always deserve higher weight than body content.
For per-feature configuration (Protected Content, WooCommerce, Documents, Autosuggest, Synonyms, Facets), use the ElasticPress admin under Dashboard > ElasticPress > Features.
Build the initial index with wp-cli
wp elasticpress index --setup--setup deletes any existing index and rebuilds from scratch. Use it on the first run and after schema changes. On subsequent runs (after a post type change, for example), use --setup again to delete-and-rebuild; ElasticPress will not migrate the existing index.
For multisite installs:
wp elasticpress index --setup --network-wideFor incremental updates without a full rebuild:
wp elasticpress index --include=42,43,44 # specific post IDs
wp elasticpress index --post-type=:post_type # one post type onlyFor a large index (100k+ posts), the initial build can take 10 to 60 minutes depending on Elasticsearch resources and network. ElasticPress streams posts in batches of 350 by default; tune with --per-page=N. Smaller batches use less PHP memory but more HTTP overhead.
Run the indexer from a screen/tmux session OR via WP-CLI's --allow-root from a systemd timer if you're scheduling it. For the underlying loop and parallel patterns, see Bash For Loops.
Route queries through ES with ep_integrate
This is the part most people are really asking about: I would like to run custom WP_Query calls through ElasticPress, how do I do this? The answer is a single argument. Once the index exists, any WP_Query with 'ep_integrate' => true runs through Elasticsearch:
$args = [
'ep_integrate' => true,
'post_type' => 'product',
'posts_per_page' => 12,
'meta_query' => [
[
'key' => '_price',
'value' => 50,
'compare' => '<=',
],
[
'key' => '_stock_status',
'value' => 'instock',
'compare' => '=',
],
],
'orderby' => 'date',
'order' => 'DESC',
];
$query = new WP_Query($args);
if ($query->have_posts()) {
while ($query->have_posts()) {
$query->the_post();
the_title('<h2>', '</h2>');
}
}
wp_reset_postdata();The same $args work with get_posts(). Behavior is otherwise identical to a MySQL-backed query: pagination, wp_reset_postdata(), the_loop(), all work as usual.
For search forms specifically, ElasticPress's "Search" feature (admin toggle) auto-routes queries with a non-empty s parameter. You don't need to set ep_integrate on the front-end search form: when search is enabled, EVERY search runs through Elasticsearch.
For WP_Term_Query and WP_User_Query, the equivalents are 'ep_integrate' => true on those query classes too. ElasticPress supports terms and users via the Term/User indexable features.
Faceted search with aggregations
ElasticPress supports Elasticsearch's aggregations directly:
$args = [
'ep_integrate' => true,
'post_type' => 'product',
'posts_per_page' => 24,
'aggs' => [
'by_category' => [
'terms' => ['field' => 'terms.product_cat.slug', 'size' => 50],
],
'price_ranges' => [
'range' => [
'field' => 'meta._price.long',
'ranges' => [
['to' => 50],
['from' => 50, 'to' => 200],
['from' => 200],
],
],
],
],
];
$query = new WP_Query($args);
// Access aggregation results from the query object
$aggs = $query->query_vars['ep_aggregations'] ?? null;The aggs parameter is Elasticsearch's native syntax: counts, ranges, histograms, nested aggregations. Returned data lives on query_vars['ep_aggregations'] after the query runs.
For e-commerce, the canonical use is "show 20 products and the count per category/price-range/attribute". MySQL would need a separate query per facet AND the full product query, each scanning the entire products table. Elasticsearch does it all in a single index scan.
ElasticPress also has a "Facets" feature that exposes WordPress-style facet widgets without writing aggregation JSON. For custom UI or non-standard facets, write your own aggregations with the aggs parameter.
Reindex after bulk imports
After a wp_insert_post loop creates thousands of posts, ElasticPress's per-insert sync (via save_post) is either too slow or was disabled during the import. Reindex explicitly:
# Full rebuild (safest, slow)
wp elasticpress index --setup
# Specific post types (faster)
wp elasticpress index --setup --post-type=:post_type
# Single post (fast)
wp elasticpress index --include=12345If you disabled the ElasticPress sync hook during the import (see wp_insert_post Consuming Large Amounts of Memory), run wp elasticpress index --setup afterwards to bring the index back in sync.
For zero-downtime reindexing on a live site, use ElasticPress's index aliasing (the default behavior since 4.0): the indexer writes to a new index, then atomically swaps the alias when the build is complete. Read traffic continues to hit the old index until the swap.
Common errors and how to debug
"No index found" / 404 from Elasticsearch. The Elasticsearch index doesn't exist yet. Run wp elasticpress index --setup.
"Different schema versions" / mapping conflicts. After upgrading ElasticPress or changing indexed fields, the existing index has the old schema. Run wp elasticpress index --setup to rebuild with the new schema.
Connection refused / timeout. Wrong EP_HOST, firewall blocking outbound 9200/9243, or the cluster is overloaded. Test the cluster directly:
# Cluster health
curl -u user:pass :es_host/_cluster/health?pretty
# List indices, filtered to your WordPress prefix
curl -u user:pass ":es_host/_cat/indices/:index_prefix*?v"Search returns 0 results even though posts exist. The index is empty (run --setup), the post status filter excludes the posts (only publish is indexed by default), or your query has a meta_query/tax_query that doesn't match in ES (some compare operators behave subtly differently).
WP_Query falls back to MySQL silently. ElasticPress falls back to MySQL when it can't translate the query. Enable debug mode (define('EP_ENABLE_DEBUG_BAR', true) plus the Debug Bar plugin) to see which queries hit ES and which fell back.
Mapping size error. A field has more than 1000 unique values per document. Configure mapping limits in the Elasticsearch index settings, or restructure the data to avoid high-cardinality flat fields.
For the underlying MySQL data structure that ElasticPress indexes, see MySQL Field Types and Sizes.
Cost vs benefit: does ES actually beat MySQL FULLTEXT?
The honest answer most ElasticPress tutorials don't give:
| Site profile | Should you use ElasticPress? |
|---|---|
| Blog with 200 posts, default search | No. MySQL LIKE search is instant at that scale. |
| Blog with 5,000 posts, default search | Maybe. MySQL FULLTEXT (with MATCH...AGAINST) handles this well. |
| News site with 50,000 articles + filtering by category, author, date | Probably yes. Search relevance and facets are noticeably better. |
| E-commerce with 10,000 products + meta_query filtering | Yes. WooCommerce on MySQL hits N+1 wpostmeta JOINs at this scale and crawls. |
| E-commerce with 100,000+ products | Definitely yes. MySQL is no longer practical for faceted product search. |
| Custom search with synonyms, fuzzy matching, weighted fields | Yes. Out-of-scope for MySQL. |
ElasticPress costs you: operational complexity (one more service to monitor), $10 to $200/month for a managed host or 4 to 8 GB of self-hosted RAM, and the discipline to keep the index in sync after every schema change.
You get back: 10 to 100x faster filtered queries on large catalogs, relevance scoring that's actually relevance-scored, fuzzy matching and synonyms, faceted aggregations in a single query, and search-as-you-type via the Autosuggest feature.
The breakeven is usually around the point where your MySQL search starts taking >500 ms or where your meta_query/tax_query JOINs hit double-digit milliseconds. Below that, stay on MySQL. Above that, ElasticPress is worth the operational cost.
For tracking when queries cross that line, profile with Query Monitor and watch the slow-query log. See PHP Memory Limit: How to Fix 'Allowed Memory Size Exhausted' for what happens to memory when a slow search starts swapping under load.
Common pitfalls
Index goes stale after schema changes. Changing ep_indexable_post_types, ep_search_fields, or adding a new custom meta field requires a --setup rebuild. ElasticPress will not auto-migrate.
save_post indexing is synchronous by default. A page save blocks until ES confirms the index update. On a slow ES cluster this makes the admin sluggish. Use ElasticPress's async indexing feature (queues changes and processes via WP-Cron) for write-heavy sites.
Network calls during admin operations. Every post save triggers an ES network call. If the ES cluster is unreachable, post saves still succeed but the index falls behind. Monitor ES health.
MySQL FULLTEXT still runs for non-EP queries. If a query doesn't have ep_integrate => true and isn't a search query, it runs against MySQL. Mixing ES and MySQL search in the same page request is fine but counterintuitive when debugging.
Versions matter. ElasticPress targets Elasticsearch, and the required Elasticsearch version has moved up across ElasticPress releases. Match versions to the ElasticPress compatibility documentation before deploying.
Aggregation cardinality blow-up. Aggregating on a high-cardinality field (e.g., user IDs across 1M posts) can OOM the ES cluster. Use size limits on terms aggregations.
WP-CLI index times out. For huge sites, the indexer can run for hours. Use --per-page=350 and run via tmux to survive disconnects.
The reindex deletes data. wp elasticpress index --setup drops the existing index. If you're running it on production, ALL searches return 0 results until the rebuild finishes. ElasticPress 4.0+ uses index aliasing to avoid this; older versions don't.
What to do next
For surrounding infrastructure:
- PHP Memory Limit: How to Fix 'Allowed Memory Size Exhausted': the runtime knob that catches WordPress queries before they swap.
- wp_insert_post Consuming Large Amounts of Memory: bulk-import pattern; disable ElasticPress sync during the import then reindex after.
- How to Remove Empty Values from a PHP Array: sanitize search inputs and faceted-filter arrays before passing them to WP_Query.
- MySQL Field Types and Sizes: the schema reference for the underlying wp_posts/wp_postmeta tables that ElasticPress mirrors into Elasticsearch.
- Export or Backup All MySQL Databases: back up before any large reindex, especially on production.
- Elasticsearch Cheat Sheet: the raw Elasticsearch query DSL ElasticPress builds on top of.
- How to Optimize JPEG Images Using jpegoptim: paired image-pipeline content for media-heavy ElasticPress sites.
FAQ
Yes. Add 'ep_integrate' => true to the query arguments and that specific WP_Query (or get_posts()) call runs through Elasticsearch instead of MySQL. It works with custom post types, meta_query, tax_query, ordering and pagination, so a custom query behaves identically to its MySQL version, just faster on large datasets.
You only need ep_integrate on non-search queries. Front-end search forms are auto-routed once the Search feature is enabled in the ElasticPress admin. See Route queries through ES with ep_integrate above for a complete example.
The plugin is free and open source (GPL). What costs money is the Elasticsearch host. Self-hosting on the same server is free (you pay in RAM and operations effort). Managed options start at ~$10/month (Bonsai) and scale up to hundreds for enterprise clusters.
10up (the company behind ElasticPress) offers a paid enterprise tier with priority support and additional features, but the core plugin is fully usable without it.
WP-CLI. Initial indexing on any non-trivial site (1000+ posts) takes longer than the admin UI's request lifecycle can sustain. The PHP-FPM/Apache request will time out before the index finishes, even though indexing continues in the background. WP-CLI runs cleanly to completion.
For ad-hoc reindexing of a few specific posts, the admin UI is fine. For full rebuilds, always use wp elasticpress index --setup from a shell session.
Not as a supported configuration. ElasticPress officially supports Elasticsearch, and 10up's compatibility documentation states it does not officially support OpenSearch or recommend it for production. Basic functionality may work, but a version-number mismatch between the two engines causes problems.
If you need OpenSearch anyway, it can be coaxed into working unofficially with a version-mapping workaround or a third-party bridge plugin. See Elasticsearch vs OpenSearch for ElasticPress for the full status and what to run.
No. When a query runs through ElasticPress (via ep_integrate or the Search feature), MySQL is not queried for that result set. ElasticPress builds the post objects from data stored in the Elasticsearch index itself.
That said, OTHER queries on the same page (e.g., menus, sidebars, widgets) still hit MySQL. ElasticPress only routes the queries you've configured to use it.
Yes for changes to the index mapping: adding a new searchable post type, adding new search fields, changing the analyzer. Run wp elasticpress index --setup to rebuild.
No for content changes: when a post is created, updated, or deleted, ElasticPress syncs the change automatically via the save_post and related hooks. Only the mapping (the schema) requires a manual rebuild.
Install the Debug Bar plugin plus Debug Bar ElasticPress. Enable define('EP_ENABLE_DEBUG_BAR', true); in wp-config.php. Each request now shows the Elasticsearch DSL query, the response, and timing data in the admin bar.
For CLI debugging, set WP_DEBUG_LOG and the elastic-related calls log to wp-content/debug.log. Query Monitor (separate plugin) catches when a query falls back to MySQL.
Run wp elasticpress index (without --setup) for incremental re-indexing of all posts. For a known subset, use --include=ID1,ID2 or --post-type=type. For "burn it down and rebuild", use --setup.
Out-of-sync indexes usually trace back to (a) hooks removed during bulk imports without being re-added afterwards, (b) Elasticsearch being unreachable when a post was saved, or (c) a mapping change that requires a rebuild.
Yes, very well. ElasticPress has a dedicated WooCommerce feature that handles product search, product filtering by meta_query, and product attribute facets. For stores with 5,000+ products it's effectively required: MySQL's wp_postmeta JOIN performance becomes a bottleneck above that scale.
Add 'ep_integrate' => true to your product query or enable the WooCommerce feature in the ElasticPress admin to auto-route product queries through Elasticsearch.





