Hxppy Thxxghts

Hxppy Thxxghts Search Project

In this feature, I’ll explain how I created the live search for this website. I see programming and web development as an art form which, when honed and refined, can delivery beautiful results.

Here’s a rough sketch of what’s going on every time you type something into the search popup:
Javascript sends the query via AJAXCustom REST endpoint intercepts queryRelevanssi processes request ⟶ Results are cleaned up ⟶ WordPress sends JSON object back to client ⟶ Javascript parses results and inserts them into the DOM

I’m going to skip right over how WordPress natively handles searches and dive into the goods.

Let’s Break it Down

AJAX Search Request

Everything starts with the events triggered when someone types something into the search box. We intercept the keyup event, make sure the contents have changed since the last search, and fire off the search via jQuery.get(). The receiving end is a custom REST endpoint defined in WordPress.

/* ---------------------- LIVESEARCH3 */
jQuery(function ($) {

  $('.livesearch3').each(function () {
    //console.log('init')
    var $livesearch = $(this),
        $search = $livesearch.find('input[type=search]'),
        $results = $livesearch.find('.results'),
        $resultslist = $results.find('.s-list'),
        $error = $results.find('.error'),
        data = {
          s: null,
        },
        datatype = 'json';

    var fire = function (terms) {
      data.s = terms;

      $.get({
        url: livesearch3.url + '?' + $.param( data ),
        dataType: datatype,
        beforeSend: bscallback,
        cache: true	// or set false if you have live data (doesn't guarantee freshness tho)
      })
        .done( function ( response ) {
        //console.log(response);
        var html = getresults( response );
        putresults( html );
      } )
        .fail( function ( xhr, status, err ) {
        //console.log( arguments );
        if ( xhr.status === 404 ) {
          if ( $search.val().length < 3 ) {
            $error.text( 'enter 3 or more characters' );
          } else {
            $error.text( 'i got nothin\'' );
          }
        } else if ( xhr.status === 400 ) {
          $error.text( 'search term is blank or something' );
        } else if ( xhr.status > 499 ) {
          $error.text( 'i am broken' );
        }
      })
        .always( function ( response ) {
        $results.removeClass( 'loading' );
      });

    };

    /* THE CODE FOR HANDLING THE DATA AND DOM MANIPULATION IS IN A SECTION BELOW */

    // the WordPress REST API will automatically verify nonce in this header
    var bscallback = function (xhr) {
      xhr.setRequestHeader('X-WP-Nonce', livesearch3.nonce);
    };

    // event handlers
    var debouncetimeout;
    var keyupcallback = function (evt) {
      //console.log( [ arguments.callee, evt ] );
      var val = $(this).val();

      if ( !isbrandnewinfo(val) ) return;

      clearTimeout(debouncetimeout);

      debouncetimeout = setTimeout(function () {
        $error.empty();
        $results.addClass( 'loading' );
        fire(val);

      }, 300);
    }

    // helpers
    var oldval;
    var isbrandnewinfo = function (val) {
      //console.log( [ arguments.callee, val, oldval ] );
      if (val == oldval) return false;
      oldval = val;
      return val;
    }


    //setup
    $search
      .attr('autocomplete', 'off')
      .on('keyup change', keyupcallback);

    
    // auto debug actions
    //$search.val('test');
    //setTimeout(function(){$search.trigger('change');}, 2000);
  });
});

REST Endpoint Processes Request

The custom endpoint was necessary because we’re using Relevanssi to generate search results for greater accuracy – the WordPress default search endpoint would probably be faster and less work but less accurate.

function livesearch3_register_search_route() {
  register_rest_route('livesearch3/v1', '/search', [
    'methods' => WP_REST_Server::READABLE,
    'callback' => 'livesearch3_search',
    'args' => livesearch3_get_search_args()
  ]);
}
add_action( 'rest_api_init', 'livesearch3_register_search_route');

function livesearch3_search( $request ) {
  $s = $_REQUEST[ 's' ];
  $s = sanitize_text_field( $s );
  if ( ! $s ) return new WP_Error( 'front_end_ajax_search', 'No search term(s) specified', array( 'status' => 400 ) ); // no search term is a bad request
  $q = livesearch3_query( $s );
  $p = livesearch3_posts( $q );
  if( empty($p) ) return new WP_Error( 'front_end_ajax_search', 'No results', array( 'status' => 404 ) ); // we got nothin

  return rest_ensure_response( $p );
}


