Gravatar for shirazi@rdacorp.com

Question by aga, Nov 19, 2015 10:49 PM

Building an OR expression

Hi

I'm trying to create a Coveo Rule that builds a OR field comparison something like @field='a' OR @field='b' OR @field='a'. The code below with exception "Unsupported expression node type: Or"

public Expression GetQueryExpression(ConditionContext p_Context)
{
    String[] seeAll = new String[]{"a","b", "c};
    Expression init = this.BuildBinaryExpression(new Func<Expression, Expression, Expression>(this.CallExactMatchExtensionMethod), p_Context, seeAll[0]);

    for (int i = 1; i < seeAll.Length; i++)
    {
        Expression n = this.BuildBinaryExpression(new Func<Expression, Expression, Expression>(this.CallExactMatchExtensionMethod), p_Context, seeAll[i]);

        init = Expression.Or(init, n);
    }

    return init;
}

private Expression BuildBinaryExpression(Func<Expression, Expression, Expression> p_OperatorMethod, ConditionContext p_Context, String val)
{
    Expression fieldIndexerExpression = GetFieldIndexerExpression(p_Context.ParameterExpression, FieldName);
    return p_OperatorMethod(fieldIndexerExpression, Expression.Constant(val));
}

private Expression CallExactMatchExtensionMethod(Expression p_Field, Expression p_Constant)
{
    return Expression.Call(typeof(CoveoQueryableExtensions), "ExactMatch", null, new Expression[]
    {
        p_Field,
        p_Constant
    });
}

internal static Expression GetFieldIndexerExpression(ParameterExpression p_Instance, string p_FieldName)
{
    string name = "Item";
    object[] customAttributes = p_Instance.Type.GetCustomAttributes(typeof(DefaultMemberAttribute), true);
    if (customAttributes.Any<object>())
    {
        name = (customAttributes[0] as DefaultMemberAttribute).MemberName;
    }
    PropertyInfo property = p_Instance.Type.GetProperty(name, new Type[]
    {
      typeof(string)
    });

    Assert.IsNotNull(property, "Could not find the proper indexer. Expecting: string this[string]");
    MethodInfo getMethod = property.GetMethod;

    return Expression.Call(p_Instance, getMethod, new Expression[]
    {
      Expression.Constant(p_FieldName)
    });
}

Error in the page:

Exception Stack:
    Line 13:     <script type="text/javascript"> 
    Line 14:         Coveo.$(function() {
    Line 15:             var searchOptions = <%= Model.GetJavaScriptInitializationOptions() %>;
    Line 16:             CoveoForSitecore.componentsOptions = Coveo.$.extend({}, CoveoForSitecore.componentsOptions, searchOptions);
    Line 17:         });

LINQ Error:

[NotSupportedException: Unsupported expression node type: Or]
   Sitecore.ContentSearch.Linq.Parsing.ExpressionParser.Visit(Expression expression) +715
   Sitecore.ContentSearch.Linq.Parsing.ExpressionParser.VisitBinary(BinaryExpression expression) +96
   Sitecore.ContentSearch.Linq.Parsing.ExpressionParser.Visit(Expression expression) +228
   Sitecore.ContentSearch.Linq.Parsing.ExpressionParser.VisitBinary(BinaryExpression expression) +96
   Sitecore.ContentSearch.Linq.Parsing.ExpressionParser.Visit(Expression expression) +228
   Sitecore.ContentSearch.Linq.Parsing.ExpressionParser.VisitBinary(BinaryExpression expression) +96
   Sitecore.ContentSearch.Linq.Parsing.ExpressionParser.Visit(Expression expression) +228
   Sitecore.ContentSearch.Linq.Parsing.ExpressionParser.VisitWhereMethod(MethodCallExpression methodCall) +221
   Sitecore.ContentSearch.Linq.Parsing.ExpressionParser.VisitQueryableMethod(MethodCallExpression methodCall) +1293
   Coveo.SearchProvider.LinqBase.CoveoExpressionParserBase.VisitMethodCall(MethodCallExpression p_MethodCall) +303
   Sitecore.ContentSearch.Linq.Parsing.ExpressionParser.Visit(Expression expression) +178
   Sitecore.ContentSearch.Linq.Parsing.ExpressionParser.Parse(Expression expression) +24
1 Reply
Gravatar for lbergeron@coveo.com

Answer by Luc Bergeron, Nov 23, 2015 3:59 PM

Hi,

I've done something similar. It creates an OR condition for every value you want to compare. The main difference is that instead of creating the LINQ expression directly, it relies on existing rule conditions to make them create the LINQ expressions.

In the end, it generates a query expression that look like field == "a" OR field == "b" OR field == "c" and so on.

Let me know how it goes.

using System;
using System.Collections.Generic;
using System.Linq;
using Coveo.UI;
using Coveo.UI.Rules;
using Coveo.UI.Rules.Conditions;
using Coveo.UI.Rules.Conditions.Wrappers;
using Sitecore.Rules;
using Sitecore.Rules.Conditions;
using System.Linq.Expressions;

namespace TheNamespace
{
    public class FieldMatchesMultipleValuesCondition<T> : RuleCondition<T>, ICoveoCondition<T> where T : RuleContext
    {
        public string Field { get; set; }
        public string Values { get; set; }

        public Expression GetQueryExpression(ConditionContext p_Context)
        {
            // Split the comma-separated values from the "Values" property.
            IEnumerable<string> splitValues = GetSplitValues();

            // Gets the sub-conditions, one for each value to filter on.
            IEnumerable<ICoveoCondition<T>> filterConditions = GetValueConditions(splitValues);

            // Takes the condition list and builds a condition tree.
            ICoveoCondition<T> completeCondition = GetValueConditionTree(filterConditions);

            // By leveraging existing rule conditions, we don't need to build the LINQ expression from scratch.
            return completeCondition.GetQueryExpression(p_Context);
        }

        public RuleCondition<T> GetWrappedCondition()
        {
            return this;
        }

        public void ValidateCondition(Coveo.UI.ErrorReport p_Report)
        {
            IEnumerable<string> splitValues = GetSplitValues();

            // We need at least the field name and one value in order to build the expression.

            if (String.IsNullOrWhiteSpace(Field)) {
                p_Report.AddError("The field name must not be empty.");
            }

            if (!splitValues.Any()) {
                p_Report.AddError("At least one value must be defined. Values must be comma-separated.");
            }
        }

        public override void Evaluate(T p_RuleContext,
                                      RuleStack p_Stack)
        {
            // This method won't be called by Coveo to generate the query expression.
        }

        private IEnumerable<string> GetSplitValues()
        {
            string values = Values ?? "";
            IEnumerable<string> splitValues = values.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries);
            return splitValues.Select(value => value.Trim()).ToArray();
        }

        private IEnumerable<ICoveoCondition<T>> GetValueConditions(IEnumerable<string> p_Values)
        {
            foreach (string value in p_Values) {
                yield return GetValueCondition(value);
            }
        }

        private ICoveoCondition<T> GetValueCondition(string p_Value)
        {
            // It is way easier to reuse an existing rule condition
            // and make it generate the LINQ expression for us.
            StringFieldCondition<T> filterCondition = new StringFieldCondition<T>();
            filterCondition.FieldName = Field;
            filterCondition.OperatorId = CoveoStringOperatorIDs.EQUALS;
            filterCondition.Value = p_Value;

            return filterCondition;
        }

        private ICoveoCondition<T> GetValueConditionTree(IEnumerable<ICoveoCondition<T>> p_Conditions)
        {
            ICoveoCondition<T> completeCondition = null;

            if (p_Conditions.Any()) {
                // When simply chaining conditions together, we might end with an unbalanced tree.
                // And, when this tree has more than a hundred conditions, we get stack overflows with LINQ.
                // One solution is to build a balanced condition tree.
                completeCondition = BuildBalancedOrConditionTree(p_Conditions);
            }

            return completeCondition;
        }

        private ICoveoCondition<T> BuildBalancedOrConditionTree(IEnumerable<ICoveoCondition<T>> p_Conditions)
        {
            ICoveoCondition<T> completeCondition = null;

            Queue<ICoveoCondition<T>> queuedNodes = new Queue<ICoveoCondition<T>>(p_Conditions);

            while (queuedNodes.Count > 1) {
                ICoveoCondition<T> leftCondition = queuedNodes.Dequeue();
                ICoveoCondition<T> rightCondition = queuedNodes.Dequeue();

                ICoveoCondition<T> newOrCondition = new OrConditionWrapper<T>(leftCondition.GetWrappedCondition(), rightCondition.GetWrappedCondition());
                queuedNodes.Enqueue(newOrCondition);
            }

            return queuedNodes.Dequeue();
        }
    }
}
Ask a question