Driving Lucinq or Lucene.Net Search With The Sitecore Rules Engine

In this post I will be looking at how to drive a lucene search by using the Sitecore Rules engine.

Pretty much since I started using Sitecore, I have found that Lucene has been the get out of jail when it comes to performance. In fact, I can’t remember the last time I used Sitecore queries or the item axes. As a developer I have always been happy to write such methods as GetDescendants(ID itemId, ID templateId).

What about giving this power to the user though?

The rules engine in its default form is a great tool but executing a rule against a bunch of items becomes a iterative process and often can carry a performance penalty. This got me thinking – could I use the rules engine to drive the Sitecore Search Layer?

After much investigation, the reliance on a concrete object to send through the IQueryable layer that Sitecore have created us in 7 meant I abandoned this in favour or returning to the old stomping ground of Lucene.Net.and in my case – Lucinq. Lucinq’s query builder is perfect for the task as it translates straight back to Lucene queries and will accept Lucene queries. It can also read the Sitecore 7 Index field attributes and integrate directly with glass if you choose it to.

So – lets take a look at how this works:

Custom conditions
Sitecore’s conditions out of the box don’t really allow for custom setup of how the rules are used, so I created an interface which allows you to drive the rules builder. In the case below, I have used Lucinq’s query builder to enable this.

    using Lucinq.Enums;
    using Lucinq.Interfaces;
    using Lucinq.SitecoreIntegration.Querying.Interfaces;

    public interface ILuceneQueryCondition
    {
        void AddToQuery(ISitecoreQueryBuilder queryBuilder, Matches matches = Matches.NotSet);
    }

To make conventions simpler to manage I decided to implement an abstract class to prevent the use of the default Rules engine.

    using System.Diagnostics.CodeAnalysis;

    using Lucinq.Enums;
    using Lucinq.SitecoreIntegration.Querying.Interfaces;

    using Sitecore.Rules;
    using Sitecore.Rules.Conditions;

    public abstract class LuceneQueryCondition : RuleCondition, ILuceneQueryCondition where T : RuleContext
    {
        public override void Evaluate(T ruleContext, RuleStack stack)
        {
            throw new System.NotImplementedException("This type of rule was not designed for the main rule engine, it must be used with a query builder");
        }
        public abstract void AddToQuery(ISitecoreQueryBuilder queryBuilder, Matches matches = Matches.NotSet);
    }

Once I had this, it was a simple matter of filling in the blanks to create a usable condition – below we can see a working model of creating a descendants query, the properties are filled in by Sitecore when the RulesFactory returns the definitions of the items.

    using Lucinq.Enums;
    using Lucinq.SitecoreIntegration.Querying.Interfaces;

    using Sitecore.Data;
    using Sitecore.Rules;

    public class ItemDescendsFromCondition : LuceneQueryCondition where T : RuleContext
    {
        public string DataSource { get; set; }

        public override void AddToQuery(ISitecoreQueryBuilder queryBuilder, Matches matches = Matches.NotSet)
        {
            ID id;
            if (!ID.TryParse(this.DataSource, out id))
            {
                return;
            }

            queryBuilder.Field("_path", id, matches);
        }
    }

The rules builder
The rules builder is responsible for taking the return from Sitecore’s default RulesFactory and utilising it in a manner that is more geared to our use.

    using Lucinq.Enums;
    using Lucinq.Interfaces;
    using Lucinq.SitecoreIntegration.Querying;
    using Lucinq.SitecoreIntegration.Querying.Interfaces;

    using Sitecore.Rules;
    using Sitecore.Rules.Conditions;

    public class RuleQueryBuilder : IRuleQueryBuilder
        where T : RuleContext
    {
        public RuleQueryBuilder(RuleList ruleList)
        {
            this.RuleList = ruleList;
        }

        protected RuleList RuleList { get; private set; }

        public virtual ISitecoreQueryBuilder GetQueryBuilder()
        {
            ISitecoreQueryBuilder queryBuilder = new SitecoreQueryBuilder();
            foreach (Rule rule in this.RuleList.Rules)
            {
                this.AddToBuilder(queryBuilder, rule.Condition);
            }

            return queryBuilder;
        }

        protected virtual void AddToBuilder(ISitecoreQueryBuilder queryBuilder, RuleCondition condition, Matches matches = Matches.NotSet)
        {
            this.AddAndCondition(queryBuilder, condition, matches);
            this.AddOrCondition(queryBuilder, condition, matches);
            this.AddLuceneCondition(queryBuilder, condition, matches);
            this.AddNotCondition(queryBuilder, condition);
        }

        protected virtual void AddAndCondition(ISitecoreQueryBuilder queryBuilder, RuleCondition condition, Matches matches)
        {
            AndCondition binaryCondition = condition as AndCondition;
            if (binaryCondition == null)
            {
                return;
            }

            var group = queryBuilder.Group(matches);
            this.AddToBuilder(group, binaryCondition.LeftOperand, Matches.Always);
            this.AddToBuilder(group, binaryCondition.RightOperand, Matches.Always);
        }

        protected virtual void AddOrCondition(ISitecoreQueryBuilder queryBuilder, RuleCondition condition, Matches matches)
        {
            OrCondition binaryCondition = condition as OrCondition;
            if (binaryCondition == null)
            {
                return;
            }

            var group = queryBuilder.Group(matches);
            this.AddToBuilder(group, binaryCondition.LeftOperand, Matches.Sometimes);
            this.AddToBuilder(group, binaryCondition.RightOperand, Matches.Sometimes);
        }

        protected virtual void AddNotCondition(ISitecoreQueryBuilder queryBuilder, RuleCondition condition)
        {
            NotCondition notCondition = condition as NotCondition;
            if (notCondition == null)
            {
                return;
            }

            this.AddToBuilder(queryBuilder, notCondition.Operand, Matches.Never);
        }

        protected virtual void AddLuceneCondition(ISitecoreQueryBuilder queryBuilder, RuleCondition condition, Matches matches)
        {
            ILuceneQueryCondition luceneQueryCondition = condition as ILuceneQueryCondition;
            if (luceneQueryCondition == null)
            {
                return;
            }

            luceneQueryCondition.AddToQuery(queryBuilder, matches);
        }
    }

Setting up the Sitecore rules
Setting up the Sitecore rules for this is exactly the same as setting up any other rules, for the purpose of keeping this concise, I would highly recommend referring to the Sitecore Rules Cookbook from the SDN.

Getting the query
Ok, so now we have the setup in place, how about we go ahead and actually get the query from Sitecore

        /// <summary>
        /// Runs a query rule
        /// </summary>
        [Test]
        public void RunQueryFromItemRule()
        {
            Item item = Database.GetDatabase("master").GetItem(new ID(""));

            RuleContext queryRuleContext = new RuleContext();

            RuleList originalRulesList = RuleFactory.GetRules(item.Fields[""]);

            originalRulesList.Run(queryRuleContext);
            var adapter = new RuleQueryBuilder(originalRulesList);
            var queryBuilder = adapter.GetQueryBuilder();

  // this will show you the lucene query string.
            Console.WriteLine(queryBuilder.Build().ToString());
        }

I will be doing some more work on this to polish it and release it as a module, but the fundamental principle still holds true.

Advertisements

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