Skip to content

Faceted search

Andrea Gazzarini edited this page Oct 19, 2015 · 24 revisions

The Hybrid mode has been temporarily disabled as there are some issues that need to be fixed

"Faceted search, also called faceted navigation or faceted browsing, is a technique for accessing information organized according to a faceted classification system, allowing users to explore a collection of information by applying multiple filters."
(Source: Wikipedia)

Apache Solr built-in faceting capabilities are nicely described in the official Solr Reference Guide [1] or in the Solr Wiki [2].
In SolRDF, due to the nature of the underlying data, faceted search assumes a shape which is a bit different from traditional faceting over structured data. For instance, while in a traditional Solr schema we could have something like this:

<field name="title" .../>
<field name="author" .../>
<field name="publisher" .../>
<field name="publication_year" .../>
<field name="isbn" .../>
<field name="subject" .../>
...

In SolRDF data is always represented as a sequence of triples, that is, a set of assertions (aka statements), representing the state of a given entity by means of three / four compounding members: a subject, a predicate, an object and an optional context. The holding schema, which is described better in a dedicated section of this Wiki, is, simplifying, something like this:

<!-- Subject -->
<field name="s" .../>

<!-- Predicate -->
<field name="p" .../>

<!-- Object -->
<field name="o" .../>

A "book" entity would be represented, in RDF, in the following way:

<#xyz>
    dc:title "La Divina Commedia" ;   
    dc:creator "Dante Alighieri" ;
    dc:publisher "ABCD Publishing";
    ...

A faceted search makes sense only when the target aggregation field or criteria leads to a literal value, a number, something that can be aggregated. That's the reason you will see, in a traditional Solr that indexes books, a request like this:

facet=true
&facet.field=year
&facet.field=subject
&facet.field=author 

In the example above, we are requesting facets for three fields: year, subject and author. In SolRDF we don't have such "dedicated" fields like year or author, but we always have s,p,o and an optional c.
Faceting on s, p and c, although perfectly possible using plain Solr facet fields (e.g. facet.field=s&facet.field=p), doesn't make much sense because they are always URI or blank nodes. Instead, the field where faceting reveals its power is the object (o). But again, asking for plain faceting on "o" field (i.e. facet.field=o), will result in a facet that aggregates apples and bananas: each object represents a different meaning, it could have a different domain and data-type. We need a way to identify a given range of objects.
In RDF, what determines the range of the object of a given triple, is the second member, the predicate. So instead of indicating what is the target field of a given facet, we will indicate a query that selects a given range of objects values. An example will be surely more clear.

Solr (field) faceting:

facet=true
&facet.field=author

SolRDF (field) faceting:

facet=true
&facet.field.q=p:<http://purl.org/dc/elements/1.1/creator>

The query will select all objects having an author as value, and then faceting will use those values. The same concept can be applied to range faceting.
The following table illustrates a comparison between Solr and SolRDF faceting capabilities:

Solr SolRDF Tracking issue
Facet Fields Facet Fields N.A.
Facet Queries N.A.
Facet Object Queries #47
Range Faceting N.A.
Object Range Queries Faceting #28
Pivot Faceting Not yet planned
Interval Faceting Not yet planned

The next sections will describe and detail the several kinds of faceting available in SolRDF.

Facet Fields

Traditional field faceting is supported on SolRDF: you can have a field (remember: s,p,o or c) to be treated as a facet by means of the facet.field parameter. All other parameters described in the Solr Reference Guide [1] are supported. Some examples:

Example 1: field faceting on predicates with a minimum count of 1
q=SELECT * WHERE { ?s ?p ?o }  
&facet=true  
&facet.field=p  
&facet.mincount=1    
Example 2: field faceting on subjects and predicates with a different minimum count
q=SELECT * WHERE { ?s ?p ?o }  
&facet=true  
&facet.field=p  
&facet.field=s  
&f.s.facet.mincount=1  
&f.p.facet.mincount=10   
Example 2: field faceting on predicates with a prefix (Dublin Core namespace) and minimum count constraints
q=SELECT * WHERE { ?s ?p ?o }
&facet=true  
&facet.field=p  
&facet.prefix=<http://purl.org/dc/elements/1.1  

