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.