Skip to main content

Query by Facets

  • A Faceted Search provides an efficient way to explore and navigate through large datasets or search results.

  • Multiple filters (facets) are applied to narrow down the search results according to different attributes or categories.

Facets

Define an index

  • To make a faceted search, a static-index must be defined for the fields you want to query and apply facets on.

  • The examples in this article will be based on the following Class, Index, and Sample Data:

public class Camera
{
public string Manufacturer { get; set; }
public double Cost { get; set; }
public double MegaPixels { get; set; }
public int MaxFocalLength { get; set; }
public int UnitsInStock { get; set; }
}

Facets - Basics

Facets definition:

  • Define a list of facets by which to aggregate the data.

  • There are two Facet types:

    • Facet - returns a count for each unique term found in the specified index-field.
    • RangeFacet - returns a count per range within the specified index-field.
// Define a list of facets to query by:
// ====================================
List<FacetBase> facets = new List<FacetBase>
{
// Define a Facet:
// ===============
new Facet
{
// Specify the index-field for which to get count of documents per unique ITEM
// e.g. get the number of Camera documents for each unique Brand
FieldName = "Brand",
// Set a display name for this field in the results (optional)
DisplayFieldName = "Camera Brand"
},

// Define a RangeFacet:
// ====================
new RangeFacet<Cameras_ByFeatures.IndexEntry>
{
Ranges =
{
// Specify ranges within an index-field in order to get count per RANGE
// e.g. get the number of Camera documents that cost below 200, between 200 & 400, etc...
x => x.Price < 200,
x => x.Price >= 200 && x.Price < 400,
x => x.Price >= 400 && x.Price < 600,
x => x.Price >= 600 && x.Price < 800,
x => x.Price >= 800
},
// Set a display name for this field in the results (optional)
DisplayFieldName = "Camera Price"
}
};

Query the index for facets results:

  • Query the index to get the aggregated facets information.

  • Either:

    • Pass the facets definition from above directly to the query

    • Or - construct a facet using a builder with the Fluent API option, as shown below.

Dictionary<string, FacetResult> results = session
// Query the index
.Query<Cameras_ByFeatures.IndexEntry, Cameras_ByFeatures>()
// Call 'AggregateBy' to aggregate the data by facets
// Pass the defined facets from above
.AggregateBy(facets)
.Execute();

Query results:

  • Query results are Not the collection documents, they are of type:
    Dictionary<string, FacetResult> which is the facets results per index-field specified.

  • Using the sample data from this article, the resulting aggregations will be:

// The resulting aggregations per display name will contain:
// =========================================================

// For the "Camera Brand" Facet:
// "canon" - Count: 1
// "fuji" - Count: 4
// "nikon" - Count: 3
// "olympus" - Count: 2
// "sony" - Count: 2

// For the "Camera Price" Ranges:
// "Price < 200" - Count: 3
// "Price >= 200.0 and Price < 400.0" - Count: 5
// "Price >= 400.0 and Price < 600.0" - Count: 2
// "Price >= 600.0 and Price < 800.0" - Count: 1
// "Price >= 800.0" - Count: 1
// Get facets results for index-field 'Brand' using the display name specified:
// ============================================================================
var brandFacets = results["Camera Brand"];
var numberOfBrands = brandFacets.Values.Count; // 5 unique brands

// Get the aggregated facet value for a specific Brand:
var facetValue = brandFacets.Values[0];
// The brand name is available in the 'Range' property
// Note: value is lower-case since the default RavenDB analyzer was used by the index
Assert.Equal("canon", facetValue.Range);
// Number of documents for 'Canon' is available in the 'Count' property
Assert.Equal(1, facetValue.Count);

// Get facets results for index-field 'Price' using the display name specified:
// ============================================================================
var priceFacets = results["Camera Price"];
var numberOfRanges = priceFacets.Values.Count; // 5 different ranges

// Get the aggregated facet value for a specific Range:
facetValue = priceFacets.Values[0];
Assert.Equal("Price < 200", facetValue.Range); // The range string
Assert.Equal(3, facetValue.Count); // Number of documents in this range

