Scope of this tutorial

This tutorial will teach you how to implement a simple instant search, refreshing the whole view as you type, by using Algolia’s engine and the Android API Client.

Note that as of March 2017, we have released a library to easily build instant search result pages with Algolia, called InstantSearch Android. We highly recommend that you use InstantSearch Android to build your search interfaces. You can get started with either the Getting started guide, the InstantSearch repo or the Example apps.

Let’s see how it goes with a simple use case: searching for movies.

Covered subjects include:

  • displaying results as you type;
  • highlighting the matched words; and
  • infinite scrolling.

The complete code of the resulting project is available on GitHub. We encourage you to clone or browse the repository while reading this tutorial, as some boilerplate parts will not be detailed here.

Dataset

Here is an extract of what the records look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[
  {
    "title": "The Shawshank Redemption",
    "year": 1994,
    "image": "https://image.tmdb.org/t/p/w154/9O7gLzmreU0nGkIB6K3BsJbzvNv.jpg",
    "color": "#8C634B",
    "score": 9.97764206054169,
    "rating": 5,
    "genre": [
      "Drama",
      "Crime"
    ],
    "objectID": "439817390"
  },
  {
    "title": "Guardians of the Galaxy",
    "year": 2014,
    "image": "https://image.tmdb.org/t/p/w154/9gm3lL8JMTTmc3W4BmNMCuRLdL8.jpg",
    "color": "#3D404C",
    "score": 9.965674684060334,
    "rating": 5,
    "genre": [
      "Science Fiction",
      "Fantasy",
      "Adventure"
    ],
    "objectID": "439486910"
  }
]

Initialization

The whole application has been built with Android Studio 1.5, using Gradle build.

New project

The first step is to create a new Android project. For this tutorial, we started from a bare-bones project using File > New > New project..., choosing “Phone and Tablet” as the target devices, and “Empty Activity” as the code template.

Dependencies

Next, you need to declare dependencies in the app module’s build.gradle (caution: not the project-level build.gradle). The following dependencies are needed:

In the end, your dependencies section in your app/build.gradle file should look like this:

1
2
3
4
5
6
7
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.1.1'
    compile 'com.algolia:algoliasearch-android:2.4.0@aar'
    compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
}

Data model

To begin, create the data model reflecting what is stored in the Algolia index. In our case, a single POJO class is enough:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Movie
{
    private String title;
    private String image;
    private int rating;
    private int year;
    public Movie(String title, String image, int rating, int year)
    {
        this.title = title;
        this.image = image;
        this.rating = rating;
        this.year = year;
    }
    public String getTitle() { return title; }
    public String getImage() { return image; }
    public int getRating() { return rating; }
    public int getYear() { return year; }
}

JSON parsing

The next step is to deserialize movie objects from their JSON representation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class MovieJsonParser
{
    public Movie parse(JSONObject jsonObject)
    {
        if (jsonObject == null)
            return null;
        String title = jsonObject.optString("title");
        String image = jsonObject.optString("image");
        int rating = jsonObject.optInt("rating", -1);
        int year = jsonObject.optInt("year", 0);
        if (title != null && image != null && rating >= 0 && year != 0)
            return new Movie(title, image, rating, year);
        return null;
    }
}
public class SearchResultsJsonParser
{
    private MovieJsonParser movieParser = new MovieJsonParser();
    public List<Movie> parseResults(JSONObject jsonObject)
    {
        if (jsonObject == null)
            return null;
        List<Movie> results = new ArrayList<>();
        JSONArray hits = jsonObject.optJSONArray("hits");
        if (hits == null)
            return null;
        for (int i = 0; i < hits.length(); ++i) {
            JSONObject hit = hits.optJSONObject(i);
            if (hit == null)
                continue;
            Movie movie = movieParser.parse(hit);
            if (movie == null)
                continue;
            results.add(movie);
        }
        return results;
    }
}

You might see that the complete code is actually a little more complex than what is shown above; that’s because we will be handling highlighting later on.

UI

The result list

To show the result list, use a simple activity containing just a ListView backed by an in-memory array (i.e. using ArrayAdapter).

Please refer to the complete code for mode details.

To add a search view to the navigation bar for this tutorial, we followed Android’s search tutorial.

Search logic

Now that everything is set up, let’s search!

Initialization

The first thing to do is to initialize the API client with your app ID and API key. This is best done in your activity’s onCreate() method.

We strongly recommend using a search-only API key; please keep your admin key(s) private.

