TechEarl

How to Use ElasticPress with WP_Query

Wire ElasticPress to WP_Query so WordPress queries hit Elasticsearch instead of MySQL. Covers installation, indexable post types, ep_integrate, the wp-cli index command, faceted search with aggregations, and when ES actually beats MySQL FULLTEXT.

Ishan KarunaratneIshan Karunaratne⏱️ 14 min readUpdated
Wire ElasticPress to WP_Query so WordPress queries hit Elasticsearch instead of MySQL. Install, indexable post types, ep_integrate, wp-cli index, faceted aggregations, and when ES actually beats MySQL FULLTEXT.

ElasticPress is the WordPress plugin that bridges WP_Query to Elasticsearch. The integration is a one-line flag ('ep_integrate' => true) 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.

Try it with your own values

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

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):

bash
composer require 10up/elasticpress

Then symlink or copy vendor/10up/elasticpress into wp-content/plugins/.

For the Elasticsearch host you have three sensible options in 2026:

HostCostNotes
Self-hosted Elasticsearch 8.x on the same boxfreeLowest latency but you're now operating a JVM in production
Elastic Cloud (managed)$$ from ~$95/monthZero ops, one-click upgrades
Bonsai (managed, ElasticPress-friendly)$ from $10/monthCheapest 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:

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:

php
// 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:

php
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

bash
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:

bash
wp elasticpress index --setup --network-wide

For incremental updates without a full rebuild:

bash
wp elasticpress index --include=42,43,44   # specific post IDs
wp elasticpress index --post-type=:post_type  # one post type only

For 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:

php
$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:

php
$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:

bash
# 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=12345

If 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:

bash
# 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 profileShould you use ElasticPress?
Blog with 200 posts, default searchNo. MySQL LIKE search is instant at that scale.
Blog with 5,000 posts, default searchMaybe. MySQL FULLTEXT (with MATCH...AGAINST) handles this well.
News site with 50,000 articles + filtering by category, author, dateProbably yes. Search relevance and facets are noticeably better.
E-commerce with 10,000 products + meta_query filteringYes. WooCommerce on MySQL hits N+1 wpostmeta JOINs at this scale and crawls.
E-commerce with 100,000+ productsDefinitely yes. MySQL is no longer practical for faceted product search.
Custom search with synonyms, fuzzy matching, weighted fieldsYes. 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:

FAQ

TagsWordPressElasticPressElasticsearchOpenSearchWP_Querywp-cliMySQLPerformance
Share
Ishan Karunaratne

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

Use grep -v 'pattern' file to print every line that does not match. Exclude multiple patterns with -e or -vE, strip comments and blank lines, count with -vc, and avoid the OR-becomes-AND double-negative trap.

How to Exclude Matches with grep -v (Invert Match)

grep -v 'pattern' file prints every line that does NOT match. The flag reference, how to exclude multiple patterns, the strip-comments-and-blank-lines pipeline, the double-negative trap where -v of an OR becomes an AND of negations, and the macOS BSD vs GNU differences.

Elasticsearch zero-downtime reindex with the alias-swap pattern: change mapping, reindex, atomically swap the alias, no search outage. Bash script for execute + rollback. Verified on 8.x and 9.x.

How to Reindex Elasticsearch with Zero Downtime

The alias-swap pattern I use in production to change Elasticsearch mappings without taking the search down. Walks through the five steps, gives you a parameterized bash script that runs the reindex and rolls back if needed, and verifies the technique against Elasticsearch 8.x and 9.x.

Search multiple patterns with grep: grep -e 'A' -e 'B', grep -E 'A|B' alternation, and grep -f patterns.txt. Covers -F fixed strings, AND logic with chained greps and PCRE lookahead, and BSD vs GNU differences on macOS.

How to Search Multiple Patterns with grep

grep can OR several patterns three ways: -e per pattern, -E with alternation, or -f reading the list from a file. The one-liner is grep -E 'ERROR|WARN|FATAL' file. Here is when to pick each, how -F speeds up literal multi-pattern search, why grep has no single-pass AND, and the BSD vs GNU differences that bite on macOS.