Query further:

  • Typically, after presenting users with the initial facets results which show the available options,
    users can select specific categories to explore further.

  • For example, if the user selects Fuji and Nikon,
    then your next query can include a filter to focus only on those selected brands.

Dictionary<string, FacetResult> filteredResults = session
.Query<Cameras_ByFeatures.IndexEntry, Cameras_ByFeatures>()
// Limit query results to the selected brands:
.Where(x => x.Brand.In("Fuji", "Nikon"))
.AggregateBy(facets)
.Execute();

Facets - Options

Facets definition:

  • Options are available only for the Facet type.

  • Available options:

    • Start - The position from which to send items (how many to skip).
    • PageSize - Number of items to return.
    • IncludeRemainingTerms - Show summary of items that didn't make it into the requested PageSize.
    • TermSortMode - Set the sort order on the resulting items.
// Define the list of facets to query by:
// ======================================
List<FacetBase> facetsWithOptions = new List<FacetBase>
{
// Define a Facet:
new Facet
{
// Specify the index-field for which to get count of documents per unique ITEM
FieldName = "Brand",
// Set some facets options
Options = new FacetOptions
{
// Return the top 3 brands with most items count:
PageSize = 3,
TermSortMode = FacetTermSortMode.CountDesc
}
}
};

Query the index for facets results:

Dictionary<string, FacetResult> results = session
// Query the index
.Query<Cameras_ByFeatures.IndexEntry, Cameras_ByFeatures>()
// Call 'AggregateBy' to aggregate the data by facets
// Pass the defined facets from above
.AggregateBy(facetsWithOptions)
.Execute();

Query results:

// The resulting items will contain:
// =================================

// For the "Brand" Facet:
// "fuji" - Count: 4
// "nikon" - Count: 3
// "olympus" - Count: 2

// As requested, only 3 unique items are returned, ordered by documents count descending:
// Get facets results for index-field 'Brand':
// ===========================================
var brandFacets = results["Brand"];
var numberOfBrands = brandFacets.Values.Count; // 3 brands

// Get the aggregated facet value for a specific Brand:
var facetValue = brandFacets.Values[0];
// The brand name is available in the 'Range' property
// Note: value is lower-case since the default RavenDB analyzer was used by the index
Assert.Equal("fuji", facetValue.Range);
// Number of documents for 'Fuji' is available in the 'Count' property
Assert.Equal(4, facetValue.Count);

Facets - Aggregations

Facets definition:

  • Aggregation of data is available for an index-field per unique Facet or Range item.
    For example:

    • Get the total number of UnitsInStock per Brand
    • Get the highest MegaPixels value for documents that cost between 200 & 400
  • The following aggregation operations are available:

    • Sum
    • Average
    • Min
    • Max
  • Multiple operations can be added on each facet, for multiple fields.

// Define the list of facets to query by:
// ======================================
List<FacetBase> facetsWithAggregations = new List<FacetBase>
{
// Define a Facet:
// ===============
new Facet
{
FieldName = "Brand",
Aggregations =
{
{
// Set the aggregation operation:
FacetAggregation.Sum,
// Create a HasSet specifying the index-fields for which to perform the aggregation
new HashSet<FacetAggregationField>
{
// Get total number of UnitsInStock per Brand
new FacetAggregationField {Name = "UnitsInStock"}
}
},
{
FacetAggregation.Average, new HashSet<FacetAggregationField>
{
// Get average Price per Brand
new FacetAggregationField {Name = "Price"}
}
},
{
FacetAggregation.Min, new HashSet<FacetAggregationField>
{
// Get min Price per Brand
new FacetAggregationField {Name = "Price"}
}
},
{
FacetAggregation.Max, new HashSet<FacetAggregationField>
{
// Get max MegaPixels per Brand
new FacetAggregationField {Name = "MegaPixels"},
// Get max MaxFocalLength per Brand
new FacetAggregationField {Name = "MaxFocalLength"}
}
}
}
},

// Define a RangeFacet:
// ====================
new RangeFacet<Cameras_ByFeatures.IndexEntry>
{
Ranges =
{
x => x.Price < 200,
x => x.Price >= 200 && x.Price < 400,
x => x.Price >= 400 && x.Price < 600,
x => x.Price >= 600 && x.Price < 800,
x => x.Price >= 800
},
Aggregations =
{
{
FacetAggregation.Sum, new HashSet<FacetAggregationField>
{
// Get total number of UnitsInStock for each group of documents per range specified
new FacetAggregationField {Name = "UnitsInStock"}
}
},
{
FacetAggregation.Average, new HashSet<FacetAggregationField>
{
// Get average Price of each group of documents per range specified
new FacetAggregationField {Name = "Price"}
}
},
{
FacetAggregation.Min, new HashSet<FacetAggregationField>
{
// Get min Price of each group of documents per range specified
new FacetAggregationField {Name = "Price"}
}
},
{
FacetAggregation.Max, new HashSet<FacetAggregationField>
{
// Get max MegaPixels for each group of documents per range specified
new FacetAggregationField {Name = "MegaPixels"},
// Get max MaxFocalLength for each group of documents per range specified
new FacetAggregationField {Name = "MaxFocalLength"}
}
}
}
}
};

