Return search hits with category counts.
dotnet add package RecursiveAI.OneIndex --prereleaseRequires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.
using OneIndex; using var index = SearchIndex.InMemory<Item>(); index.Upsert(new Item { Category = "shoes" }); index.Commit(); var results = index.Search() .Facets(x => x.Category).Execute(); foreach (var hit in results) Console.WriteLine(hit.Document.Category); var values = results.Facets["Category"].Values; foreach (var value in values) Console.WriteLine( $"{value.Value}: {value.Count}"); public class Item { [SearchId] public string Id { get; set; } = "1"; [Keyword, Facet] public string Category { get; set; } = ""; }
shoes shoes: 1
Unzip the example, then dotnet run.
The full program
This example uses preview 0.4.0-preview.2; APIs and file formats may change between previews.
The full program indexes this four-product catalog in memory:
- ID 1: “running shoe”; category
shoes. - ID 2: “running shoe”; category
shoes. - ID 3: “running sock”; category
socks. - ID 4: “wool hat”; category
hats.
It returns one hit for “running”, plus counts for every matching product.
using OneIndex; using var index = SearchIndex.InMemory<Product>(); index.Upsert(new Product { Id = "1", Title = "running shoe", Category = "shoes" }); index.Upsert(new Product { Id = "2", Title = "running shoe", Category = "shoes" }); index.Upsert(new Product { Id = "3", Title = "running sock", Category = "socks" }); index.Upsert(new Product { Id = "4", Title = "wool hat", Category = "hats" }); index.Commit(); var results = index.Search("running") .Facets(x => x.Category).Take(1).Execute(); Console.WriteLine($"Matches: {results.TotalHits}"); foreach (var hit in results) Console.WriteLine(hit.Document.Title); foreach (var value in results.Facets["Category"].Values) Console.WriteLine($"{value.Value}: {value.Count}"); public class Product { [SearchId] public string Id { get; set; } = ""; [FullText] public string Title { get; set; } = ""; [Keyword, Facet] public string Category { get; set; } = ""; }
dotnet run prints:
Matches: 3 running shoe shoes: 2 socks: 1
A facet on a full-text field, rather than a supported column field, throws ArgumentException naming the field.
Variations
Replace the query and print loops in the full program with this filtered, counts-only request:
var results = index.Search("running") .Where(x => x.Category == "shoes") .Facets(x => x.Category, maxValues: 1, sortBy: OneIndex.Aggregations.FacetSort.Count) .Take(0).Execute(); Console.WriteLine($"Matches: {results.TotalHits}"); foreach (var value in results.Facets["Category"].Values) Console.WriteLine($"{value.Value}: {value.Count}");
Matches: 2 shoes: 2
Related recipes
Typo-tolerant search in C# · “Did you mean” search suggestions in C# · Full-text search
The short answer
For faceted search in C#, mark the category property with [Keyword, Facet], index your objects, and call Facets(x => x.Category) on the search query. Execute() returns both the requested hits and the value counts in results.Facets["Category"].Values.
OneIndex counts facets over all documents the query matches, not just the returned page. In the full example, the query matches three products. Take(1) returns one title, while the counts remain shoes: 2 and socks: 1. The hat is excluded because its title does not match. These rules are documented in Facets.
What is faceted search?
A facet is a count for each distinct value of a field within a result set. A category facet lets a reader see both the available categories and how many matching documents belong to each. It is not itself a filter: asking for category counts does not remove any hits. A filter restricts the matching documents, and the facet describes the restricted set.
For a catalog UI, keep three values separate: the number of hits displayed on this page, the total number of matching documents, and the count beside each category. Using the displayed page to compute counts hides categories that happen not to occur on that page. OneIndex’s facet collector instead visits every match. Requesting a facet also makes TotalHits exact without a separate CountExactly() call.
Counts need not add up to the total when a property contains multiple values. A document with category values a, a, and b contributes once to a and once to b. Repeated values within a document do not inflate a bucket. Documents with no value contribute nothing; deleted documents do not count.
Eligible fields include keywords, booleans, numbers, and dates with a column, kept through [Facet], [Filterable], or [Sortable]. Full-text, geo, vector, bytes, and JSON fields cannot be faceted. Keep a display category as a keyword property instead of trying to facet an analyzed title.
How do I implement a faceted search example in C#?
Use the full program above to check the relationship between hits and counts before connecting it to a UI. The variation applies a category filter, requests the most common facet value, and returns no hits with Take(0). Its output still reports two matching shoes. This is useful for a counts-only request; it is not a way to avoid visiting matching documents.
maxValues limits the number of returned facet values, not the documents considered while counting. DistinctValues reports how many different values existed before that limit. The default ordering is alphabetical for keywords; count ordering places the most common values first and breaks equal-count ties by the alphabetical order. Those are separate choices from document ranking and pagination.
Filters apply to the facet counts too. If the UI needs category counts that ignore its currently selected category but respect other filters, run a separate query without the category filter. A facet request does not automatically remove that filter for you. For numeric columns, Stats supplies the minimum and maximum over matching values; keyword facets do not have numeric stats. See the facet API rules before choosing how to label your controls.