Object Queries Faceting

Facet field queries have basically the same meaning of facet fields: the only difference is that, instead on indicating a target field, faceting is always done on the o(bject) field, and you can indicate, with a query, what are the objects that will be faceted. Some examples:

Example 1: field faceting on publishers
q=SELECT * WHERE {?s ?p ?o}
&facet=true  
&facet.field.q=p:<http://purl.org/dc/elements/1.1/publisher>
&facet  
Example 2: field faceting on names (authors or contributors)
q=SELECT * WHERE {?s ?p ?o}
&facet=true  
&facet.field.q=
    p:<http://purl.org/dc/elements/1.1/creator> 
    p:<http://purl.org/dc/elements/1.1/contributor>  
Example 3: field faceting on relations belonging to a given resource
q=SELECT * WHERE {?s ?p ?o}
&facet=true  
&facet.field.q=
    +s:<http://org.example#xyz> 
    +p:<http://purl.org/dc/elements/1.1/relation>  

The facet.field.q parameter can be repeated using an optional progressive number as suffix in the parameter name:

q=SELECT * WHERE {?s ?p ?o}
&facet=true  
&facet.field.q.1=p:<http://purl.org/dc/elements/1.1/creator> 
&facet.field.q.2=p:<http://purl.org/dc/elements/1.1/language>  

or

q=SELECT * WHERE {?s ?p ?o}
&facet=true  
&facet.field.q=p:<http://purl.org/dc/elements/1.1/creator> 
&facet.field.q=p:<http://purl.org/dc/elements/1.1/language>  

In this case you will get a facet for each query, keyed using the query itself:

<lst name="facet_counts">
 <lst name="facet_fields">
   <lst name="p:<http://purl.org/dc/elements/1.1/creator>">
     <int name="Ross, Karlint">12</int>
     <int name="Earl, James">9</int>
     <int name="Foo, John">9</int>
     ...
   </lst>
   <lst name="p:<http://purl.org/dc/elements/1.1/language>">
     <int name="en">3445</int>
     <int name="de">2958</int>
     <int name="it">2865</int>
     ...
   </lst>
 </lst>
</lst>

The suffix in the parameter name is not required, but it is useful to indicate an alias for each query:

q=SELECT * WHERE {?s ?p ?o}
&facet=true  
&facet.field.q.1=p:<http://purl.org/dc/elements/1.1/creator> 
&facet.field.q.2=p:<http://purl.org/dc/elements/1.1/language>  
&facet.field.q.alias.1=creator 
&facet.field.q.alias.2=language  

The response in this case will be (note that each facet is now associated with the alias):

<lst name="facet_counts">
 <lst name="facet_fields">
   <lst name="author">
     <int name="Ross, Karlint">12</int>
     <int name="Earl, James">9</int>
     <int name="Foo, John">9</int>
     ...
   </lst>
   <lst name="language">
     <int name="en">3445</int>
     <int name="de">2958</int>
     <int name="it">2865</int>
     ...
   </lst>
 </lst>
</lst>

Object Range Queries Faceting

Range faceting is described in the Solr Reference Guide [1] or in the Solr Wiki [2]. You can get this kind of facet on all fields that support range queries (e.g. dates and numerics).
A request like this:

facet.range=year
&facet.range.start=2000
&facet.range.end=2015
&facet.range.gap=1

will produce a response like this:

<lst name="facet_ranges">
    <lst name="year">
      <lst name="counts">
          <int name="2000">3445</int>
          <int name="2001">2862</int>
          <int name="2002">2776</int>
          <int name="2003">2865</int>
          ...  
       </lst>      
       <int name="gap">1</int>
       <int name="start">2000</int>
       <int name="end">2010</int>
    </lst>
    ...

As briefly explained before, with semi-structured data like RDF we don't have "year" or "price" or whatever strictly dedicated field for representing a given concept; we always have 3 or 4 fields:

  • a s(ubject)
  • a p(redicate)
  • an o(bject)
  • and optionally a c(ontext)

