WordPress search has a reputation, and it is not a good one. Out of the box it runs a LIKE '%term%' query against post titles, excerpts and content, orders by date, and calls it a day. No relevance, no typo tolerance, and on a site with a few hundred thousand posts it can take seconds. ElasticPress, a free plugin maintained by 10up, replaces that with Elasticsearch while keeping the familiar WP_Query interface. This article goes through what the plugin can do, with a closer look at the features most sites end up relying on: Post Search, Did You Mean, Filters, Related Posts, Custom Search Results, Autosuggest and the WooCommerce integration.
The basics: how ElasticPress works
The plugin needs an Elasticsearch server to talk to. That can be one you host yourself, a managed cluster, or ElasticPress.io, which is 10up’s hosted option with a few extras bundled in. The current 5.x release line asks for WordPress 6.2 or later, PHP 7.4 or later and Elasticsearch 5.2 or later; OpenSearch is also supported. Once you point the plugin at a host in ElasticPress → Settings (or via a constant in wp-config.php), you run a sync and the plugin pushes your posts into an index.
// wp-config.php
define( 'EP_HOST', 'https://search.example.com:9200' );
// Optional, if your host needs basic auth
define( 'ES_SHIELD', 'username:password' );From then on, ElasticPress keeps the index in step with WordPress. Save a post, it is reindexed; trash it, it is removed. You can also sync from the command line, which is the sensible way to do it on a large site:
wp elasticpress sync --setup --yes
wp elasticpress status
wp elasticpress statsEverything else is organised as Features, which you switch on and off from the dashboard. Beyond the seven covered in detail below, the plugin ships with: Protected Content (routes admin queries through Elasticsearch too, which speeds up the post list and media library considerably), Documents (indexes the text inside PDFs, Word files and similar attachments so they show up in search), Comments and Terms (indexes those objects and lets you query them), Instant Results (a modal search experience that queries Elasticsearch straight from the browser), Synonyms, Search Ordering, and Weighting. We will come back to the last three under Post Search and Custom Search Results, since that is where they actually live in the interface.
Post Search
What it does
This is the one feature that is on by default, and it is the reason most people install the plugin. With it enabled, any WP_Query that has an s parameter is sent to Elasticsearch instead of MySQL. That includes the site search page, the search widget and block, and any custom queries you write yourself. Results come back ranked by relevance rather than date, and the match is fuzzy, so “accesibility” will still find your accessibility guidelines.
What gets searched is broader than core: title, content, excerpt, author name, taxonomy terms and (if you opt in) post meta. The search algorithm was overhauled in version 3.5 and is now a layered query: exact phrase matches score highest, then matches of all terms, then matches of most terms with fuzziness applied. If you ever need the older behaviour, there is a filter for it.
Weighting
The Post Search feature has a sub-screen called Weighting. For each post type you can decide which fields are searchable and how much a hit in each field is worth. Pushing the title up to a weight of 40 and the content down to 1, for example, makes title matches dominate the results. You can also add individual meta fields here and weight them like any other field. This is a dashboard setting, not code, which makes it easy to tune with an editor sitting next to you.
Synonyms
Also tucked under Post Search is a Synonyms editor. You can define sets where every word means the same thing (laptop, notebook, portable computer) or one-directional replacements (sneakers => shoes). Synonyms are applied at query time, so you do not need to reindex after changing them. Hyphenated terms deserve a mention: Elasticsearch’s standard tokenizer splits on hyphens, so a synonym for “e-commerce” needs to be written to account for that.
Using it in code
Searches already go through Elasticsearch. For non-search queries, which still hit MySQL unless Protected Content is on, you can opt in per query with ep_integrate. This is useful for archive pages with complex meta or tax queries that are slow in SQL:
$query = new WP_Query( [
'post_type' => 'event',
'posts_per_page' => 20,
'ep_integrate' => true,
'meta_query' => [
[
'key' => 'event_date',
'value' => date( 'Y-m-d' ),
'compare' => '>=',
'type' => 'DATE',
],
],
'orderby' => 'meta.event_date.date',
'order' => 'ASC',
] );If you need to change how posts are indexed (for example to include a custom field that is not stored as post meta), the ep_post_sync_args filter lets you alter the document before it is sent:
add_filter( 'ep_post_sync_args', function ( $post_args, $post_id ) {
$post_args['meta']['reading_time'] = [
[ 'value' => my_theme_reading_time( $post_id ) ],
];
return $post_args;
}, 10, 2 );Did You Mean?
Fuzzy matching covers most typos, but sometimes a query is so far off that nothing comes back, or what comes back is clearly not what the visitor meant. The Did You Mean feature uses Elasticsearch’s phrase suggester to propose a correction based on terms that actually exist in your content. Search for “wordpres plguin” and you get a line at the top of the results reading Did you mean: wordpress plugin?, with the suggestion linked to a fresh search.
Enabling the feature changes the index mapping (it needs a shingle analyzer on the title and content), so you have to run a full sync after turning it on. In the feature settings you choose whether to show the suggestion only when there are no results, or always. There is also an option to automatically redirect to the suggested term when the original search returns nothing, which is a small change that noticeably reduces dead ends.
Block themes get this for free on the search results template. Classic themes may need to output it manually:
// In search.php, above the loop
if ( function_exists( 'ElasticPress\Features::factory' ) ) {
$feature = \ElasticPress\Features::factory()->get_registered_feature( 'did-you-mean' );
if ( $feature && $feature->is_active() ) {
echo $feature->get_suggestion(); // outputs the markup, or nothing
}
}You can also hook the suggestion output. The ep_suggestion_html filter receives the generated HTML, the raw terms and the query object, so wrapping it in your own markup is simple.
Filters
The Filters feature (it was called Facets in older versions, and the code still uses that name) adds faceted navigation: a list of taxonomy terms, each with a count, that visitors can click to narrow results. Those counts come from Elasticsearch aggregations on the current result set, so they are always accurate for whatever is being shown right now. This is something that is painful to do in plain WordPress, because it needs one COUNT query per term.
It comes as a set of blocks: Filter by Taxonomy, Filter by Metadata, Filter by Meta Range (a slider, handy for prices or dates), Filter by Post Type and Filter by Date. There are matching widgets for classic themes. Drop one into a sidebar, pick the taxonomy, choose whether the display is a list or a dropdown, and set the ordering. Selected filters show up in the URL as query parameters, so filtered views can be shared and bookmarked.
One setting is worth understanding before you ship it. Match type decides what happens when a visitor ticks two terms from the same taxonomy. “Show any content tagged to all selected terms” is an AND; “Show all content tagged to any selected term” is an OR. Which one feels right depends on the taxonomy. For a “colour” facet on products, OR is almost always what people expect.
By default the filters only apply on search and archive pages. To get them working on a custom page template, or to change which queries they attach to, there is a filter:
add_filter( 'ep_facet_include_dynamic_term_count', '__return_true' );
add_filter( 'ep_facet_query_filters', function ( $filters, $args, $query ) {
// Only let filters touch our product catalogue query
if ( empty( $args['post_type'] ) || 'product' !== $args['post_type'] ) {
return [];
}
return $filters;
}, 10, 3 );Metadata filters need the meta key to be indexed, which means it has to appear in the Weighting screen or be whitelisted with ep_prepare_meta_allowed_keys. If a facet renders but shows no terms, that is the first thing to check.
Related Posts
Most “related posts” plugins either match on shared tags or run a brutally expensive query against post content. ElasticPress uses Elasticsearch’s more_like_this query, which takes the title and content of the current post, extracts the most distinctive terms, and finds other documents that share them. The results are usually a lot more relevant than tag matching, and the query runs in a few milliseconds regardless of how many posts you have.
There is a Related Posts block (and a widget) with a setting for how many posts to show. If you would rather control the markup yourself, the feature exposes a function:
$feature = \ElasticPress\Features::factory()->get_registered_feature( 'related_posts' );
$related = $feature->find_related( get_the_ID(), 4 );
if ( ! empty( $related ) ) {
echo '<ul class="related-posts">';
foreach ( $related as $post ) {
printf(
'<li><a href="%s">%s</a></li>',
esc_url( get_permalink( $post ) ),
esc_html( get_the_title( $post ) )
);
}
echo '</ul>';
}The underlying query arguments can be adjusted with ep_related_posts_query_args. A common tweak is restricting related items to the same post type, or excluding a “news” category so evergreen articles do not recommend stale announcements:
add_filter( 'ep_related_posts_query_args', function ( $args, $post_id ) {
$args['post_type'] = get_post_type( $post_id );
$args['tax_query'] = [
[
'taxonomy' => 'category',
'field' => 'slug',
'terms' => [ 'news' ],
'operator' => 'NOT IN',
],
];
return $args;
}, 10, 2 );Custom Search Results
Relevance scoring is good, but it does not know about your business. When the marketing team wants the new pricing page to be the first result for “pricing” no matter what, you need a way to pin results by hand. That is what Custom Search Results does. In the admin it appears as a post type called Custom Results: you create one, type in the search term, and drag posts into the order you want them to appear at the top. Everything else follows below in normal relevance order.
The matching is exact on the term, so “pricing” and “prices” are two separate custom results. In the interface there is a live preview of what the search currently returns, which makes it quick to spot where the natural ranking is letting you down. Because the pinned items are stored as regular posts, they go through the same editorial workflow as anything else: drafts, revisions, scheduled publishing.
This feature has no public PHP API to speak of; it is meant to be driven from the dashboard. If you need to pin programmatically you can create the ep-pointer posts yourself. The important meta key is pointers, an array of post IDs with their order:
$pointer_id = wp_insert_post( [
'post_type' => 'ep-pointer',
'post_title' => 'pricing', // the search term
'post_status' => 'publish',
] );
update_post_meta( $pointer_id, 'pointers', [
[ 'ID' => 1532, 'order' => 1 ],
[ 'ID' => 1601, 'order' => 2 ],
] );The same area of the plugin is where Search Ordering used to live before it was folded in, so if you see that name in older documentation, it is this.
Autosuggest
Autosuggest adds a dropdown of matching posts under the search box as the visitor types. Unlike every other feature here, it runs in the browser: a small script watches the input field, debounces keystrokes, and sends the query straight to Elasticsearch over HTTP. That is why it feels instant, and it is also why it needs a little care to set up.
Your Elasticsearch endpoint has to be reachable from the public internet, with CORS allowing your domain, and it must only accept read requests from anonymous users. If you are on ElasticPress.io this is handled for you. Self-hosters usually put a small proxy in front of the cluster that only forwards _search requests to the right index. Do not expose an unprotected cluster; people will find it.
Out of the box the script attaches to any input with the class .ep-autosuggest, input[type="search"], or .search-field. If your theme uses something else, add the selector in the feature settings. You can also change which post types appear and how many results to show. The suggestions use the same Weighting configuration as Post Search, so a title-heavy weighting gives you title-heavy suggestions.
Two filters cover most customisation. ep_autosuggest_options adjusts the JavaScript configuration, and the query itself can be shaped with ep_autosuggest_query on the PHP side (the plugin generates a template query and caches it):
add_filter( 'ep_autosuggest_options', function ( $options ) {
$options['selector'] = '#masthead .search-form input';
$options['triggerAnalytics'] = true;
return $options;
} );On the front end, each suggestion item is rendered by a JavaScript template you can override with window.epas.itemTemplate or by listening to the ep.Autosuggest.itemHTML event if you want to show a thumbnail or price alongside the title. The companion Instant Results feature goes further and replaces the whole results page with a modal, but Autosuggest on its own is the lighter touch and is what most content sites want.
WooCommerce
WooCommerce stores are where default WordPress queries hurt most. Product archives filter on taxonomies and meta at the same time, the admin order list searches across several tables, and every one of those operations gets slower as the catalogue grows. The WooCommerce feature in ElasticPress routes a good chunk of that through Elasticsearch, and the plugin keeps compatibility with the last two major WooCommerce releases.
With it enabled, the following go through Elasticsearch:
- Shop and category pages. Product listings, including sorting by price, popularity and rating, come from the index. Attribute filtering (size, colour) is handled as a tax query and works with the Filters feature out of the box.
- Product search. SKUs, variation SKUs, attributes and product categories are all indexed and searchable, so a customer can paste a part number and get the product. Variations are flattened into the parent so you do not get five identical results for one t-shirt.
- Admin order search. Searching orders in the WooCommerce admin (by customer name, email, billing address, order items) is run against the index, which turns a 20-second query on a busy store into a sub-second one. With HPOS enabled this uses a dedicated orders index.
- Admin product list. Combined with Protected Content, the product list in the dashboard also uses Elasticsearch, including the search box and category dropdown.
Everything Woo-specific that matters for ranking is in the Weighting screen under the Products post type, so you can push SKU matches to the top or give the short description more weight than the long one. Prices and stock status are indexed as meta, which means a price-range slider is a Filter by Meta Range block pointed at _price, no custom code required.
One thing people regularly want is to hide out-of-stock products from search without hiding them from the catalogue (or the other way round). That is a query filter away:
add_filter( 'ep_post_formatted_args', function ( $formatted_args, $args ) {
if ( empty( $args['s'] ) || ( $args['post_type'] ?? '' ) !== 'product' ) {
return $formatted_args;
}
$formatted_args['post_filter']['bool']['must'][] = [
'term' => [ 'meta._stock_status.raw' => 'instock' ],
];
return $formatted_args;
}, 10, 2 );The ep_post_formatted_args filter is the escape hatch for anything the higher-level options do not cover; it hands you the raw Elasticsearch query body just before it is sent. It is worth pairing it with the Query Log in the ElasticPress Status menu, which records every request and response so you can see exactly what is going over the wire. Between the two, there is very little you cannot adjust.
A few practical notes before you switch it on
Run the first sync from WP-CLI, not the browser, on anything bigger than a hobby site. The dashboard sync works, but a browser tab is a fragile place to leave a process that might take an hour.
Only index the meta you actually need. The default is to index nothing unless it is used in Weighting or whitelisted, which is the right default; indexing every meta key on a WooCommerce site produces enormous documents and mapping explosions.
Some features change the mapping. Did You Mean does, and so does enabling or disabling the WooCommerce feature. The dashboard will tell you when a full resync is required; believe it.
If Elasticsearch goes down, ElasticPress falls back to MySQL for search rather than breaking the site. It is slower and less relevant, but visitors still get results. That graceful failure is one of the underrated reasons to use this plugin rather than rolling your own integration.
Put together, the features above cover most of what a site needs from search: relevant results, forgiveness for typos, ways to narrow things down, suggestions while typing, editorial control over the top spots, and a store that stays responsive as it grows. The fact that you can turn each of them on from a settings page, and still reach into the raw query when you need to, is what makes ElasticPress the standard choice for WordPress sites that have outgrown the default.
