Faceted Search

Our goal for this post is to build a screen that looks something like this for the admin page -

Faceted search

By default, we show first page of unfiltered questions, with Category and Tag facets on the left that show the aggregate counts. As we filter by categories the top matching tags should change along with it’s count. The table also supports sorting and pagination.

Elasticsearch aggregations

To get the counts for our categories and tags we use Elasticsearch aggregations - https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html

A basic aggregation query will give us counts for categories and tags -

GET /questions/_search

{

"from": 0,

"size": 25,

"aggs" : {

"category_counts" : {

"terms": {"field": "categoryIds"}

},

"tag_counts" : {

"terms": {"field": "tags", "size": 10}

}

}

}

The query above will give category counts for all categoryIds and tag counts only for the top 10 tags.

Note that the “from” and “to” will allow us to do paging and return the first 25 matching records.

In order to filter by certain categories, we can add the filter clause. For example, to filter by categoryIds of 1 & 2 -

"filter": { "terms": {"categoryIds": [1, 2]} }

Note that even though we filter by these, our aggregations are still returning counts for all data. To narrow down our aggregations by filtered results, we need to add the filter to the aggregations too. For example, if we need tag counts filtered by categories -

GET /questions/_search

{

"from": 0,

"size": 25,

"filter": { "terms": {"categoryIds": [1, 2]} },

"aggs" : {

"category_counts" : {

"terms": {"field": "categoryIds"}

},

"tag_counts" : {

"terms": {"field": "tags", "size": 10}

},

"top_tags_in_categories": {

"filter" : { "terms": {

"categoryIds": [1, 2]

} },

"aggs": {

"tag_counts" : {

"terms": {"field": "tags", "size": 10}

}}

}

}

}

The above query returns questions for categoryIds 1 & 2, category and tag counts for all records and tag count for categoryIds 1 & 2 also. To further filter by tags, I can add a query filter to the above.

With that, let’s build out our application for this.

Search Criteria and Results

We’ll start by defining two new classes for our search — a SearchCriteria class for our criteria and a SearchResults class to hold our questions data and aggregations coming back from Elasticsearch.

// src/app/model/search-criteria.ts

export class SearchCriteria {

categoryIds: number[];

tags: string[];

status: string; //QuestionStatus

searchInput: string;

sortOrder: string;

constructor() {

this.categoryIds = [];

this.tags = [];

}

} // src/app/model/search-results.ts import {SearchCriteria} from './search-criteria';

import {Question} from './question'; export class SearchResults {

searchCriteria: SearchCriteria;

totalCount: number;

questions: Question[];

categoryAggregation: {[key: number]: number};

tagsCount: {tag: string, count: number}[];

}

We’d then add a function “getSearchResults” to map the search criteria to the Elasticsearch query and another one “getQuestions” to parse out the results to our ESUtils class in the functions folder. The functions can be viewed here.