// create query, return results
if (!function_exists('livesearch3_query')) {
  function livesearch3_query( $search_data ) {
    $args = [
      's' => $search_data, 
      'posts_per_page' => 7,
      'post_types' => [ 'post', 'product' ]
    ];
    $q = new WP_Query();
    $q->parse_query( $args );
    relevanssi_do_query( $q );
    return $q;
  }
};

We also do a little extra processing to clean up the results returned by Relevanssi, removing data we want to hide from the client and adding the thumbnail URL.

// extract posts, filter to needed fields
if (!function_exists('livesearch3_posts')) {
  function livesearch3_posts( $query ) {
    $fields = [ 'post_title', 'post_date' ];
    $return = [];

    foreach($query->posts as $post) {
      $newPost = [];
      foreach($fields as $field) {
        $newPost[$field] = $post->$field;
      }
      // add perma
      $newPost['link'] = get_post_permalink($post);
      // add thumb
      $newPost['thumbnail'] = get_the_post_thumbnail( $post, 'thumbnail' );
      $return[] = $newPost;
    }
    return $return;
  }
};

Return JSON Search Results to Client

This is why I freaking LOVE WordPress. With one line, I can take a PHP object and send it back to the client like so:

return rest_ensure_response( $p ); // $p = the array of post data (the search results)

Parse JSON Results, Add to DOM

The beauty of JSON is that Javascript can parse it and build on the existing DOM in the blink of an eye. In our case, we just need to parse each post for a few things: 1. image; 2. post title; 3. the link. I think there are probably better (and definitely more understandable) ways to write this, but my solutions was: create an HTML template, cycle through the returned results, and generate a chunk of HTML that can get slapped into our search results container.

var getresults = function ( data ) {
  var html = [
    '<li class="small-post content_out clearfix"><div class="thumb hide-thumb"><a href="',
    null, // link
    '">',
    null, //thumb
    '</a></div><div class="post-c-wrap"><h4 class="title"><a href="',
    null, //link again
    '">',
    null, //title
    '</a></h4></div></li>'
  ];
  var output = '';
  $.each( data, function ( key, post ) {
    html[1] = html[5] = post.link;
    html[3] = post.thumbnail;
    html[7] = post.post_title;
    //console.log( [post, html] );
    output += html.join('');
  } );
  return output;
};

var putresults = function ( html ) {
  $resultslist.html( html );
};

Done!

In the end, these efforts didn’t revolution the Web, they simply change us over from a click and wait model to a type and see results model. A normal search will take 1 – 3 seconds. This gets results between about 45ms and 1000ms, and we can do it on a budget. Thanks for reading. I’m happy to take suggestions to improve this further! The following section is not required reading, but if you’re really trying to understand all the technology here, this might be helpful.

A full round-trip looking for ‘love’ took 10.62ms (while signed in as an admin).

What’s Not Covered

There is one more piece that is crucial to the speed achieved. I mentioned it in the video. I implemented a Must-Use plugin which runs before all other plugins load and filters the plugins that initialize during the AJAX/REST request to just the essentials: Relevanssi and Code Snippets. We could even eliminate Code Snippets if we put our code in functions.php of our theme, but for development and portability purposes, I like using this plugin to manage bits of PHP.

Since this Must-Use plugin is not directly tied to the topic at hand, I’ll plan to release another article in the future to cover that.

The Nitty Gritty

Here, I’ll dive deeper into all the stuff that came together to make this possible, essentially turning a 4 month project into an overnight project. Big up to the thousands of developers who made contributions.

Technologies

  • WordPress (WP)- the web publishing platform of choice
  • jQuery – built into WordPress, wonderful for creating event listeners, firing AJAX requests, and DOM manipulation
  • WordPress REST API – a very sensible and data-conservative way to talk to the database
  • Code Snippets – allows us to manage PHP scripts just like we do WP plugins
  • Relevanssi Plugin – awesome plugin to enhance on-site search features of any WP website
  • I would be here all day if I documented everything, including but not limited to Apache web server, Cent OS Linux distro, WHM and cPanel, the Chromebook I use for all development, and so on, but you’re in my thoughts.

Teachers, Tutorials, Code

Leave a Comment

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

Hxppy Thxxghts
0
    0
    Your Cart
    Your cart is emptyReturn to Shop
    Scroll to Top