Requesting something like this:

facet.range=o

wouldn't work: we would mix apples and bananas. In addition, without knowing in advance the domain of the target value (e.g. integer, double, date) how could we express a valid facet.range.start, facet.range.end and facet.range.gap?

Range faceting for s or p or c attributes doesn't make any sense at all because the corresponding URI datatype (i.e. string) doesn't support range queries.

In order to enable range faceting on SolRDF, the default FacetComponent has been replaced with a custom subclass that does something I called Objects Range Query Faceting, which is actually a mix between facet ranges and facet queries.

  • Facet because, of course, the final results are a set of facets
  • Object because faceting uses the o(bject) field
  • Range because what we are going to compute are facet ranges
  • Queries because instead of indicating the target attribute in request (by means of facet.range parameter), this kind of faceting requires a facet.range.q which is a query (by default parsed by the Solr Query Parser) that selects the objects (i.e. the "o" attribute) of all matching triples (i.e. SolrDocument instances) and then calculates the ranges on them.

In this way, we can issue a request like this:

facet.range.q=p:<http://a.b.c#start_year>
facet.range.start=2000
facet.range.end=2010
facet.range.gap=1

or like this

facet.range.q=p:<http://c.d.e#release_date>
facet.range.start=2000-01-10T17:00:00Z
facet.range.end=2010-01-10T17:00:00Z
facet.range.gap=+1MONTH

You can also have more than one facet.range.q parameter. In this case the facet response will look like this:

<lst name="facet_ranges">
    <lst name="p:<http://a.b.c#start_year>">
      <lst name="counts">
          <int name="2000">3445</int>
          <int name="2001">2862</int>
          <int name="2002">2776</int>
          <int name="2003">2865</int>
          ...
       </lst>
       <int name="gap">1</int>
       <int name="start">2000</int>
       <int name="end">2010</int>
    </lst>
    <lst name="p:<http://c.d.e#release_date>">
      <lst name="counts">
          <int name="2000-03-29T17:06:02Z">2516</int>
          <int name="2001-04-03T21:30:00Z">1272</int>
          ...
       </lst>       <int name="gap">+1YEAR</int>
       <int name="start">2000-01-10T17:00:00Z</int>
       <int name="end">2010-01-10T17:00:00Z</int>
    </lst>
    ... 

Aliasing is supported in the same way that has been described for Facet Objects Queries. The same request above with aliases would be:

facet.range.q.1=p:<http://a.b.c#start_year>
&facet.range.q.alias.1=start_year_alias
&facet.range.q.hint.1=num <-- optional as num(eric) is the default value
&facet.range.start.1=2000
&facet.range.end.1=2010
&facet.range.gap.1=1
&facet.range.q.2=p:<http://c.d.e#release_date>
&facet.range.q.alias.2=release_date_alias
&facet.range.q.hint.2=date
&facet.range.start.2=2000-01-10T17:00:00Z
&facet.range.end.2=2010-01-10T17:00:00Z
&facet.range.gap.2=+1MONTH

Note in the response the aliases instead of the full queries:

<lst name="facet_ranges">
    <lst name="start_year_alias">
      <lst name="counts">
          <int name="2000">3445</int>
          <int name="2001">2862</int>
          <int name="2002">2776</int>
          <int name="2003">2865</int>
          ...
       </lst>
       <int name="gap">1</int>
       <int name="start">2000</int>
       <int name="end">2010</int>
    </lst>
    <lst name="release_date_alias">
      <lst name="counts">
          <int name="2000-03-29T17:06:02Z">2516</int>
          <int name="2001-04-03T21:30:00Z">1272</int>
          ...
       </lst>       <int name="gap">+1YEAR</int>
       <int name="start">2000-01-10T17:00:00Z</int>
       <int name="end">2010-01-10T17:00:00Z</int>
    </lst>
    ... 

[1] https://cwiki.apache.org/confluence/display/solr/Faceting
[2] https://wiki.apache.org/solr/SolrFacetingOverview