Guides / Building Search UI / Going further

Turn off Automatic Filtering and Boosting with React InstantSearch

This is the React InstantSearch v7 documentation. React InstantSearch v7 is the latest version of React InstantSearch and the stable version of React InstantSearch Hooks.

If you were using React InstantSearch v6, you can upgrade to v7.

If you were using React InstantSearch Hooks, you can still use the React InstantSearch v7 documentation, but you should check the upgrade guide for necessary changes.

If you want to keep using React InstantSearch v6, you can find the archived documentation.

Automatic Filtering and Boosting applies a category filter when users enter a query. You can use Query Categorization to predict categories for your most popular queries. If you enable Automatic Filtering and Boosting the categories are automatically applied as filters.

Controlling Automatic Filtering and Boosting in your UI

If you want to have Automatic Filtering and Boosting on or off for all searches, you only need to enable the feature to filter the results automatically.

If you want to let users turn Automatic Filtering and Boosting on or off, you can build an InstantSearch widget. This widget should inform users that the results are filtered. The widget should also let users remove any applied filters.

Building a widget for Automatic Filtering and Boosting

There are three steps to building an Automatic Filtering and Boosting widget:

  1. Check the search response. The search response includes the following properties if Automatic Filtering and Boosting is enabled:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
     {
       "extensions": {
         "queryCategorization": {
           "autofiltering": {
             "facetFilters": [],
             "optionalFilters": []
           }
         }
       }
     }
    

    If filters are applied to the query, they’re listed in the facetFilters property.

    1
    2
    3
    4
    5
    6
    7
    
     {
       "facetFilters": [
         "category.level0:Fresh products",
         "category.level1:Fresh vegetables",
         "category.level2:Tomatoes"
       ]
     }
    
  2. Let users remove applied filters. To remove filters applied with Automatic Filtering and Boosting, you need to turn the feature off for the current query using the enableAutoFiltering API parameter.

    1
    2
    3
    4
    5
    6
    7
    
     {
       "extensions": {
         "queryCategorization": {
           "enableAutoFiltering": false
         }
       }
     }
    
  3. Turn on Automatic Filtering and Boosting for new queries. To keep Automatic Filtering and Boosting for other queries, you must first check for a query change and then set enableAutoFiltering to true.

Implementing the widget

The following custom connector implements all three of the preceding steps.

The connector requires the Algolia search helper.

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
// connectAutoFiltering.ts

import type { Connector } from 'instantsearch.js';

export type AutoFilteringConnectorParams = {};

export type AutoFilteringRenderState = {
  appliedFilters: Array<{
    name: string;
    value: string;
  }>;
  cancelAutoFiltering(): void;
};

export type AutoFilteringWidgetDescription = {
  $$type: 'algolia.beta.autoFiltering';
  renderState: AutoFilteringRenderState;
  indexRenderState: {};
  indexUiState: {};
};

type AutoFilteringConnector = Connector<
  AutoFilteringWidgetDescription,
  AutoFilteringConnectorParams
>;

type ConnectorState = {
  disabledQuery?: string;
  cancelAutoFiltering?: AutoFilteringRenderState['cancelAutoFiltering'];
};

// util function to set the query parameter (step 2)
function setAutoFiltering(value, helper) {
  return helper.setQueryParameter('extensions', {
    queryCategorization: {
      enableAutoFiltering: value,
    },
  });
}

type WidgetRenderStateWithResultsExtensions = Parameters<
  ReturnType<ReturnType<AutoFilteringConnector>>['getWidgetRenderState']
>[0] & {
  state: {
    extensions?: {
      queryCategorization?: {
        enableAutoFiltering?: boolean;
      };
    };
  };
  results: {
    extensions?: {
      queryCategorization?: {
        normalizedQuery?: string;
        autofiltering?: {
          facetFilters?: string[];
        };
      };
    };
  };
};