1
2
3
4
5
6
7
8
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        // [...]
        // Init Algolia.
        apiClient = new APIClient("YOUR_APP_ID", "YOUR_API_KEY");
        // [...]
    }

Next, we get a reference to the index that will be searched:

1
        index = apiClient.initIndex("movies");

Building a query

Now it’s time to build a query with all the criteria for a search.

Because there will be a query issued for every letter entered by the user, it is more convenient—and also faster—to re-use the same query object for every search, changing only the criteria that have changed. Therefore, we create the query in the activity’s onCreate() method:

1
2
3
4
5
6
7
8
9
10
11
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        // [...]
        // Pre-build query.
        query = new Query();
        query.setAttributesToRetrieve("title", "image", "rating", "year");
        query.setAttributesToHighlight("title");
        query.setHitsPerPage(20);
        // [...]
    }

Then, in our search() method, we can just update the searched text:

1
query.setQueryString(searchView.getQuery().toString());

Issuing a query

Now that the query is constructed, it can be sent to Algolia using the searchAsync() method. This method takes two arguments:

  1. the query object;
  2. a callback interface that will be notified asynchronously of the search’s outcome (be it results or an error).

You will note that there is no synchronous version of the searchAsync() method. This is because, although Algolia answers lightning-fast 99% of the time, networks are inherently unpredictable—and you don’t want to block the UI thread.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    private void search()
    {
        // [...]
        index.searchASync(query, new SearchListener()
        {
            @Override
            public void searchResult(Index index, Query query, JSONObject jsonResults)
            {
                // TODO: Handle results here.
            }
            @Override
            public void searchError(Index index, Query query, AlgoliaException e)
            {
                // TODO: Any error will be notified here.
            }
        });
    }

Handling results

Results are received in the searchResult() method under the form of a JSON object. Because Algolia is schema less, this is the best the wrapper can do at this stage; you have to write the JSON-parsing code yourself. In this instance, we did this beforehand, so it just needs to be plugged in:

1
2
3
4
5
6
7
8
9
10
            @Override
            public void searchResult(Index index, Query query, JSONObject jsonResults)
            {
                List<Movie> results = resultsParser.parseResults(jsonResults);
                moviesListAdapter.clear();
                moviesListAdapter.addAll(results);
                moviesListAdapter.notifyDataSetChanged();
                // Scroll the list back to the top.
                moviesListView.smoothScrollToPosition(0);
            }

As a reminder, the above code is a dried-up version of the complete code. This is because we will soon be adding collision management, highlighting and infinite scrolling! :)

That’s it! In just a few lines of code, you added as-you-type search to your Android application.

However, although fully functional, our app can still be improved…

Safe-proofing

As mentioned above, networks are unreliable. Consequently, there is a chance (although slim) that search results will be received out of order. The Algolia client cannot guard against this on your behalf, because it would be perfectly acceptable to perform two different searches in parallel. Only your business logic can know that a given query is a more recent version of a previous one.

Fortunately, the fix is easy: we just need to tag every query with a sequence number. If search results arrive and they are not more recent than the latest displayed results, they will be discarded.

Your search code now looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    private int lastSearchedSeqNo;
    private int lastDisplayedSeqNo;
    // [...]
    private void search()
    {
        final int currentSearchSeqNo = ++lastSearchedSeqNo;
        query.setQueryString(searchView.getQuery().toString());
        index.searchASync(query, new SearchListener()
        {
            @Override
            public void searchResult(Index index, Query query, JSONObject jsonResults)
            {
                if (currentSearchSeqNo <= lastDisplayedSeqNo)
                    return;
                List<Movie> results = resultsParser.parseResults(jsonResults);
                moviesListAdapter.clear();
                moviesListAdapter.addAll(results);
                moviesListAdapter.notifyDataSetChanged();
                lastDisplayedSeqNo = currentSearchSeqNo;
                // Scroll the list back to the top.
                moviesListView.smoothScrollToPosition(0);
            }
            // [...]
        });
    }

Highlighting

Algolia comes with a powerful “highlight” feature that enables you to show the user which portions of the results matched their query. This is especially useful when the query contains typos, in which case the match is not 100% identical to the query.

Configuration

To begin, you must first tell Algolia which attributes to highlight. Because highlighting is verbose, be sure to specify only the necessary attributes, so as to keep the JSON payload as light as possible.

1
2
3
4
5
    protected void onCreate(Bundle savedInstanceState)
    {
        // [...]
        query.setAttributesToHighlight(Arrays.asList("title"));
    }

JSON parsing

