November Happy Hour will be moved to Thursday December 5th.

EPiServer Find facet management and product/variant search

Vote:
 

I'm starting to implement Find on a QuickSilver solution replacing the Lucene-based implementation. What I'm a bit puzzled about is how to implement a flexible way of handling facets/filters.

In the Lucene implementation all is controlled by the Mediachase.Search.Filters.config file which gives a quite good flexible way of handling which facets/filters should be available.

In Find it seems I must implement all facet logic in code making it not so flexible or am I missing something?

The structure for facets is to use the TermsFacetFor(x => x.Property) and since I need to provide that strongly typed information in the builup of the query there is no simple way of handling this, like say in a config-file or appSettings. A good solution would have been to let administrators have a way of controlling the facets and filters but since it seems that all needs to be coded there is no such way?

Another thing that comes apparent in how the customer we are working for wants the search functionality to be is that available filters and facets is based on the variants but the searchresult is presented as products.

The Quicksilver implementation for this is that the products have fields for color, sizes that represent their variants different settings. Is this the way to solve this or is there another way of filtering variants but presenting products?

Any suggestions on how to handle those scenarios?

#144770
Feb 18, 2016 15:21
Vote:
 

Solved parts of this problem by using extensionmethods on the products and set all values from a products variations on the product itself. This way I can search for variations values but present the products that has a matching value. 

What I'm still strugling with is how to manage all these filters in a good way both in code but also for the customer to be able to disable/enable a filter.

I have at the moment 3 extensionmethod based filters for ItemColor, ItemFinish and ItemMaterial

When the search is made I have to do like this:

first save the base query which only have the basic filtering made (like which catalog node to search in)

var facetQuery = query;

then I have to check the applied facetGroups and their active filters

var filter = client.BuildFilter<ProductEntity>();
                    foreach (var facet in SelectedGroup.Facets.Where(x => x.Selected))
                    {
                        if (facetGroup.GroupFieldName == "itemcolor")
                            filter = filter.Or(x => x.ItemColor().Match(facet.Key));

                        if (facetGroup.GroupFieldName == "itemfinish")
                            filter = filter.Or(x => x.ItemFinish().Match(facet.Key));

                        if (facetGroup.GroupFieldName == "itemmaterial")
                            filter = filter.Or(x => x.ItemMaterial().Match(facet.Key));
                    }
                    query = query.Filter(filter);

Then get the result

query = query.Skip((page - 1) * pageSize).Take(pageSize);

            var searchResult = query.GetContentResult();

After that go back to the clean facetQuery and start applying the Facet logic to be able to return available facets for the current category

facetQuery = facetQuery
                .TermsFacetFor(x => x.ItemFinish())
                .TermsFacetFor(x => x.ItemMaterial())
                .TermsFacetFor(x => x.ItemColor())
            // .TermsFacetFor(x => x.AvailableColors)
            .Take(0);

            var facetResult = facetQuery.GetContentResult();

            var remainingFacets = new List<FacetGroupOption>();

            foreach (var facetGroup in GetFacetGroups())
            {
                var inputFacetGroup = facetGroups.Where(x => x.GroupFieldName.Equals(facetGroup.GroupFieldName)).SingleOrDefault();
                switch (facetGroup.GroupFieldName)
                {
                    
                    case "itemcolor":
                        // Color facet
                        var colorFacetGroup = new FacetGroupOption() { Facets = new List<FacetOption>(), GroupName = "Färg", GroupFieldName = "itemcolor" };
                        var colorFacets = facetResult.TermsFacetFor(x => x.ItemColor());
                        foreach (var facet in colorFacets)
                        {
                            var inputFacet = inputFacetGroup != null ? inputFacetGroup.Facets.Where(x => x.Key.Equals(facet.Term)).SingleOrDefault() : null;
                            colorFacetGroup.Facets.Add(new FacetOption { Count = facet.Count, Key = facet.Term, Name = facet.Term, Selected = inputFacet != null ? inputFacet.Selected : false });
                        }
                        remainingFacets.Add(colorFacetGroup);
                        break;
                    case "itemfinish":
                        var finishFacetGroup = new FacetGroupOption() { Facets = new List<FacetOption>(), GroupName = "Finish", GroupFieldName = "itemfinish" };
                        var finishFacets = facetResult.TermsFacetFor(x => x.ItemFinish());
                        foreach(var facet in finishFacets)
                        {
                            var inputFacet = inputFacetGroup != null ? inputFacetGroup.Facets.Where(x => x.Key.Equals(facet.Term)).SingleOrDefault() : null;
                            finishFacetGroup.Facets.Add(new FacetOption { Count = facet.Count, Key = facet.Term, Name = facet.Term, Selected = inputFacet != null ? inputFacet.Selected : false });
                        }
                        remainingFacets.Add(finishFacetGroup);
                        break;
                    case "itemmaterial":
                        var materialFacetGroup = new FacetGroupOption() { Facets = new List<FacetOption>(), GroupName = "Material", GroupFieldName = "itemmaterial" };
                        var materialFacets = facetResult.TermsFacetFor(x => x.ItemMaterial());
                        foreach (var facet in materialFacets)
                        {
                            var inputFacet = inputFacetGroup != null ? inputFacetGroup.Facets.Where(x => x.Key.Equals(facet.Term)).SingleOrDefault() : null;
                            materialFacetGroup.Facets.Add(new FacetOption { Count = facet.Count, Key = facet.Term, Name = facet.Term, Selected = inputFacet != null ? inputFacet.Selected : false });
                        }
                        remainingFacets.Add(materialFacetGroup);
                        break;

                }
            }


            facetGroups = remainingFacets;

