Razor and Faceted Search In Umbraco

A little over a year ago we had a few sites that needed faceted search of content nodes for a specific Document Type.  At the time Razor had just been added to the core and I was finding excuses to create macros in Razor.  Rather than implementing the faceted search in XSLT or straight .NET User Controls, I went straight to Razor.  However, I found using the Umbraco Razor API challenging for search so I built my own workaround that has worked well for us and our clients.  

In doing so I found some solutions to two key problems:

  • Multiple selection facets to multiple selection properties in Razor
  • Complex DynamicNode sorting using Razor

I also used this technique to help quickly add a somewhat complex filter to the North American Umbraco Users Group site.  This will be my example for this post.

nauug_wide

(Full disclosure: I'm not necessarily proposing this as a "best practice", but hope it will either help a few people or simply spark a conversation about better ways to get the same outcome (which I welcome).  Also, this was before Niels Kühnel's awesome Faceted Search with Examine demo at Codegarden '12 which I still want to figure out how to do.) 

Problem #1: Multiple selection facets to multiple selection properties in Razor

Currently each profile at the NAUUG site has a set of skills that they can select.  In the Umbraco admin this is simply content in a folder that is selected with a Multi-Node Tree Picker.  On the Search page, there is an advanced search that allows for search by skills.  Multiple skills can be selected.  If none are selected, the assumption is that all profiles will be shown that match the other filters.

This was a difficult problem to solve for me using the the standard .Where() method in the Umbraco Razor API.  After trying my best, I gave up and simply looped through the DynamicNodeList and did my own conditional check.   

This is where I ran into my first problem.  I couldn't change the DynamicNodeList to remove the items I didn't want.  I also couldn't add to an empty list.  But I could just create a List<DynamicNode> and add to that.  Sweet!

// Get your set of nodes to loop through (could be any query including Where())
var personListings = @Model.NodeById(parent).Descendants("Person");

// Place to store matched profiles
List<DynamicNode> possibleListings = new List<DynamicNode>();

// Loop through and add
foreach(dynamic item in profileListings)
{
    bool includeFromSkills = true;
    // **** Filter code goes here      

    // Include in results if matches    
    if( … // **** other checks
            && includeFromSkills )
    {
      possibleListings.Add(item);
    }
}

Now we need to actually filter the nodes.  I borrowed a technique I saw first in the XSLTSearch package from Douglas Robar to add commas around a comma separated list and the search term and do a Contains check on a string.  This means my Multi-Node Tree Picker needed to be set to CSV instead of XML and the list of filtered skills needed to be a list of node ids (comma separated). 

I've now added the param passed in, added of the commas to the filter (outside of the foreach), and the skills filtering code.

// Get params
var skills = String.IsNullOrEmpty(Parameter.Skills) ? "all" : Parameter.Skills;

// Get your set of nodes to loop through (could technically be any query)
var personListings = @Model.NodeById(parent).Descendants("Person");

// Place to store matched profiles
List<DynamicNode> possibleListings = new List<DynamicNode>();

// Ids of skills that the User has selected to filter results with
string skillsListJoined = String.Format(",{0},", skills);

// Loop through and add
foreach(dynamic item in profileListings)
{
    /////////////////////////////////////////    
    // Skills search
    bool includeFromSkills = true;
    if( skills != "all" )    {
      includeFromSkills = false;
      string profileSkills = item.GetProperty("skills").Value;
      if( profileSkills.Length > 0 )
      {      
        foreach(var skill in profileSkills.Split(','))
        {
          if( skillsListJoined.Contains(String.Format(",{0},", skill)) )
          {
            includeFromSkills = true;
            break;
          }
        }
      }   
    }    
    /////////////////////////////////////////

    // Include in results if matches    
    if( … // other checks
            && includeFromSkills )
    {
      possibleListings.Add(item);
    }
}

The skill filtering does do a little more processing than I would like (the second foreach loop), but it hasn't affected performance considerably from the implementations we have done so far.  I would recommend doing only the things you absolutely have to in the loop.  For example, don't do extra node lookups or searches that can be done outside of the top level foreach.

Problem #2: Complex DynamicNode sorting using Razor

Now imagine you want to sort by some properties on each node like "levels" or "type" with a fallback to alphabetical when the properties are equal.

In this case we're sorting by type (business/person) then alphabetical.  On other projects we have built there have been member levels and then even "auction" values that allow some listings to float up based on a paid amount (like Google AdWords).  Basically, you get full control over the sort.

The nice thing is that by using a standard .NET List<> object we have access to the Sort() method and can provide our own delegate to handle the sorting logic.  The unfortunate thing we found is that you really have to watch what you put in the delegate or things can slow down quickly.  That is why there are ids in the sort logic.  The types are ids in the tree selected via Ultimate Picker.  I found that when we did a node lookup based on strings or names, it slowed things down considerably.  

Here is the delegate code I used:

    possibleListings.Sort(delegate(DynamicNode x, DynamicNode y)    {       
      dynamic xListingLevel = x.GetProperty("listingType").Value;
      dynamic yListingLevel = y.GetProperty("listingType").Value;
          
      int xSort = 2;      // default
      if( xListingLevel == "3245" ) { xSort = 1; }  // listing with more importance (direct id for speed)
       
      int ySort = 2;
      if( yListingLevel == "3245" ) { ySort = 1; }
     
      if( xSort == ySort )
      {
        return x.Name.CompareTo(y.Name);
      }
      else
      {
        return xSort.CompareTo(ySort);
      }
    });

Hope that helps someone out there!  Or perhaps you have a different way to solve these problems?  

Oh, and if you're located in North America join the North American Umbraco Users Group and tell your friends to do the same! We need to unearth all the Umbraco goodness that's over here and hope to have a North America Umbraco Festival someday!  Join the Roll Call!

Happy Holidays!

Jason Prothero

Jason is on Twitter as