Algolia returns highlights in a dedicated _highlightResult attribute of the JSON payload. The structure of this attribute mirrors the structure of your data object.

For the complete parsing code, please refer to the SearchResultsJsonParser class.

Rendering

Highlight values are simple HTML text, with an em tag being used to indicate the highlight—unless you specified otherwise at query time or in your index’s configuration.

Displaying them in Android is really straightforward, involving as little code as calling Html.fromHtml() on the value.

However, in our example, we opted for a slightly more elaborate highlighter, which allows to finely tune the text appearance. Please refer to the HighlightRenderer class for more details.

Plugging it all together

Once parsing and rendering are available, plugging everything together is straightforward.

We just need to modify the type of the data objects that we will be handling:

1
List<HighlightedResult<Movie>> results = resultsParser.parseResults(jsonResults);

… and call the renderer in the ListView’s ArrayAdapter:

1
2
3
4
5
6
7
8
9
10
11
12
13
    private class MovieAdapter extends ArrayAdapter<HighlightedResult<Movie>>
    {
        // [...]
        @Override
        public View getView(int position, View convertView, ViewGroup parent)
        {
            // [...]
            HighlightedResult<Movie> result = moviesListAdapter.getItem(position);
            titleTextView.setText(highlightRenderer.renderHighlights(
                result.getHighlight("title").getHighlightedValue()));
            // [...]
        }
    }

Infinite scrolling

So far, we only show the user the first 20 results. This is fine, because we want the most relevant results, and we want them fast.

However, the user might be searching for something that is not in the first 20 results. So what we would like is to have the app automatically fetch the next page of results when the user scrolls the list down, so that the results get a reasonable chance to arrive before the user reaches the bottom, thus providing an “infinite scrolling” experience.

We need to keep track of a few more things now:

1
2
3
    private int lastRequestedPage;
    private int lastDisplayedPage;
    private boolean endReached;

We will monitor the scroll position of the ListView and detect when the user approaches the bottom:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    protected void onCreate(Bundle savedInstanceState)
    {
        // [...]
        moviesListView.setOnScrollListener(this);
        // [...]
    }
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)
    {
        // Abort if list is empty or the end has already been reached.
        if (totalItemCount == 0 || endReached)
            return;
        // Ignore if a new page has already been requested.
        if (lastRequestedPage > lastDisplayedPage)
            return;
        // Load more if we are sufficiently close to the end of the list.
        int firstInvisibleItem = firstVisibleItem + visibleItemCount;
        if (firstInvisibleItem + 5 >= totalItemCount)
            loadMore();
    }

The scroll listener calls a new loadMore() method. As the name implies, this method is in charge of loading the next page of results.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    private void loadMore()
    {
        Query loadMoreQuery = new Query(query);
        loadMoreQuery.setPage(++lastRequestedPage);
        final int currentSearchSeqNo = lastSearchedSeqNo;
        index.searchASync(loadMoreQuery, new SearchListener()
        {
            @Override
            public void searchResult(Index index, Query query, JSONObject jsonResults)
            {
                // Ignore results if they are for an older query.
                if (lastDisplayedSeqNo != currentSearchSeqNo)
                    return;
                List<HighlightedResult<Movie>> results = resultsParser.parseResults(jsonResults);
                if (results.isEmpty()) {
                    endReached = true;
                }
                else {
                    moviesListAdapter.addAll(results);
                    moviesListAdapter.notifyDataSetChanged();
                    lastDisplayedPage = lastRequestedPage;
                }
            }
            // [...]
        });
    }