export const connectAutoFiltering: AutoFilteringConnector = (
  renderFn,
  unmountFn = () => {}
) => {
  return function autoFiltering(widgetParams) {
    const connectorState: ConnectorState = {};
    return {
      $$type: 'algolia.beta.autoFiltering',

      init(initOptions) {
        const { instantSearchInstance } = initOptions;

        renderFn(
          {
            ...this.getWidgetRenderState(initOptions),
            instantSearchInstance,
          },
          true
        );
      },

      render(renderOptions) {
        const { instantSearchInstance } = renderOptions;

        renderFn(
          {
            ...this.getWidgetRenderState(renderOptions),
            instantSearchInstance,
          },
          false
        );
      },

      dispose() {
        unmountFn();
      },

      getWidgetUiState(uiState) {
        return uiState;
      },

      getWidgetSearchParameters(searchParameters) {
        return searchParameters;
      },

      getRenderState(renderState, renderOptions) {
        return {
          ...renderState,
          autoFiltering: this.getWidgetRenderState(renderOptions),
        };
      },

      // this is where the logic happens
      getWidgetRenderState({
        results,
        helper,
        state,
      }: WidgetRenderStateWithResultsExtensions) {
        if (!connectorState.cancelAutoFiltering) {
          connectorState.cancelAutoFiltering = () => {
            // Disable auto filtering for next search
            setAutoFiltering(false, helper);
            helper.search();

            // storing in the state the disabled query
            connectorState.disabledQuery = helper.getQuery().query;
          };
        }

        // empty results case
        if (!results) {
          return {
            appliedFilters: [],
            cancelAutoFiltering: () => {},
            widgetParams,
          };
        }

        // enabling back auto filtering if the query has changed (step 3)
        if (
          // "state" stores the current query parameters
          state.extensions?.queryCategorization?.enableAutoFiltering ===
            false &&
          connectorState.disabledQuery &&
          results.query !== connectorState.disabledQuery
        ) {
          setAutoFiltering(true, helper);

          if (results.extensions.queryCategorization.normalizedQuery) {
            // if the current query has predicted categories, we refine the search with autofiltering enabled
            helper.search();
          }
        }

        // Retrieving the applied filters (step 1)
        const facetFilters = results.extensions?.queryCategorization?.autofiltering?.facetFilters || [];

        return {
          appliedFilters:
            facetFilters.map((facetFilter) => ({
              name: facetFilter.split(':')[0],
              value: facetFilter.split(':')[1],
            })),
          cancelAutoFiltering: connectorState.cancelAutoFiltering,
          widgetParams,
        };
      },
    };
  };
};

Using the useConnector hook, you can then build your own useAutoFiltering hook.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// useAutoFiltering.ts

import { useConnector } from 'react-instantsearch';
import { connectAutoFiltering } from './connectAutoFiltering';

import type { AdditionalWidgetProperties } from 'react-instantsearch';
import type {
  AutoFilteringConnectorParams,
  AutoFilteringWidgetDescription,
} from './connectAutoFiltering';

export type AutoFilteringParams = AutoFilteringConnectorParams;

export function useAutoFiltering(
  props?: AutoFilteringParams,
  additionalWidgetProperties?: AdditionalWidgetProperties
) {
  return useConnector<
    AutoFilteringConnectorParams,
    AutoFilteringWidgetDescription
  >(connectAutoFiltering, props, additionalWidgetProperties);
}

And finally, you can build the UI around the hook.

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
39
40
41
// Autofiltering.tsx

import React from 'react';
import { useAutoFiltering } from './useAutoFiltering';

export function AutoFiltering() {
  const { appliedFilters, cancelAutoFiltering } = useAutoFiltering(
    {},
    {
      $$widgetType: 'algolia.beta.autoFiltering',
    }
  );

  /**
   * Here you choose to display the applied filters in a banner with a button to disable autofiltering.
   * Only the last filter of the hierarchy is displayed here, but you can choose to display all of them if you want to.
   * It is entirely up to you how you choose to render the filters.
   */
  if (appliedFilters.length === 0) {
    return null;
  }

  return (
     <div
       style={{
         border: '1px solid lightgray',
         borderRadius: '3px',
         margin: '1rem 0',
         padding: '1rem',
       }}
     >
       <span>
         <strong>Applied filter:</strong>{' '}
       </span>
       <span>
         {appliedFilters.pop().value}{' '}
         <button onClick={() => cancelAutoFiltering()}>&times;</button>
       </span>
     </div>
   )
}

If you have a standard React InstantSearch implementation, you can copy and paste both the preceding snippets into your app. However, ensure you tweak the final <AutoFiltering /> component to fit your design.

You can then place <AutoFiltering /> in your app.