Drupal Tutorial: How to Sort Search Results by Content Type
For a recent website I worked on, the client wanted search results to be grouped by content type, and also wanted to control which content type results were displayed first, second, and third. While you can get most of the way there in terms of sorting search results using Views, if the desired order of the content types doesn't luckily correspond to some kind of alphabetical sorting of the content type's names, you'll need to do a little configuration and custom module work to make this happen.
I will walk you through how to do this using a vanilla Drupal 7 site. (I used Devel to generate dummy content). I also created two additional content types called Checklists and Toolkits, so we'd have more than just the default Basic Page and Article content types that come out of the box with Drupal. You'll also want to download and enable the Views and Views UI modules.
To accomplish this, we will do the following:
- Create a new view for better control of how the search results are displayed
- Create an exposed filter for our view to use for the actual search
- Display our custom search form in a block
- Hook into the view's display to re-order the search results per ranked content type
Part 1: Setting up our View
Create a new View - I've named mine Search Results. We'll want to show Content of type All, and create a Page.
Click Continue and Edit to complete the View setup. This View will be the page that site visitors land on when they enter search terms into the new search bar we are going to create shortly. You can chose to display whatever fields you'd like here, but for this simple case we'll just show the Content: Title, Content: Body, and Content: Type. You can hide the Content: Type later but it's useful to show it now while we're working so we'll know that future work will be sorting the results as desired. Also remove any Sort Criteria that were created automatically by Views.
I've linked the title to the original content and trimmed the body field so it's not too long, as well as added an ellipsis and read more link.
Once you've got your fields set up the way you'd like, you should see something like this - right now it's just displaying all content on the site, without any sorting.
2. Create exposed filter for Search
Next we'll create an exposed filter and place it in a block. We'll replace the default Drupal Search block with our custom exposed filter after we've done this.
Under Filter Criteria, click the Add button and then select Search: Search Terms.
Exposed this filter to users, and click the box next to 'remove search score'. Save.
Rather than including this exposed filter on this search page only, we want this exposed filter to be available in a block, so we can use it as our default search. Under Advanced, click Exposed Filter in block: No
Select Yes, and then save. At this point your exposed filter will disappear from the Views preview below, as it is no longer part of the page, but is in a separate block. Save your view and then head over the the Block configuration.
3. Replace Drupal's default search block with our new exposed filter
Under Structure > Blocks, set the Search form block to -None-, and set your new exposed filter block to display in whichever region you'd like. In this case I'll just put it in the Sidebar first.
If you visit your home page, you should see your new search form. Test it out and make sure if you enter a term you get sent to the custom search results page we just built with views.
You'll see it's returning results fairly randomly, with the different content types all mixed in with each other. If you don't care what order the content types are ordered by, you could use the built in Views Sort Criteria to sort by content type. But that's kind of limiting - it only lets you chose ascending or descending. So your choices right now are either Ascending:
Article
Basic Page
Checklist
Toolkit
or Descending:
Toolkit
Checklist
Basic Page
Article
Let's say we needed to return our search results in the following order:
Checklist
Toolkit
Article
Basic Page
We'll need a custom module for that.
4. Hook into the view's display to re-order the search results per content type
We'll use hook_views_pre_render() to accomplish this. First, let's take a look at what's going on under the hood for this hook. So, create yourself a custom module - I've called mine search_customization, and create the necessary .info and .module files. Enable your module, and then we'll add our first statement to the .module file. I'm using the Devel module to dpm() out variables for display on the screen.
/** * Implements hook_views_pre_render(). * * Using a view to display search results, group search results by content type * and also order results per content type ranking. */ function search_customization_views_pre_render(&$view) { dpm($view, 'view'); }
Head back to your website, clear cache and refresh the page to see the view object. First, we'll only want to modify our search View, so let's get the View's name.
Modify the code so it will only be run when this specific view is loaded on the page
function search_customization_views_pre_render(&$view) { if ($view->name == 'search_results') { dpm($view, 'view'); } }
Here's the rough outline of what we're going to do next:
- Get the result of the view
- Set up some kind of ranking for the content types
- Group the results per content type
- Sort the grouped results per our ranked content types
- Set the views results reordered per our ranked results
Go back to the website and look for the view->result in your dpm() statement. It will list all of the results that found.
In our custom module we'll get the results array and initialize an empty array. We'll use this empty $grouped_results array later to save our results grouped by content type.
function search_customization_views_pre_render(&$view) { if ($view->name == 'search_results') { // Get array of results from view object. $results = $view->result; // Initialize empty array that we will use to save results // grouped by content type. $grouped_results = array(); dpm($view, 'view'); } }
If you inspect any of the individual results in view->result, you'll see they're each objects that have all of the information we included in our custom view - the title, content type, and body.
Now we'll loop through the $results array using a foreach, and each time an individual result object in our $results arrays is encountered, we will get the content type of that result. We'll place that result in our $grouped_results array, which groups results per the ranking of the content type. A little confusing to explain, so here's the code:
function search_customization_views_pre_render(&$view) { if ($view->name == 'search_results') { // Get array of results from view object. $results = $view->result; // Initialize empty array that we will use to save results // grouped by content type. $grouped_results = array(); // Each $result is an object within the $results array. foreach ($results as $result) { $content_type = $result->node_type; // Rank content types in order to be sorted. // Ranking closest to 1 will be listed first. // Ranking with highest number will be listed last. switch ($content_type) { case 'checklist': $ranking = 1; break; case 'toolkit': $ranking = 10; break; case 'article': $ranking = 20; break; case 'page': $ranking = 30; break; default: $ranking = 200; } // Add result to new array where results are grouped by ranking. $grouped_results[$ranking][] = $result; } dpm($view, 'view'); } }
If we print out our $grouped_results array, hopefully this will be more clear - I did this via dpm($grouped_results)
. You'll see that this array now has 4 rows, one for each of the rankings we assigned to the 4 different content types. And that each of these rows is actually an array of results objects.
However, the rankings aren't ordered per ranking number. We'll do that next - reorder the $ranked_results array per our ranking with a simple ksort.
function search_customization_views_pre_render(&$view) { if ($view->name == 'search_results') { // Get array of results from view object. $results = $view->result; // Initialize empty array that we will use to save results // grouped by content type. $grouped_results = array(); // Each $result is an object within the $results array. foreach ($results as $result) { $content_type = $result->node_type; // Rank content types in order to be sorted. // Ranking closest to 1 will be listed first. // Ranking with highest number will be listed last. switch ($content_type) { case 'checklist': $ranking = 1; break; case 'toolkit': $ranking = 10; break; case 'article': $ranking = 20; break; case 'page': $ranking = 30; break; default: $ranking = 200; } // Add result to new array where results are grouped by ranking. $grouped_results[$ranking][] = $result; } // Sort results by ranking. ksort($grouped_results); dpm($view, 'view'); dpm($grouped_results, 'grouped_results'); } }
Now if we inspect the $grouped_results array the results are ordered per ranking.
Now that our results are grouped and ranked by result, we'll want to pull those results objects out of their groupings and save them as an un-grouped array (still ordered by ranking) that we will use to override the default views results. We'll make a new empty $ranked_results array to save our ranked results. We'll then use a foreach loop to go through each of the ranking arrays in our $grouped_results. Within our ranking arrays, we'll use a second foreach loop to save the results object into our new $ranked_results array. Again, super confusing to explain, so here's the code and we'll dpm() out some stuff to make this more clear.
function search_customization_views_pre_render(&$view) { if ($view->name == 'search_results') { // Get array of results from view object. $results = $view->result; // Initialize empty array that we will use to save results // grouped by content type. $grouped_results = array(); // Each $result is an object within the $results array. foreach ($results as $result) { $content_type = $result->node_type; // Rank content types in order to be sorted. // Ranking closest to 1 will be listed first. // Ranking with highest number will be listed last. switch ($content_type) { case 'checklist': $ranking = 1; break; case 'toolkit': $ranking = 10; break; case 'article': $ranking = 20; break; case 'page': $ranking = 30; break; default: $ranking = 200; } // Add result to new array where results are grouped by ranking. $grouped_results[$ranking][] = $result; } // Sort results by ranking. ksort($grouped_results); dpm($grouped_results, 'grouped_results'); // Now initialize new array that will save search results in order per ranking // but without results being grouped by ranking. $ranked_results = array(); // Reorganize $grouped_results so $results are no longer grouped // just listed in order of ranking. foreach ($grouped_results as $ranking) { dpm($ranking, 'ranking'); foreach ($ranking as $result) { $ranked_results[] = $result; } } dpm($ranked_results, 'ranked_results'); dpm($view, 'view'); } }
Refresh the page and let's take a look at the arrays that we're dealing with now. You'll see in addition to our $grouped_results array, we now have 4 $ranking arrays. This comes from the first foreach loop, where we went through our $grouped_result array, that has 4 arrays, each array being results objects grouped by ranking per the content type.
In our next foreach loop we are taking each of the results objects from our ranking array and placing them into a new $ranked_results array. This simply serves to take the results out of 4 different arrays, each array containing results of a different content type, and puts them into a new array where results aren't grouped by content type ranking, but are now ordered per the ranking. You can see this in the $ranked_results array - this is now a simple array of results, ordered per content type ranking. This is instead of the $grouped_results array, which was an array of arrays.
Now we just have one last thing to do. Our $view->results array has the results not ordered per content type ranking. We will set the $view->results array to our new $ranked_results array and we'll be done! Here is the final code (of course you will want to remove the dpm() statements for your real module).
function search_customization_views_pre_render(&$view) { if ($view->name == 'search_results') { // Get array of results from view object. $results = $view->result; // Initialize empty array that we will use to save results // grouped by content type. $grouped_results = array(); // Each $result is an object within the $results array. foreach ($results as $result) { $content_type = $result->node_type; // Rank content types in order to be sorted. // Ranking closest to 1 will be listed first. // Ranking with highest number will be listed last. switch ($content_type) { case 'checklist': $ranking = 1; break; case 'toolkit': $ranking = 10; break; case 'article': $ranking = 20; break; case 'page': $ranking = 30; break; default: $ranking = 200; } // Add result to new array where results are grouped by ranking. $grouped_results[$ranking][] = $result; } // Sort results by ranking. ksort($grouped_results); dpm($grouped_results, 'grouped_results'); // Now initialize new array that will save search results in order per ranking // but without results being grouped by ranking. $ranked_results = array(); // Reorganize $grouped_results so $results are no longer grouped // just listed in order of ranking. foreach ($grouped_results as $ranking) { dpm($ranking, 'ranking'); foreach ($ranking as $result) { $ranked_results[] = $result; } } dpm($ranked_results, 'ranked_results'); // Set views results array to new ranked results array. $view->result = $ranked_results; dpm($view, 'view'); } }
Refresh the page and you can see the results are now ordered per our ranking: checklists, then toolkits, then articles, then basic pages.