Because a search might return no results at all, we also need to put the same logic to detect the end of content in the initial search:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    private void search()
    {
        // [...]
        index.searchASync(query, new SearchListener()
        {
            public void searchResult(Index index, Query query, JSONObject jsonResults)
            {
                if (currentSearchSeqNo <= lastDisplayedSeqNo)
                    return;
                List<HighlightedResult<Movie>> results = resultsParser.parseResults(jsonResults);
                if (results.isEmpty()) {
                    endReached = true;
                }
                else {
                    moviesListAdapter.clear();
                    moviesListAdapter.addAll(results);
                    moviesListAdapter.notifyDataSetChanged();
                    lastDisplayedSeqNo = currentSearchSeqNo;
                    lastDisplayedPage = 0;
                }
                // [...]
            }
            // [...]
        }
    }

Now, the list should automatically add more results when the user is approaching the bottom, until there are no more results at all.

See it in action

You now have a nice instant search application, complete with “as you type” search, result highlighting and infinite scrolling.

We encourage you to check out the source code from GitHub and run it yourself.

Filtering your results

There are multiple ways to filter results using Algolia. You can filter by date, by numerical value and by tag. You can also use facets, which filter with the added benefit of being able to retrieve and display the values to filter by.

Filtering

There are several way to filter a result set depending on the attribute you want to filter by:

Filter by Numerical Value

Numerical Values Indexing

Algolia supports indexing of numerical values (integers, doubles and boolean). This can be used for searching for products in a given price range for example.

To enable it, you need to have objects with numerical attributes (ensure your numerical values are not encoded as strings, for boolean we transform false as 0 and true as 1).

Considering the following object with its price attribute:

1
2
3
4
5
6
7
8
9
10
11
12
[
  {
    "title": "Apple MacBook Pro 15.4-Inch Laptop with Retina Display",
    "price": 2594,
    "url": "http://www.amazon.com/Apple-MacBook-ME294LL-15-4-Inch-Display/dp/B0096VD85I"
  },
  {
    "title": "Apple iPhone 5S - 32GB",
    "price": 969.99,
    "url": "http://www.amazon.com/Apple-iPhone-5s-32GB-Space/dp/B00F3J4KYA"
  }
]

You can search with numeric conditions on the price. We support six operators: <, <=, =, >, >= and !=.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// search only with a numeric filter
Query query = new Query();
query.setFilters("price>1000");
index.searchAsync(query, new CompletionHandler() {
    @Override
    public void requestCompleted(JSONObject content, AlgoliaException error) {
        if (content == null) {
            System.err.println("Error: " + error);
            return;
        }
        System.out.println("Results: " + content);
    }
});
// search by query string and numeric filter
query = new Query();
query.setFilters("price>1000");
query.setQuery("los");
index.searchAsync(query, new CompletionHandler() {
    @Override
    public void requestCompleted(JSONObject content, AlgoliaException error) {
        // [...]
    }
});

You can also mix OR and AND operators. The OR operator is defined with a parenthesis syntax (warning: != cannot be ORed). For example (code=1 AND (price:[0-100] OR price:[1000-2000])) translates in:

1
2
3
4
5
6
7
8
9
10
// search by query string and complex numerical conditions
Query query = new Query();
query.setQuery("los");
query.setFilters("code=1 AND (price:1000 TO 3000 OR price:10 TO 100)");
index.searchAsync(query, new CompletionHandler() {
    @Override
    public void requestCompleted(JSONObject content, AlgoliaException error) {
        // [...]
    }
});

Filter by date

By default the engine doesn’t interpret strings following the ISO date format. To enable search by date, you must convert your dates into numeric values. We recommend using a unix timestamp as illustrated by the following record:

1
2
3
4
5
6
{
  "objectID": "myID1",
  "name": "Jimmy",
  "company": "Paint Inc.",
  "date": 1362873600
}

Pro tip: In the case you need to index and filter on dates previous to the 1st of January 1970, you can convert the date following a rule like: year * 10000 + month * 100 + day. The 30th of March 1964 will then become 19640330.

1
2
3
4
5
6
7
8
9
// search by date between 2013-03-10 & 2013-04-20
Query query = new Query();
query.setFilters("date>=1362873600 AND date<=1366416000");
index.searchAsync(query, new CompletionHandler() {
    @Override
    public void requestCompleted(JSONObject content, AlgoliaException error) {
        // [...]
    }
});

Filter by tag

Algolia supports indexing of categories (tags) that you can use when searching for a specific kind of object.

To enable it, you need to index objects with a _tags attribute that contains the list of their categories (you can also use faceting, tags is just a simplified version of faceting).

Here is an example indexing products with tags:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[
  {
    "title": "Apple MacBook Pro 15.4-Inch Laptop with Retina Display",
    "url": "http://www.amazon.com/Apple-MacBook-ME294LL-15-4-Inch-Display/dp/B0096VD85I",
    "_tags": [
      "laptop",
      "computer",
      "retina"
    ]
  },
  {
    "title": "Apple iPhone 5S - 32GB",
    "url": "http://www.amazon.com/Apple-iPhone-5s-32GB-Space/dp/B00F3J4KYA",
    "_tags": [
      "phone",
      "smartphone",
      "retina"
    ]
  }
]

You can then easily search for one or multiple tags, using our filters feature:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// search only by tags
Query query = new Query();
query.setFilters("smartphone AND retina");
index.searchAsync(query, new CompletionHandler() {
    @Override
    public void requestCompleted(JSONObject content, AlgoliaException error) {
        // [...]
    }
});
// search by query string and tags
query = new Query();
query.setQuery("appl");
query.setFilters("smartphone AND retina");
index.searchAsync(query, new CompletionHandler() {
    @Override
    public void requestCompleted(JSONObject content, AlgoliaException error) {
        // [...]
    }
});

You can also mix OR and AND operators. The OR operator is defined with a parenthesis syntax. For example (retina AND (laptop OR smartphone)) translates in:

1
2
3
4
5
6
7
8
9
10
// search by query string and tags
Query query = new Query();
query.setQuery("appl");
query.setFilters("retina AND (smartphone OR laptop)");
index.searchAsync(query, new CompletionHandler() {
    @Override
    public void requestCompleted(JSONObject content, AlgoliaException error) {
        // [...]
    }
});

Negations are also supported via the NOT operator, prefixing the value. For example (retina AND NOT(smartphone)) translates in:

1
2
3
4
5
6
7
8
9
10
// search by query string and tags
Query query = new Query();
query.setQuery("appl");
query.setFilters("retina AND NOT smartphone");
index.searchAsync(query, new CompletionHandler() {
    @Override
    public void requestCompleted(JSONObject content, AlgoliaException error) {
        // [...]
    }
});

Filter by facet

You can also filter your result set using facets. We’ll have a look at facets in the next section.

Faceting

Algolia supports faceting and faceted search (or filtering, navigation). To enable it, you need to set the list of attributes on which you want to enable faceting in the index settings. The attributes can be either numerical or string values.

Here is an example of a book listing:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
[
  {
    "title": "The Hitchhiker's Guide to the Galaxy",
    "authors": "Adams Douglas",
    "type": "Literature & Fiction",
    "url": "http://www.amazon.com/Hitchhikers-Guide-Galaxy-Douglas-Adams/dp/0345391802"
  },
  {
    "title": "Remote: Office Not Required",
    "authors": [
      "Jason Fried",
      "David Heinemeier Hansson"
    ],
    "type": "Business & Investing",
    "url": "http://www.amazon.com/Remote-Office-Required-Jason-Fried/dp/0804137501"
  },
  {
    "title": "Rework",
    "authors": [
      "Jason Fried",
      "David Heinemeier Hansson"
    ],
    "type": "Business & Investing",
    "url": "http://www.amazon.com/Rework-Jason-Fried/dp/0307463745"
  }
]

You can enable faceting on the authors and type attributes with the following code:

set_attributes_for_faceting

In the query you need to specify the list of attributes on which you want to enable faceting (* is a shortcut to enable faceting on all attributes specified in index settings).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// search by query string with faceting on all attributes
Query query = new Query();
query.setQuery("appl");
query.setFacets("*");
index.searchAsync(query, new CompletionHandler() {
    @Override
    public void requestCompleted(JSONObject content, AlgoliaException error) {

    }
});
// search by query string with faceting on authors attribute
query = new Query();
query.setQuery("appl");
query.setFacets("authors");
index.searchAsync(query, new CompletionHandler() {
    @Override
    public void requestCompleted(JSONObject content, AlgoliaException error) {
    }
});
// search by query string with faceting on authors & type attributes
query = new Query();
query.setQuery("appl");
query.setFacets("authors", "type");
index.searchAsync(query, new CompletionHandler() {
    @Override
    public void requestCompleted(JSONObject content, AlgoliaException error) {
    }
});

Filtering / Navigation

Filtering faceting 1

You can implement faceted navigation (or facet filtering) by specifying the list of facet values you want to use as refinements using the filters query parameter:

1
2
3
4
5
6
7
8
9
10
11
// filter on author=Adams Douglas AND type=Literature & Fiction
Query query = new Query();
query.setQuery("appl");
query.setFacets("*");
query.setFilters("authors:\"Adams Douglas\" AND type:\"Literature & Fiction\"")
index.searchAsync(query, new CompletionHandler() {
    @Override
    public void requestCompleted(JSONObject content, AlgoliaException error) {

    }
});

Do not forget to configure your attributesForFaceting index setting with the list of attributes of want to facet on, otherwise you’ll not be able to use it at query-time.

Refinements are ANDed by default (Conjunctive selection).

You can get facets’ counts in the facets attribute of the JSON answer. These might be approximated (linear approximation) based on the size of the index. You can check if they are by using the exhaustiveFacetsCount of the JSON answer.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
  "hits": ["/* many hits */"],
  "page": 0,
  "nbHits": 2,
  "nbPages": 1,
  "hitsPerPage": 20,
  "processingTimeMS": 1,
  "query": "appl",
  "params": "query=appl&facets=*",
  "facets": {
    "authors": {
      "Jason Fried": 2,
      "David Heinemeier Hansson": 1,
      "Adams Douglas": 1
    },
    "type": {
      "Literature & Fiction": 1,
      "Business & Investing": 2
    }
  },
  "exhaustiveFacetsCount": true
}

