Multi-Select Faceting


NOTE: This uses syntax from the upcoming Solr 5.4 release. If you are using Solr 5.2 or 5.3,
specify domain:{excludeTags:mytag} as excludeTags:mytag.

Multi-Select Faceting with Solr

Multi-select faceting is a powerful faceting style that allows users to see and select multiple facet constraints (facet values) for certain facets. This example uses Solr’s JSON Facet API along with filter tagging and excluding to implement this style of faceting.

Sports store example

Let’s say we have 3 facets, Size, Color, and Brand.
This is multi-select faceting because for the Color and Brand facets, we want the user to be able to select multiple constraints (values). The Size facet is single-select since we’ve decided that customers in general will only be interested in one size at a time.

Here is our super-fancy ASCII UI, after the user has searched for "running shorts":

=== Size ===   === Color ===    === Brand ===
[Small] (7)    [ ] Red (2)      [ ] Nike (7)
[Medium] (5)   [ ] Blue (8)     [ ] Adidas (5)
[Large] (6)    [ ] Green (3)    [ ] Reebok (4)
               [ ] Black (5)    [ ] Under Armour (2)

(Top matches sorted by popularity displayed here... use your imagination!)

Note that the Color and Brand facets have checkboxes to indicate which constraints have been selected. We’re starting off with no constraints. Below the facets is where we would display the top matching items, along with pretty pictures, prices, etc.

 

1. User selects “Blue”

The Wrong Way

The user selects “Blue” so we add that as a filter and re-issue the request (we’re using Solr’s JSON Facet API):

&q="running shorts"
&fq=color:Blue
&json.facet={
      sizes:{type:terms, field:size},
      colors:{type:terms, field:color},
      brands:{type:terms, field:brand}
}

We get back the response and update our UI from that data:

=== Size ===   === Color ===    === Brand ===
[Small] (3)    [ ] Red (0)      [ ] Nike (3)
[Medium] (2)   [x] Blue (8)     [ ] Adidas (2)
[Large] (3)    [ ] Green (0)    [ ] Reebok (2)
               [ ] Black (0)    [ ] Under Armour (1)

(Top Blue running shorts displayed here)

What’s right:
The Size and Brand facets now reflect the fact that we’ve selected Blue, and that’s what we wanted. Our list of top matches also only includes Blue things, just as we wanted.

What’s wrong:
Because we filtered out anything that wasn’t Blue, we get back 0 counts for other colors! But we still want the other color information so the customer can select additional colors.

 

The Right Way

When we compute the multi-select Color facet, we want to ignore any constraints (filters) on that facet so we will get back the correct counts for other colors. To accomplish this, we can tag filters and then selectively exclude filters (i.e. pretend they don’t exist) by tag when faceting.

The same thing applies to the multi-select Brand facet… we want any Brand selections to affect everything else (including all other facets), except for the Brand facet itself.

When the user selects Blue, we add that as a filter tagged with COLOR and re-issue the request:

&q="running shorts"
&fq={!tag=COLOR}color:Blue
&json.facet={
      sizes:{type:terms, field:size},
      colors:{type:terms, field:color, domain:{excludeTags:COLOR} },
      brands:{type:terms, field:brand, domain:{excludeTags:BRAND} }
}

Now when we get back our response, it still includes the other Colors in the Color facet.

=== Size ===   === Color ===    === Brand ===
[Small] (3)    [ ] Red (2)      [ ] Nike (3)
[Medium] (2)   [x] Blue (8)     [ ] Adidas (2)
[Large] (3)    [ ] Green (3)    [ ] Reebok (2)
               [ ] Black (5)    [ ] Under Armour (1)

(Top Blue running shorts displayed here)

domain and excludeTags

The domain is the set of documents that facets will be calculated over. In the JSON Facet API, the domain keyword/command is normally used to change the domain before the facets are calculated.

In our example above, we specified domain:{excludeTags:COLOR} for the colors facet. This will re-calculate the facet domain as if any filters tagged with COLOR were not applied.

 

2. User selects “Black”

Ok, now the user selects Black as well.

The Wrong Way

We naively add an additional filter, fq={!tag=COLOR}color:Black to the request, just as we would with traditional single-select faceting.

What’s wrong:
Everything! Our request matches nothing and we get back all 0’s.

This is because filters are logically intersected. We searched for things that were Blue AND Black, and that will match nothing (assuming our items only have a single color). What we really want is Blue OR Black.

 

The Right Way

We need the logical OR, or union, of all the selected colors.

&q="running shorts"
&fq={!tag=COLOR}color:(Blue Black)
&json.facet={
      sizes:{type:terms, field:size},
      colors:{type:terms, field:color, domain:{excludeTags:COLOR} },
      brands:{type:terms, field:brand, domain:{excludeTags:BRAND} }
}
=== Size ===   === Color ===    === Brand ===
[Small] (5)    [ ] Red (2)      [ ] Nike (5)
[Medium] (4)   [x] Blue (8)     [ ] Adidas (3)
[Large] (4)    [ ] Green (3)    [ ] Reebok (3)
               [x] Black (5)    [ ] Under Armour (2)

(Top Blue and Black running shorts displayed here)

Note that the counts on the other facets increased to reflect the larger domain (it includes both blue and black items).

Not just counts!

Although our simple example just dealt with facet counts, multi-select faceting via excludeTags works with the broad range of features in the JSON Facet API. The domain change will apply to everything else under that facet, including Sub-facets and Facet Functions.

 
 

Tagging and excluding filters with excludeTags

Solr filter queries (fq parameters) can be tagged with arbitrary strings using the localParams {!tag=mystring} syntax.
Example: fq={!tag=COLOR}color:Blue

  • Multiple filters can be tagged with the same tag. Example:
    fq={!tag=foo}one_filter&fq={!tag=foo}another_filter
  • A single filter may be tagged with multiple tags. Example:
    fq={!tag=tag1,tag2,tag3}my_field:my_filter

During faceting, the facet domain may be changed to exclude filters that match certain tags via the excludeTags keyword. It’s as if the filter was never specified for that specific facet. This is useful for implementing multi-select faceting
Example: colors:{type:terms, field:color, domain:{excludeTags:COLOR}}

  • excludeTags can be multi-valued comma-separated string. Example: excludeTags:"tag1,tag2"
  • excludeTags can be a JSON array of tags. Example: excludeTags:["tag1","tag2"]
  • One can exclude tags that are not used in the current request. This makes constructing requests simpler since you don’t need to worry about changing the faceting part of the request based on what filters have been applied.
  • For nested facets, excludeTags can appear at any level of the hierarchy. They do not currently “stack” though. If a parent facet has excludeTags:tag1 and a child facet wants to additionally exclude tag2 filters, then they must currently do so explicitly with excludeTags:”tag1,tag2″. Nested exclusions are experimental and subject to change.