Sitecore 7 Search - a quickstart guide

A real world example of the new functionality

Hints and tips for using Sitecore's powerful new tools

This is a technical article aimed at developers working with Sitecore. If you are a Marketer, looking for information on Sitecore, here are our recent pieces on Sitecore 7.5 and Sitecore 8 or please contact us.   

Over the past year I've been working extensively with Sitecore 7 and its new search capabilities. To start with, I tried some of the examples from the Sitecore 7 Development Blog and downloaded the Autohaus example project from GitHub (which I'd highly encourage you to do). After copying, pasting and some prototyping, I eventually pieced together the code I needed. To help you out, I've put together a real world example so that you can get up to speed quickly with this new functionality.

This example is for a people search. Not the most obvious of examples I hear you say, but I think it deals with the main points you'll need to get up and running with Sitecore 7 Search.

The example assumes you already have your Sitecore 7 instance running, be it with regular Lucene or SOLR, and the fields you need to query are already in your search index. For these examples I've created some custom "Person" Sitecore items that will hold person information in the site. Here are the fields I used for the person Sitecore item:

Sitecore 7 Search 1

The Basics

Firstly, I put together a Person class that will hold the properties of the person. This entity can be used to map fields in your index to strongly typed properties using the IndexField attribute. If you are familiar with the concept of POCO entities, these are just Sitecore 7 equivalents of these. In this example I have created my own class for handling the mapping - you can use the Sitecore.ContentSearch.SearchTypes.SearchResultItem if the data you are searching is generic.  The IndexField attribute names must match the name of the fields in your index.


This class also contains a Fields property. I've set this up so that the properties of the class can be accessed with an indexer, which is useful if you need to get access to the fields dynamically using an Aggregate statement in Linq.

using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Sitecore.ContentSearch;
using Sitecore.ContentSearch.Converters;
using Sitecore.ContentSearch.SearchTypes;
using Sitecore.Data;

namespace Coreblimey.Business
{
    public class Person
    {
        [IndexField("firstname_t")]
        public string Firstname { get; set; }
        [IndexField("surname_t")]
        public string Surname { get; set; }
        [IndexField("role_sm")]
        public IEnumerable<string> Role { get; set; }
        [IndexField("_template")]
        [TypeConverter(typeof(IndexFieldIDValueConverter))]
        public ID TemplateId { get; set; }
        private readonly Dictionary<string, object> _fields = new Dictionary<string, object>();
        public Dictionary<string, object> fields
        {
            get { return _fields; }
        }

        public string this[string key]
        {
            get
            {
                if (key == null)
                {
                    throw new ArgumentNullException("key");
                }

                return (string)this.fields[key.ToUpperInvariant()];
            }

            set
            {
                if (key == null)
                {
                    throw new ArgumentNullException("key");
                }

                fields[key.ToUpperInvariant()] = value;
            }
        }

    }


Sitecore 7 Search allows you to use Linq to perform your search queries and uses the IQueryable<T> interface. Let's plug the Person Entity class into some Linq and start using the IQueryable.

using (var context = ContentSearchManager.GetIndex("sitecore_web_index").CreateSearchContext())
{
     IQueryable<Person> query = context.GetQueryable<Person>().Where(p=> p.Firstname.Equals("John"));
}

So with a single line of code I've set up an IQueryable<Person> where the firstname is John. Well, apart from the using statement; - any Sitecore 7 searching has to be done inside a context.

Note, I haven't actually called upon the index yet; this is the next step...

query.GetResults();


One line again! Sitecore 7 gives you the GetResults() method on the IQueryable. This will then translate the predicates, that you specified using Linq, into actual query language that can be executed on your index. The GetResults() method saves you a lot of time and creates an object that contains all the things you need for search results rendering e.g. total number of results, relevancy scoring of items, all of which are invaluable when you start adding paging and sorting to your searches. 

When you have called the GetResults method, take a look in the search.log file in your Sitecore data folder. In here you'll find the actual query that Sitecore uses behind the scenes to query the index. This is invaluable for debugging search queries, particularly as Sitecore hides a lot of the complexities of searching. The query output in here can be pasted into Luke if your using Lucene or the SOLR admin Query tester if you're using SOLR.

Predicate Builder

This feature is slightly hidden away, but really useful for adding dynamic values in your search. The predicate builder allows you to create more complex queries using AND and OR and it generates complex Expression trees without having to know about their inner workings.

When performing a search I only wanted to search Sitecore items with a particular template and these specific templates needed to be dynamic - cue PredicateBuilder:

var predicate = PredicateBuilder.True<Person>();
// Restrict search to limited number of templates (only person items) using an Or on the predicate
predicate = TemplateRestrictions.Aggregate(predicate, (current, t) => current.Or(p => p.TemplateId == t));
// Use filter and get an IQueryable
IQueryable<Person> query = context.GetQueryable<Person>().Filter(predicate);


Firstly I create a list of Sitecore IDs called TemplateRestrictions which correspond to Sitecore template IDs. Then I use the handy Aggregate of IEnumerable to go through each item in the list and build up a predicate.

Next I use the predicate instead of a Linq expression. Note that I'm using Filter instead of Where. The main difference between these two methods is that Filter doesn't calculate relevancy, so if you are doing any other queries the scoring will not be affected.

Faceting

// Apply facets to query
if (Facets.Any())
{
     // Go through and set up facets on the IQueryable
     query = Facets.Aggregate(query, (current, facetName) => current.FacetOn(c => c[facetName]));
}

When you facetOn an IQueryable, Sitecore will instruct the search technology to apply faceting on the fields you have selected. In the above example I've used an Aggregate again (because I like one liners - but you could use a foreach) to go through a list of index field names and call facetOn on the IQueryable

When you have executed the search using the GetResults() method, Sitecore returns a SearchResults<Person> object , which has a Hits collection and a Facets.Categories list property. You can loop through this list to get all the facet information about the selected fields.

            List<FacetCategory> facetCategories = peopleResults.Facets.Categories;
            foreach (var category in facetCategories)
            {
                outputBuilder.Append("<p>Facet Category Name: " + category.Name + "</p>");
                foreach (var value in category.Values)
                {
                    outputBuilder.Append("<p>Value: " + value.Name + "<p>");
                    outputBuilder.Append("<p>Number of results with this value: " + value.Aggregate + "<p>");
                    outputBuilder.Append("<p>****************************</p>");
                }                
            }


This makes it really easy to construct search interfaces that rely on faceting and you can give your users a great experience by telling them exactly how many of each value are in the search results.

Putting it all together...

Here's a full example of the People Search using the Person Entity a PeopleSearch business class and some code to write out the results.

Person class

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Sitecore.ContentSearch;
using Sitecore.ContentSearch.Converters;
using Sitecore.ContentSearch.SearchTypes;
using Sitecore.Data;

namespace Coreblimey.Business
{
    public class Person
    {
        [IndexField("firstname_t")]
        public string Firstname { get; set; }
        [IndexField("surname_t")]
        public string Surname { get; set; }
        [IndexField("role_sm")]
        public IEnumerable<string> Role { get; set; }
        [IndexField("_template")]
        [TypeConverter(typeof(IndexFieldIDValueConverter))]
        public ID TemplateId { get; set; }

        private readonly Dictionary<string, object> _fields = new Dictionary<string, object>();
        public Dictionary<string, object> fields
        {
            get { return _fields; }
        }

        public string this[string key]
        {
            get
            {
                if (key == null)
                {
                    throw new ArgumentNullException("key");
                }

                return (string)this.fields[key.ToUpperInvariant()];
            }
            set
            {
                if (key == null)
                {
                    throw new ArgumentNullException("key");
                }

                fields[key.ToUpperInvariant()] = value;
            }
        }

    }
}

PeopleSearch Class

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Sitecore.ContentSearch;
using Sitecore.ContentSearch.Linq;
using Sitecore.ContentSearch.Utilities;
using Sitecore.Data;

namespace Coreblimey.Business
{
    public class PeopleSearch
    {
        private static ISearchIndex _index;
        private List<ID> _templateRestrictions = new List<ID>();
        private List<string> _facets = new List<string>();

        private static ISearchIndex Index
        {
            get { return _index ??
            (_index = ContentSearchManager.GetIndex(Sitecore.Configuration.Settings.GetSetting("SearchIndex.Index"))); }
        }

        public List<ID> TemplateRestrictions
        {
            get { return _templateRestrictions; }
            set { _templateRestrictions = value; }
        }

        public List<string> Facets
        {
            get { return _facets; }
            set { _facets = value; }
        }

