see on GitHub

Faceted Search

When displaying a large amount of data, often paging is used to make viewing the data manageable. However it's also useful to give some context of the entire data-set and a easy way to drill-down into particular categories. The common approach to doing this is "faceted search", as shown in the image below. Note how the count of each category within the current search is across the top.

Facets


To achieve this in RavenDB, lets say you have a document like this:

public class Camera
{
	public DateTime DateOfListing { get; set; }

	public string Model { get; set; }

	public decimal Cost { get; set; }

	public int Zoom { get; set; }

	public double Megapixels { get; set; }

	public bool ImageStabilizer { get; set; }

	public string Manufacturer { get; set; }
}

Step 1

Create an index to work against, this can be setup like so:

public class Cameras_ByManufacturerModelCostDateOfListingAndMegapixels : AbstractIndexCreationTask<Camera>
{
	public Cameras_ByManufacturerModelCostDateOfListingAndMegapixels()
	{
		Map = cameras => from camera in cameras
						 select new
						 {
							 camera.Manufacturer,
							 camera.Model,
							 camera.Cost,
							 camera.DateOfListing,
							 camera.Megapixels
						 };
	}
}

Step 2

Next you need to setup your facet definitions:

List<Facet> facets = new List<Facet>
{
	new Facet
	{
		Name = "Manufacturer"
	},
	new Facet<Camera>
	{
		Name = x => x.Cost,
		Ranges =
		{
			x => x.Cost < 200m,
			x => x.Cost > 200m && x.Cost < 400m,
			x => x.Cost > 400m && x.Cost < 600m,
			x => x.Cost > 600m && x.Cost < 800m,
			x => x.Cost > 800m
		}
	},
	new Facet<Camera>
	{
		Name = x => x.Megapixels,
		Ranges =
		{
			x => x.Megapixels < 3.0,
			x => x.Megapixels > 3.0 && x.Megapixels < 7.0,
			x => x.Megapixels > 7.0 && x.Megapixels < 10.0,
			x => x.Megapixels > 10.0
		}
	}
};

This tells RavenDB that you would like to get the following facets:

  • For the Manufacturer field look at the documents and return a count for each unique Term found
  • For the Cost field, return the count of the following ranges:
    • Cost <= 200.0
    • 200.0 <= Cost <= 400.0
    • 400.0 <= Cost <= 600.0
    • 600.0 <= Cost <= 800.0
    • Cost >= 800.0
  • For the Megapixels field, return the count of the following ranges:
    • Megapixels <= 3.0
    • 3.0 <= Megapixels <= 7.0
    • 7.0 <= Megapixels <= 10.0
    • Megapixels >= 10.0

Step 3

Finally you can write the following code and you get back the data below:

FacetResults facetResults = session
	.Query<Camera, Cameras_ByManufacturerModelCostDateOfListingAndMegapixels>()
	.Where(x => x.Cost >= 100 && x.Cost <= 300)
	.ToFacets(facets);
FacetResults facetResults = session
	.Advanced
	.DocumentQuery<Camera, Cameras_ByManufacturerModelCostDateOfListingAndMegapixels>()
	.WhereBetweenOrEqual(x => x.Cost, 100, 300)
	.ToFacets(facets);
FacetResults facetResults = store
	.DatabaseCommands
	.GetFacets(
		"Cameras/ByManufacturerModelCostDateOfListingAndMegapixels",
		new IndexQuery
		{
			Query = "Cost_Range:[Dx100 TO Dx300]"
		},
		facets);
List<Facet> facets = new List<Facet>
{
	new Facet
	{
		Name = "Manufacturer"
	},
	new Facet<Camera>
	{
		Name = x => x.Cost,
		Ranges =
		{
			x => x.Cost < 200m,
			x => x.Cost > 200m && x.Cost < 400m,
			x => x.Cost > 400m && x.Cost < 600m,
			x => x.Cost > 600m && x.Cost < 800m,
			x => x.Cost > 800m
		}
	},
	new Facet<Camera>
	{
		Name = x => x.Megapixels,
		Ranges =
		{
			x => x.Megapixels < 3.0,
			x => x.Megapixels > 3.0 && x.Megapixels < 7.0,
			x => x.Megapixels > 7.0 && x.Megapixels < 10.0,
			x => x.Megapixels > 10.0
		}
	}
};

The data below represents the sample faceted data that satisfies above query:

{
   Manufacturer: [
      {
         Range: 'canon',
         Count: 42
      },
      {
         Range: 'jessops',
         Count: 50
      },
      {
         Range: 'nikon',
         Count: 46
      },
      {
         Range: 'phillips',
         Count: 44
      },
      {
         Range: 'sony',
         Count: 35
      }
   ],
   Cost_Range: [
      {
         Range: '[NULL TO Dx200.0]',
         Count: 115
      },
      {
         Range: '[Dx200.0 TO Dx400.0]',
         Count: 102
      }
   ],
   Megapixels_Range: [
      {
         Range: '[NULL TO Dx3.0]',
         Count: 42
      },
      {
         Range: '[Dx3.0 TO Dx7.0]',
         Count: 79
      },
      {
         Range: '[Dx7.0 TO Dx10.0]',
         Count: 82
      },
      {
         Range: '[Dx10.0 TO NULL]',
         Count: 14
      }
   ]
}

Storing facets

Alternatively, if you do not have to change your facets dynamically, you can store your facets as FacetSetup document and pass the document Id instead of the list each time:

session.Store(new FacetSetup { Id = "facets/CameraFacets", Facets = facets });

FacetResults facetResults = session
	.Query<Camera, Cameras_ByManufacturerModelCostDateOfListingAndMegapixels>()
	.Where(x => x.Cost >= 100 && x.Cost <= 300)
	.ToFacets("facets/CameraFacets");
FacetResults facetResults = session
	.Advanced
	.DocumentQuery<Camera, Cameras_ByManufacturerModelCostDateOfListingAndMegapixels>()
	.WhereBetweenOrEqual(x => x.Cost, 100, 300)
	.ToFacets("facets/CameraFacets");
FacetResults facetResults = store
	.DatabaseCommands
	.GetFacets(
		"Cameras/ByManufacturerModelCostDateOfListingAndMegapixels",
		new IndexQuery
		{
			Query = "Cost_Range:[Dx100 TO Dx300]"
		},
		"facets/CameraFacets");
List<Facet> facets = new List<Facet>
{
	new Facet
	{
		Name = "Manufacturer"
	},
	new Facet<Camera>
	{
		Name = x => x.Cost,
		Ranges =
		{
			x => x.Cost < 200m,
			x => x.Cost > 200m && x.Cost < 400m,
			x => x.Cost > 400m && x.Cost < 600m,
			x => x.Cost > 600m && x.Cost < 800m,
			x => x.Cost > 800m
		}
	},
	new Facet<Camera>
	{
		Name = x => x.Megapixels,
		Ranges =
		{
			x => x.Megapixels < 3.0,
			x => x.Megapixels > 3.0 && x.Megapixels < 7.0,
			x => x.Megapixels > 7.0 && x.Megapixels < 10.0,
			x => x.Megapixels > 10.0
		}
	}
};

Stale results

The faceted search does not take into account a staleness of an index. You can't wait for non stale results by customizing your query with one of WaitForNonStaleResultsXXX method.