Predicate Builder

In many of the Sitecore 7 Hangouts and various presentations, they show how to query either Solr or Lucene using a simple POCO (Plain Old CSharp Object) object. This is a great approach to querying. It allows us to use an object orientated approach and has changed the way we go about accessing data from our indexes, similar to what the Custom Item Generator did for us some years back in how we access an item’s fields. The question is, how do you iterate over the parameters in a querystring (#q/activitylevel=Level 2&price=3000&continentnames=Europe,Asia) and match the key to a property on your POCO object? How do you dynamically build an index query to represent the many layers of possible search clauses? This is where the predicate builder becomes valuable.

public class TourResultItem : SearchResultItem
{
	[IndexField("Title")]
	public string Title { get; set; }

	[IndexField("ActivityLevel")]
	public string ActivityLevel { get; set; }

	[IndexField("BadgeImageUrl")]
	public string BadgeImageUrl { get; set; }

	[IndexField("ContinentNames")]
	public string ContinentNames { get; set; }
}

We use the predicate builder on our search page. When the page loads or when you initiate a search, it eventually hits one of our classes called the TourSearchContext. This class is responsible for building out the query, applying sorting, pagination, executing the query, and returning the results to the requester. Below, I have added a small snippet of code that shows how we are using the predicate builder. As you can see, we are still passing in our POCO object (which is of type TourResultItem) to the search provider. This tells the provider to return its result set as that object, or a list of those objects.

As we look into the foreach statement, we do some simple checks to verify the parameter and begin iterating over the parameters values. Just above we instantiate the innerPredicate which is allowing us to build out a where clause for that specific parameter. If that parameter has multiple values, they will be separated by an OR statement. When it is added back to the search object it is appended onto its where clauses as an AND query.

If we were to look at the parameters I used above, the query to Sitecore’s search provider would look something like this (if we ran it through the code below):
.Where(activitylevel == Level 2)
.Where(price == 3000)
.Where(continentnames == Europe OR continentnames == Asia)

ISearchIndex index = ContentSearchManager.GetIndex(IndexName);
using (IProviderSearchContext context = index.CreateSearchContext())
{
	//build query
	IQueryable<TourResultItem> search =
		context.GetQueryable<TourResultItem>(
			new CultureExecutionContext(CultureInfo.GetCultureInfo(Sitecore.Context.Language.Name)));
	search = search.Where(x => x.Language == Sitecore.Context.Language.Name);

	//filter by parameter values
	foreach (SearchParameter parameter in SearchOptions.Parameters)
	{
		//verify item
		if (string.IsNullOrEmpty(parameter.Id) || !parameter.Values.Any())
		{
			continue;
		}

		Expression<Func<TourResultItem, bool>> innerPredicate = PredicateBuilder.True<TourResultItem>();
		foreach (string val in parameter.Values)
		{
			if (string.IsNullOrEmpty(val))
			{
				continue;
			}

			string parameterValue = val;
			innerPredicate = innerPredicate.Or(i => i[(ObjectIndexerKey) parameter.Id] == parameterValue);
		}

		//add to master predicate (and statements)
		search = search.Where(innerPredicate);
	}
}

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s