How to Make WordPress Search Only Titles of Posts Instead of Their Whole Content

Share

By default, searching on a WordPress website, whether in the admin area, in the editor for linking, or in the public part of the website, will search the title of the post, the excerpt of the post, and the content of the post. There are various filters in WordPress to customize how this works, like post_search_columns, wp_link_query_args, and rest_post_search_query. We can use them to disable searching the content of the article and only allow searching the titles, for example.

An example:

/**
 * Restricts search functionality to searching by post title.
 */
function virtualcuriosities__post_search_columns($columns, $search, $query) {
    return ['post_title'];
}

add_filter('post_search_columns', 'virtualcuriosities__post_search_columns', 10, 3);

The post_search_columns hook is used to filter which columns in the wp_posts SQL table to search through. The filtered value is the first argument, $columns.

By default, $columns will be ['post_title', 'post_excerpt', 'post_content'], which means WordPress will search for a given query in title, excerpt, or content. We can restrict this functionality by returning an array with any combination of these three column names. Above, we returned ['post_title'], so only wp_posts.post_title will be searched.

Caveat: Empty Search Columns

If you look at the source code where this hook is called, you'll see that WordPress undos the filter if we remove all columns, from wp-includes/class-wp-query.php:

$search_columns = (array) apply_filters( 'post_search_columns', $search_columns, $q['s'], $this );
// Use only supported search columns.
$search_columns = array_intersect( $search_columns, $default_search_columns );
if ( empty( $search_columns ) ) {
    $search_columns = $default_search_columns;
}

This means we can't use a function like array_intersect without checking first whether we'll end up returning an empty array, since if WordPress sees that, it will undo all our work.

Full Search for Admin-only

We can use the $query argument to conditionally allow full search for admin area, while allowing only search by post_title in public areas:

function virtualcuriosities__post_search_columns($columns, $search, $query) {
    if($query->is_admin) {
        return $columns;
    } else {
        return ['post_title'];
    }
}
add_filter('post_search_columns', 'virtualcuriosities__post_search_columns', 10, 3);

Disable Full Text Search for Inserting Links in Gutenberg Only

You may have noticed that when trying to add a link in Gutenberg, or even in TinyMCE, Gutenberg will search through the contents of all your posts, which gets kind of annoying when quickly. We could solve this by disabling full text search in general, but then we will never be able to do full text search in the admin area. We could find our published posts through Google, but Google wouldn't be able to find our drafts, so it would be better if we could remove this functionality from that insert link box specifically.

Making this work in a bit more complicated than I expected, as WordPress has two different API's for this, one modern and one outdated, and both are still used by default.

First, before Gutenberg, we could search for links in a TinyMCE editor. It's possible that some plugins use this classic editor for their fields. Its link search uses an AJAX API that can be customized server-side through the wp_link_query_args filter. Similarly, in media pages, there's that description box with blue buttons for making words bold and linking that just add HTML code. That one also uses this API.

Second, on Gutenberg, the API used is completely different: it's the REST API. As far as I know, there is no way to identify that a query came from Gutenberg as opposed to another possible REST client. It could be you could guess that from the Referrer HTTP header, but that's hacky at best. To filter Gutenberg, we'll have to change how all REST requests work. At least if we're doing it from the PHP side. On Javascript, it might be possible to change what data Gutenberg sends when it does REST requests, and we could always make our own link plugin. But this is all too complicated for a small article like this.

One problem: $query->is_admin isn't set when searching the REST API even from Gutenberg. I guess because this API is public. This means that if we wanted to search only titles when searching for links, but we wanted to allow public users to search excerpts, we would need some extra code in the other filter as well!

/**
 * Restricts public search functionality to
 * searching by post title and excerpt.
 */
function virtualcuriosities__post_search_columns($columns, $search, $query) {
    if($query->is_admin) {
        return $columns;
    } else {
        $allowed_columns = ['post_title', 'post_excerpt'];
        $result = array_intersect($columns, $allowed_columns);
        if(empty($result)) {
            $result = $allowed_columns;
        }
        return $result;
    }
}

/**
 * Restricts search functionality in insert link box in editors
 * to searching by post title only.
 * This doesn't work for Gutenberg, only for searching links in 
 * TinyMCE and in the basic HTML editor that shows up by default
 * in the media pages.
 */
function virtualcuriosities__wp_link_query_args($query) {
    $query['search_columns'] = ['post_title'];
    return $query;
}

/**
 * Restricts search functionality in insert link box in Gutenberg
 * to searching by post title only.
 */
function virtualcuriosities__rest_post_search_query($query_args, $request) {
    if($request->get_route() === '/wp/v2/search') {
        $query_args['search_columns'] = ['post_title'];
        return $query_args;
    }
    return $query_args;
}

add_filter('post_search_columns', 'virtualcuriosities__post_search_columns', 10, 3);
add_filter('wp_link_query_args', 'virtualcuriosities__wp_link_query_args', 10, 1);
add_filter('rest_post_search_query', 'virtualcuriosities__rest_post_search_query', 10, 2);

You should probably create your own API if you want more customization is how searching works.

More Complex Cases

It's possible that you could use these filters to implement something more complex, like creating your own search operators, e.g. checking if the search query starts with title:, and if it does, remove that from the search query and filter the search by title posts only.

This would be possible in wp_link_query_args because the searched text is available through $query['s']. It wouldn't be possible in post_search_columns, since this filter only changes the columns, and can't change the text being searched for.

Technical Notes

By default, WordPress doesn't create SQL indexes on any of the text columns in wp_posts. This means any query involving text fields performs a full table scan, which becomes linearly slower as there are more articles (and even revisions) added to the table.

Note that WordPress does use indexes in other columns, it's just the text columns that don't have them.

The reason for this is that the simplest index you can create is a binary tree (btree). Binary trees speed up performance by being sorted. They can optimize a LIKE search in a text column, so long as the pattern means "titles that START with this word." When it comes to "titles that have this word anywhere, including the middle of the title," a btree won't be able to help at all, and a more specialized index will be necessary.

Such specializes indexes do exist for MySQL, but WordPress doesn't make use of them. Indexes save costs when you search a lot, but updating the index has its own costs. Perhaps WordPress figured any website large enough to have problems with search would either disable search or implement a solution through a third-party plugin, and they felt it wouldn't be a good idea to create an index by default for everyone.

In any case, disabling search for post's content is likely to improve performance, not just because it's fewer columns to search through, but also because the post's title is just much smaller than its content, so it's less text to search through. On the other hand, it may also make search a lot less useful for your users. If you think just the title isn't enough, perhaps you could include 'post_excerpt' to the returned array. It's an extra column, but excerpts are very short as well, so it shouldn't be much of a problem.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *