5

Here is my scenario:

  • Bucket of sitecore items of a specific template
  • This template has a field tags(which is a treelist) where you can associate x items there. I'm building a query using lucene to retrieve all the items from the index that have the same tags as my current item. How can I do that?

My code is not 100% done because getting this relashionship is not working properly.

using (var context = ContentSearchManager.GetIndex(indexName).CreateSearchContext())
{
    //Filters related articles
    var relatedSearchQuery = context.GetQueryable<SearchResultItem>()
        .Where( item => item.ItemId != currentArticle.Id.ToID() 

How do I make that relationship as I can't make standard Linq queries at this point?

  • Have you added the Tags field to the SearchResultItem class? – Richard Seal Oct 27 '16 at 19:20
  • no. I just created the Tags field on Sitecore and have published and reindexed but that's how far it goes – Diego Oct 27 '16 at 19:21
11

To be able to search on a field in the Sitecore SearchAPI you need to add that field to a model. So you could create a new class derived from SearchResultItem and add your tags field in:

public class MySearchResultItem: SearchResultItem
{
    [TypeConverter(typeof (IndexFieldEnumerableConverter))]
    [IndexField("tags")]
    public IEnumerable<ID> Tags { get; set; }
}

I'm here assuming that your Tags field is a list type field that stores the Guids of the tags. The IndexField attribute tells Sitecore what the field name is for this property. The TypeConverter field tells the API how to convert the value stored in the index to the C# type.

Now you have that you can use it in your Linq query:

using (var context = ContentSearchManager.GetIndex(indexName).CreateSearchContext())
{
    //Filters related articles
    var relatedSearchQuery = context.GetQueryable<MySearchResultItem>()
        .Where(item => item.ItemId != currentArticle.Id.ToID() &&
                       item.Tags.Contains(currentArticle.TagId))

Here I am assuming a single tag on the current article.

If you need to compare a list with a list it gets a bit more complicated. In that case you need to create a search extension that will build that query.

Here is the code. I have included the comments in the code to explain what is going on.

public static class SearchExtensions
{
    public static IQueryable<TSource> ContainsOr<TSource, TKey>(this IQueryable<TSource> queryable, Expression<Func<TSource, TKey>> keySelector, IEnumerable values) where TKey : IEnumerable
    {
        return Contains(queryable, keySelector, values, true);
    }

    public static IQueryable<TSource> ContainsAnd<TSource, TKey>(this IQueryable<TSource> queryable, Expression<Func<TSource, TKey>> keySelector, IEnumerable values) where TKey : IEnumerable
    {
        return Contains(queryable, keySelector, values, false);
    }

    public static IQueryable<TSource> Contains<TSource, TKey>(this IQueryable<TSource> queryable, Expression<Func<TSource, TKey>> keySelector, IEnumerable values, bool orOperator) where TKey : IEnumerable
    {
        const string methodName = "Contains";

        // Ensure the body of the selector is a MemberExpression
        if (!(keySelector.Body is MemberExpression))
        {
            throw new InvalidOperationException("Expression must be a member expression");
        }

        var typeOfTSource = typeof(TSource);
        var typeOfTKey = typeof(TKey);

        // x
        var parameter = Expression.Parameter(typeOfTSource);

        // Create the enumerable of constant expressions based off of the values
        var constants = values.Cast<object>().Select(id => Expression.Constant(id));

        IEnumerable<MethodCallExpression> expressions = Enumerable.Empty<MethodCallExpression>();
        /*
         * Create separate MethodCallExpression objects for each constant expression created
         *
         * Each expression will effectively be like running the following;
         * x => x.MyIdListField.Contains(AnId)
         *
         * Check to see if we can find a method on TKey type which matches the method we want to run.
         * We do this because not all types use the static IEnumerable extension e.g. the String class
         * has it's own implementation of .Contains.
         *
         * If we can't find a matching method then we try to run the extension method found in Enumerable
         */
        if (typeOfTKey.GetMethods().Any(m => m.Name.Equals(methodName)))
        {
            var method = typeOfTKey.GenericTypeArguments.Any() ? typeOfTKey.GetMethod(methodName, typeOfTKey.GenericTypeArguments) : typeOfTKey.GetMethod(methodName);

            /*
             * instance     -> this would be property we want to run the expession on e.g.
             *                 IQueryable<MyPocoTemplate>.Where(x => x.MyIdListField)
             *                 so keySelector.Body will contain the "x.MyIdListField" which is what we want to run
             *                 each constant expression against
             * method       -> the method to run against the instance e.g. "x.MyIdListField.Contains(...)"
             * arguments    ->
             *      constant    ->  this is the constant expression (value) to be passed to the method
             */
            expressions = constants.Select(constant => Expression.Call(keySelector.Body, method, constant));
        }
        else
        {
            /*
             * type             ->  we need to specify the type which contains the method we want to run
             * methodName       ->  in this instance we need to specify the Contains method
             * typeArguments    ->  the type parameter from TKey
             *                      e.g. if we're passing through IEnumerable<Guid> then this will pass through the Guid type
             *                      this is because we're effectively running IEnumerable<Guid>.Contains(Guid guid) for each
             *                      guid in our values object
             * arguments        ->
             *      keySelector.Body    ->  this would be property we want to run the expession on e.g.
             *                              IQueryable<MyPocoTemplate>.Where(x => x.MyIdListField)
             *                              so keySelector.Body will contain the "x.MyIdListField" which is what we want to run
             *                              each constant expression against
             *      constant            ->  this is the constant expression (value) to be passed to the method
             */
            var typeArgs = typeOfTKey.IsArray ? new[] { typeOfTKey.GetElementType() } : typeOfTKey.GenericTypeArguments;

            expressions = constants.Select(constant => Expression.Call(typeof(Enumerable), methodName, typeArgs, keySelector.Body, constant));
        }

        /* 
         * Combine all the expressions into one expression so you would end with something like;
         * 
         * x => x.MyIdListField.Contains(AnId) OR x.MyIdListField.Contains(AnId) OR x.MyIdListField.Contains(AnId)
         */
        var aggregateExpressions = expressions.Select(expression => (Expression)expression).Aggregate((x, y) => orOperator ? Expression.OrElse(x, y) : Expression.AndAlso(x, y));

        // Create the Lambda expression which can be passed to the .Where
        var lambda = Expression.Lambda<Func<TSource, bool>>(aggregateExpressions, parameter);

        return queryable.Where(lambda);
    }
}

This will then allow you to do a .ContainsAnd or a .ContainsOr with your lists. ContainsAnd means that all entries in the list must be present in the returned item. ContainsOr returns items where it contains any of the ID's in the list.

So your query would become:

using (var context = ContentSearchManager.GetIndex(indexName).CreateSearchContext())
{
    //Filters related articles
    var relatedSearchQuery = context.GetQueryable<MySearchResultItem>()
        .Where(item => item.ItemId != currentArticle.Id.ToID())
        .ContainsAnd(item => item.Tags, currentArticle.Tags)

Where currentArticle.Tags is an already populated list of ID's.

SearchExtensions code taken from Fortis Search Extensions

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.