Thats a lot of code even for just three filters and the customer is talking alot more filters than that.

Am I missing something here in how to working with the facets and filters? I have tried to look into better ways to manage this but it feels the need of strongly typed referencing in TermsFacetFor (x.ItemMaterial()) gives me no other chice than to structure it this way. Or does anyone have any input on this?

And what happens if the customer decides that the avaliable product count in each filter not currently used in the search should reflect the count that would be the result if it is used, it becomes much more complicated. I would then need to apply all other selected facets than the current one and do the facet query on that and then move on to the next one and allpy all other filters than that. This would grow into an unmanagable code hell.

#147334
Apr 12, 2016 12:37
Vote:
 

What you can do is to optimize this into just one query by using FilterHits for those filters that only should be applied to the search result and not the facets. http://world.episerver.com/documentation/Items/Developers-Guide/EPiServer-Find/9/DotNET-Client-API/Searching/Filtering/Filter-and-FilterHits/

Another thing you can do to avoid the strongly typed facet names is to specify and fetch using a ustom name:

Query:
query.TermsFacetFor(x => x.ItemFinish(), r => r.Name = "itemfinish")
.GetResult();

Resutl:
results.Facets["mycustomname"] as TermsFacet

/Henrik

#148784
May 24, 2016 10:34
Vote:
 

Thanks for the input.

The main issue I'm struggling with though is how to make a good structure for getting the correct itemcount for each filter in the different facetgroups without making specific code for each facetgroup.

If I have active filters in two groups, say color and material, out of 10 available facet groups

It then feels like I have to build up ten different facetqueries to get the correct count and available facets 

for color group

colorQuery = colorQuery.TermsFacetFor(x => x.ItemColor())
var Filter = client.BuildFilter<ProductEntity>();
                    foreach (var facet in SelectedGroup.Facets.Where(x => x.Selected))
                    {
                        if (facetGroup.GroupFieldName == "itemcolor")
                           continue;

                        if (facetGroup.GroupFieldName == "itemfinish")
                            filter = filter.Or(x => x.ItemFinish().Match(facetKey));

                        if (facetGroup.GroupFieldName == "itemmaterial")
                            filter = filter.Or(x => x.ItemMaterial().Match(facetKey));
                    }
                    colorQuery = colorQuery.Filter(filter);
    }
}
var colorResult = colorQuery.GetContentResult();
var colorFacets = colorResult.TermsFacetFor(x => x.ItemColor());  

Then for the next filter

materialQuery = materialQuery.TermsFacetFor(x => x.ItemMaterial()).Take(0);
var Filter = client.BuildFilter<ProductEntity>();
                    foreach (var facet in SelectedGroup.Facets.Where(x => x.Selected))
                    {
                        if (facetGroup.GroupFieldName == "itemcolor")
                           filter = filter.Or(x => x.ItemColor().Match(facetKey));

                        if (facetGroup.GroupFieldName == "itemfinish")
                            filter = filter.Or(x => x.ItemFinish().Match(facetKey));

                        if (facetGroup.GroupFieldName == "itemmaterial")
                            continue;
                    }
                    materialQuery = materialQuery.Filter(filter);
    }
}

var materialResult = materialQuery.GetContentResult();
var materialFacets = materialResult.TermsFacetFor(x => x.ItemMaterial());


With only three groups this is somewhat maintainable but if theere are more groups both the number of queries needed and the if cases in each just gets too much.

Therefor it would make things a lot easier if there were anything that could be done to make this kind of query for correct count and available filters based on active filters more effective.

#148849
May 25, 2016 9:51
Vote:
 

I've created a small extension to Find (FacetFilter2Find) that enables setting filters on individual facets that will enable you to execute all your facets in a single request by specifying different filters for each facet.

/Henrik

#149528
Jun 02, 2016 15:38
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.