Query the index for facets results:

Dictionary<string, FacetResult> results = session
// Query the index
.Query<Cameras_ByFeatures.IndexEntry, Cameras_ByFeatures>()
// Call 'AggregateBy' to aggregate the data by facets
// Pass the defined facets from above
.AggregateBy(facetsWithAggregations)
.Execute();

Query results:

// The resulting items will contain (Showing partial results):
// ===========================================================

// For the "Brand" Facet:
// "canon" Count:1, Sum: 30, Name: UnitsInStock
// "canon" Count:1, Min: 200, Average: 200, Name: Price
// "canon" Count:1, Max: 30.4, Name: MegaPixels
// "canon" Count:1, Max: 400, Name: MaxFocalLength
//
// "fuji" Count:4, Sum: 42, Name: UnitsInStock
// "fuji" Count:4, Min: 410, Name: Price
// "fuji" Count:4, Max: 102, Name: MegaPixels
// "fuji" Count:4, Max: 800, Name: MaxFocalLength
//
// etc.....

// For the "Price" Ranges:
// "Price < 200.0" Count:3, Sum: 17, Name: UnitsInStock
// "Price < 200.0" Count:3, Min: 100, Average: 133.33, Name: Price
// "Price < 200.0" Count:3, Max: 32, Name: MegaPixels
// "Price < 200.0" Count:3, Max: 300, Name: MaxFocalLength
//
// "Price < 200.0 and Price > 400.0" Count:5, Sum: 75, Name: UnitsInStock
// "Price < 200.0 and Price > 400.0" Count:5, Min: 200, Average: 252, Name: Price
// "Price < 200.0 and Price > 400.0" Count:5, Max: 40, Name: MegaPixels
// "Price < 200.0 and Price > 400.0" Count:5, Max: 600, Name: MaxFocalLength
//
// etc.....
// Get results for the 'Brand' Facets:
// ==========================================
var brandFacets = results["Brand"];

// Get the aggregated facet value for a specific Brand:
var facetValue = brandFacets.Values[0];
// The brand name is available in the 'Range' property:
Assert.Equal("canon", facetValue.Range);
// The index-field on which aggregation was done is in the 'Name' property:
Assert.Equal("UnitsInStock", facetValue.Name);
// The requested aggregation result:
Assert.Equal(30, facetValue.Sum);

// Get results for the 'Price' RangeFacets:
// =======================================
var priceRangeFacets = results["Price"];

// Get the aggregated facet value for a specific Brand:
facetValue = priceRangeFacets.Values[0];
// The range string is available in the 'Range' property:
Assert.Equal("Price < 200.0", facetValue.Range);
// The index-field on which aggregation was done is in the 'Name' property:
Assert.Equal("UnitsInStock", facetValue.Name);
// The requested aggregation result:
Assert.Equal(17, facetValue.Sum);

Storing facets definition in a document

Define and store facets in a document:

  • The facets definitions can be stored in a document.

  • That document can then be used by a faceted search query.

// Create a FacetSetup object:
// ===========================
FacetSetup facetSetup = new FacetSetup
{
// Provide the ID of the document in which the facet setup will be stored.
// This is optional -
// if not provided then the session will assign an ID for the stored document.
Id = "customDocumentID",

// Define Facets and RangeFacets to query by:
Facets = new List<Facet> {
new Facet()
{
FieldName = "Brand"
}},

RangeFacets = new List<RangeFacet>
{
new RangeFacet<Cameras_ByFeatures.IndexEntry>
{
Ranges =
{
x => x.MegaPixels < 20,
x => x.MegaPixels >= 20 && x.MegaPixels < 30,
x => x.MegaPixels >= 30 && x.MegaPixels < 50,
x => x.MegaPixels >= 50
}
}
}
};

// Store the facet setup document and save changes:
// ================================================
session.Store(facetSetup);
session.SaveChanges();

// The document will be stored under the 'FacetSetups' collection

Query using facets from document:

Dictionary<string, FacetResult> results = session
// Query the index
.Query<Cameras_ByFeatures.IndexEntry, Cameras_ByFeatures>()
// Call 'AggregateUsing'
// Pass the ID of the document that contains your facets setup
.AggregateUsing("customDocumentID")
.Execute();

Syntax

IAggregationQuery<T> AggregateBy<T>(FacetBase facet);
IAggregationQuery<T> AggregateBy<T>(IEnumerable<FacetBase> facets);
IAggregationQuery<T> AggregateBy<T>(Action<IFacetBuilder<T>> builder);
IAggregationQuery<T> AggregateUsing<T>(string facetSetupDocumentKey);
ParameterTypeDescription
facetFacetBaseFacetBase implementation defining the facet and its options.
Either Facet or RangeFacet.
facetsIEnumerable<FacetBase>Enumerable containing FacetBase implementations.
builderAction<IFacetFactory<T>>Builder with a fluent API that constructs a FacetBase instance.
facetSetupDocumentIdstringID of a document containing FacetSetup.
public class Facet
{
public string FieldName { get; set; }
public FacetOptions Options { get; set; }
}

public class Facet<T>
{
public Expression<Func<T, object>> FieldName { get; set; }
public FacetOptions Options { get; set; }
}

Fluent API builder methods:

IFacetOperations<T> ByField(string fieldName);
IFacetOperations<T> ByField(Expression<Func<T, object>> path);
IFacetOperations<T> ByRanges(Expression<Func<T, bool>> path, params Expression<Func<T, bool>>[] paths);
IFacetOperations<T> WithDisplayName(string displayName);
IFacetOperations<T> WithOptions(FacetOptions options);
IFacetOperations<T> SumOn(Expression<Func<T, object>> path);
IFacetOperations<T> MinOn(Expression<Func<T, object>> path);
IFacetOperations<T> MaxOn(Expression<Func<T, object>> path);
IFacetOperations<T> AverageOn(Expression<Func<T, object>> path);
ParameterTypeDescription
fieldNamestringThe index-field to use for the facet
pathExpression<Func<T, bool>>Points to the index-field to use for the facet (ByRanges, ByField) or for the aggregation (SumOn, MinOn, MaxOn, AverageOn)
displayNamestringIf set, results of a facet will be returned under this name
optionsFacetOptionsNon-default options to use in the facet definition

Options:

public class FacetOptions
{
public FacetTermSortMode TermSortMode { get; set; } = FacetTermSortMode.ValueAsc;
public bool IncludeRemainingTerms { get; set; }
public int Start { get; set; }
public int PageSize { get; set; } = int.MaxValue;
}
OptionTypeDescription
TermSortModeFacetTermSortModeSet the sort order on the resulting items
(ValueAsc (Default), ValueDesc, CountAsc, CountDesc)
StartintThe position from which to send items (how many to skip)
PageSizeintNumber of items to return
IncludeRemainingTermsboolIndicates if remaining terms that didn't make it into the requested PageSize should be included in results