To OR refinements, you must use nested arrays. For example, to refine on “Business & Investing” books written by Jason Fried or David Heinemeier Hansson:

1
2
3
4
5
6
7
8
9
10
Query query = new Query();
query.setQuery("appl");
query.setFacets("*");
query.setFilters("authors:\"Jason Fried\" OR authors:\"David Heinemeier Hansson\" AND type:\"Business & Investing\"")
index.searchAsync(query, new CompletionHandler() {
    @Override
    public void requestCompleted(JSONObject content, AlgoliaException error) {

    }
});

Negations are also supported via the NOT operator, prefixing the facet value. For example to refine on “Business & Investing” book written by Jason Fried and not David Heinemeir Hanssan:

1
2
3
4
5
6
7
8
9
10
Query query = new Query();
query.setQuery("appl");
query.setFacets("*");
query.setFilters("authors:\"Jason Fried\" AND NOT authors:\"David Heinemeier Hansson\" AND type:\"Business & Investing\"")
index.searchAsync(query, new CompletionHandler() {
    @Override
    public void requestCompleted(JSONObject content, AlgoliaException error) {

    }
});

Disjunctive Faceting

Filtering faceting 2

The most common use case for faceted search or navigation is to select at most one value per facet, but there are at least two ways from which a user might select multiple values from the same facet:

  • Conjunctive “AND” selection (standard Navigation, described above)

  • Disjunctive “OR” selection. Selecting hotel ratings (e.g., hotels with 4 OR 5 stars) may be a kind of disjunctive selection. Checkboxes are usually used to represent such navigation capabilities.