        public SearchResults<Person> Search(string searchTerm)
        {
             // Create search context - required for searching
            using (var context = Index.CreateSearchContext())
            {
                  // Setup a predicate builder as an easy way to build up predicate
                  var predicate = PredicateBuilder.True<Person>();
                  // Restrict search to limited number of templates (only person items) using an Or on the predicate
                  predicate = TemplateRestrictions.Aggregate(predicate, (current, t) => current.Or(p => p.TemplateId == t));
                  // Use filter and get an IQueryable
                  IQueryable<Person> query = context.GetQueryable<Person>().Filter(predicate);

                // now we can perform filter if we have a search term
                if (!string.IsNullOrEmpty(searchTerm))
                {
                    query = query.Where(i => i.Firstname.Equals(searchTerm).Boost(10) ||
                                        i.Surname.Equals(searchTerm).Boost(20));

                }

                // Apply facets to query
                if (Facets.Any())
                {
                     // Go through and set up facets on the IQueryable 
                     query = Facets.Aggregate(query, (current, facetName) => current.FacetOn(c => c[facetName]));
                }

                // Call query and return results
                return query.GetResults();
            }
        }
    }
}

Output.aspx

          // Setup template restrictions
            Sitecore.Data.ID templateId = new Sitecore.Data.ID("{4E4C2EB4-F7A5-403B-A80E-44146C66A42A}");
            PeopleSearch peopleSearch = new PeopleSearch();
            peopleSearch.TemplateRestrictions.Add(templateId);
            // Add a facet
            peopleSearch.Facets.Add("role_sm");
            // get results
            SearchResults<Person> peopleResults =  peopleSearch.Search("");

            StringBuilder outputBuilder = new StringBuilder();
            outputBuilder.Append("<p>***********Results*************************************</p>");
            outputBuilder.Append("<p>Found: " + peopleResults.TotalSearchResults + " results </p>");

            // Display hits
            foreach (var hit in peopleResults.Hits)
            {
                outputBuilder.Append("<p>Name: " + hit.Document.Firstname + " " + hit.Document.Surname + "</p>");
                if (hit.Document.Role != null)
                {

                    foreach (var role in hit.Document.Role)
                    {
                        outputBuilder.Append("<p>Role Sitecore Id:" + role + "</p>");
                    }
                }

                outputBuilder.Append("<p>****************************</p>");
            }

            outputBuilder.Append("<p>***********Facets*************************************</p>");
            // Loop through facet categories
            List<FacetCategory> facetCategories = peopleResults.Facets.Categories;
            foreach (var category in facetCategories)
            {
                outputBuilder.Append("<p>Facet Category Name: " + category.Name + "</p>);
                foreach (var value in category.Values)
                {
                    outputBuilder.Append("<p>Value: " + value.Name + "</p>");
                    outputBuilder.Append("<p>Number of results with this value: " + value.Aggregate + "</p>");
                    outputBuilder.Append("<p>****************************</p>");
                }                
            }

  lblOutput.Text = outputBuilder.ToString();

Actual Output
***********Results*************************************

Found: 5 results

Name: James Royce

Role Sitecore Id:501703e57c884ee586793e90650e28cd

Role Sitecore Id:bdf6b678a6fd43ebab6edb22b98958a4

Role Sitecore Id:332e33a9854a44628555272cd4b91ad2

****************************

Name: Kerry Davies

Role Sitecore Id:96352039bf3440d183a8fd92e14a0b72

Role Sitecore Id:332e33a9854a44628555272cd4b91ad2

Role Sitecore Id:963970baae63414da6d44777c103cff9

****************************

Name: Jackie Hunt

Role Sitecore Id:96352039bf3440d183a8fd92e14a0b72

Role Sitecore Id:ed801165611841cab084013dcbbed191

Role Sitecore Id:501703e57c884ee586793e90650e28cd

****************************

Name: Maxwell House

Role Sitecore Id:ed801165611841cab084013dcbbed191

Role Sitecore Id:501703e57c884ee586793e90650e28cd

****************************

Name: Marjory Stewart-Baxter

Role Sitecore Id:332e33a9854a44628555272cd4b91ad2

Role Sitecore Id:96352039bf3440d183a8fd92e14a0b72

****************************

***********Facets*************************************

Facet Category Name: role

Value: 332e33a9854a44628555272cd4b91ad2

Number of results with this value: 3

****************************

Value: 501703e57c884ee586793e90650e28cd

Number of results with this value: 3

****************************

Value: 96352039bf3440d183a8fd92e14a0b72

Number of results with this value: 3

****************************

Value: ed801165611841cab084013dcbbed191

Number of results with this value: 2

****************************

Value: 963970baae63414da6d44777c103cff9

Number of results with this value: 1

****************************

Value: bdf6b678a6fd43ebab6edb22b98958a4

Number of results with this value: 1

****************************

And that's it! I hope you've got something out of this and you're a bit clearer on how to get started with Sitecore 7 Search.