We’ve implemented a JavaScript helper to help you generate such pages:

1
2
3
4
5
6
7
8
9
10
11
Query query = new Query("luxury");
query.setFacets("facilities", "stars");
Map<String, List<String>> refinements = new HashMap<>();
refinements.put("facilities", Arrays.asList("wifi"));
refinements.put("stars", Arrays.asList("4", "5"));
index.searchDisjunctiveFacetingAsync(query, Arrays.asList("stars"), refinements, new CompletionHandler() {
    @Override
    public void requestCompleted(JSONObject content, AlgoliaException error) {
        // [...]
    }
});

The results of a disjunctive faceting search are the same as those of a regular search, except that they contain an additional disjunctiveFacets top-level attribute listing counts for disjunctive facets:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
  "hits": ["/* many hits */"],
  "facets": {
    "facilities": {
      "wifi": 13,
      "swimming pool": 5
    }
  },
  "disjunctiveFacets": {
    "stars": {
      "1": 6,
      "2": 17,
      "3": 24,
      "4": 8,
      "5": 5
    }
  }
}

Disjunctive faceting results in querying several times the index:

  • a query is performed to display the result set ANDing refined conjunctive facets and ORing refined disjunctive facets.

  • a query used to display each disjunctive facet (with the associated number of hits that would be added to the result set if selected) ANDing the refined conjunctive facets.

For example, if a user is looking for an hotel matching the full-text query luxury with 4 OR 5 stars AND with a wifi facility, the following queries must be performed:

  • To display the result set and the conjunctive facet facilities: index.search("luxury", { facets: "facilities", filters: "facilities:wifi AND (stars:4 OR stars:5)" }).
  • To display the disjunctive facet stars: index.search("luxury", { facets: "stars", filters: "facilities:wifi" }).

Aggregations & Stats

All numerical-based facets returns the associated min, max & avg values. The values are available in the facets_stats attribute of the JSON answer.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
  "hits": ["/* many hits */"],
  "page": 0,
  "nbHits": 2,
  "nbPages": 1,
  "hitsPerPage": 20,
  "processingTimeMS": 1,
  "query": "appl",
  "params": "query=appl&facets=*",
  "facets": {
    "price": {
      "42": 4,
      "12": 3,
      "1": 1
    }
  },
  "exhaustiveFacetsCount": true,
  "facets_stats": {
    "price": {
      "min": 1,
      "max": 42,
      "avg": 25.625
    }
  }
}

Did you find this page helpful?