Implement filter suggestions and instant products (CSR)

This is for:

Developer

To improve the functionality of your search box, you can implement filter suggestions and instant products.

  • Filter suggestions are a content discovery feature connected to search boxes and facets. When end-users start typing in a search box, Coveo for Commerce suggests relevant fields and field values to use as filters. The end-user can select one of these filter suggestions to filter the results with a facet on the selected field value.

    Filter suggestions demo
  • Instant products enhance the search experience by displaying relevant products in real time as users type in the search box. The end-user can then select a product straight from the search box.

    Instant results demo

This article explains how to implement filter suggestions and instant products in Coveo for Commerce with Coveo Headless in a client-side rendering scenario. It uses an example from the Coveo Headless repository.

Prerequisite

You must have created a Predictive Query Suggestion model or Query Suggestion model, and have implemented query suggestions in your search box.

Enable filter suggestions

Important

Filter suggestions in Headless are currently behind a feature flag. Contact your Coveo team to enable it.

To leverage filter suggestions in Headless, set useInFieldSuggestions to true in the target facets in the target search configuration. See Using the search configuration endpoints.

Only hierarchical and regular facets support filter suggestions.

{
    "id": "644e1c29-ae95-4cdc-965b-95e9a2c0d2e5",
    // ...
    "queryConfiguration": {
        "additionalFields": [
            "ec_ball_weight",
            "ec_traditional_design"
        ],
        "facets": {
            "enableIndexFacetOrdering": false,
            "freezeFacetOrder": false,
            "facets": [
                {
                    "type": "hierarchical",
                    "facetId": "ec_category",
                    "field": "ec_category",
                    "displayNames": [
                        {
                            "value": "Categories",
                            "language": "en"
                        }
                    ],
                    "numberOfValues": 10,
                    "delimitingCharacter": "|",
                    "basePaths": [],
                    "filterByBasePath": false,
                    "useEssentialFilterRuleAsBasePath": false,
                    "preventAutoSelect": false,
                    "retrieveCount": 5,
                    "isFieldExpanded": false,
                    "useInFieldSuggestions": true 1
                }
                // ...
            ]
        },
        // ...
    },
    // ...
}
1 The useInFieldSuggestions property is set to true for the target facets. This enables the facet for use in filter suggestions. You can enable multiple facets for filter suggestions.

Headless implementation

Implement these features in Coveo Headless as follows:

This section explains the key parts of the implementation.

Initialize the target controllers

In the pages where you include your search box and your standalone search box, initialize the InstantProducts and FilterSuggestionsGenerator controllers and pass them to the search box and standalone search box components.

See the layout.tsx file in the sample project for a full example.

// src/layout/layout.tsx
// ...

  const standaloneSearchBoxId = 'standalone-search-box';

  // ...

  <StandaloneSearchBox
    navigate={navigate}
    controller={buildStandaloneSearchBox(engine, {
      options: {
        redirectionUrl: '/search',
        id: standaloneSearchBoxId,
        highlightOptions,
      },
    })}
    instantProductsController={buildInstantProducts( 1
      engine
    )}
    filterSuggestionsGeneratorController={buildFilterSuggestionsGenerator( 2
      engine
    )}
  />
// ...
1 Uses the buildInstantProducts function to create the InstantProducts controller.
2 Uses the buildFilterSuggestionsGenerator function to create the FilterSuggestionsGenerator controller.

Implement a filter suggestion generator component

To display filter suggestions in your search box components, implement a component that displays the filter suggestions using the FilterSuggestions controller.

// src/components/filter-suggestions/filter-suggestions-generator.tsx

import {
  FilterSuggestionsGenerator as HeadlessFilterSuggestionsGenerator,
  FilterSuggestions as HeadlessFilterSuggestions,
  CategoryFilterSuggestions,
  RegularFacetSearchResult,
  CategoryFacetSearchResult,
} from '@coveo/headless/commerce';
import {useEffect, useState} from 'react';
import FilterSuggestions from './filter-suggestions.js'; 1

interface IFilterSuggestionsGeneratorProps {
  controller: HeadlessFilterSuggestionsGenerator;
  onClickFilterSuggestion: ( 2
    controller: HeadlessFilterSuggestions | CategoryFilterSuggestions,
    value: RegularFacetSearchResult | CategoryFacetSearchResult
  ) => void;
}

export default function FilterSuggestionsGenerator(
  props: IFilterSuggestionsGeneratorProps
) {
  const {controller, onClickFilterSuggestion} = props;
  const [filterSuggestionsState, setFilterSuggestionsState] = useState(
    controller.filterSuggestions
  );

  useEffect(() => {
    controller.subscribe(() => {
      setFilterSuggestionsState(controller.filterSuggestions);
    });
  }, [controller]);

  if (filterSuggestionsState.length === 0) {
    return null;
  }

  return (
    <div className="FilterSuggestionsGenerator">
      {filterSuggestionsState.map((filterSuggestionsController) => {
        return (
          <FilterSuggestions
            key={filterSuggestionsController.state.facetId}
            controller={filterSuggestionsController}
            onClickFilterSuggestion={onClickFilterSuggestion}
          />
        );
      })}
    </div>
  );
}
1 The FilterSuggestions component will display the filter suggestions for a given facet. You’ll define it just below.
2 The onClickFilterSuggestion function is called when the user clicks a filter suggestion. It’s passed as a prop and you’ll define it below.

The FilterSuggestions component is defined as follows.

// src/components/filter-suggestions/filter-suggestions.tsx

import {
  CategoryFacetSearchResult,
  CategoryFilterSuggestions,
  FilterSuggestions as HeadlessFilterSuggestions,
  RegularFacetSearchResult,
} from '@coveo/headless/commerce';
import {useEffect, useState} from 'react';

interface IFilterSuggestionsProps {
  controller: HeadlessFilterSuggestions | CategoryFilterSuggestions;
  onClickFilterSuggestion: (
    controller: HeadlessFilterSuggestions | CategoryFilterSuggestions,
    value: RegularFacetSearchResult | CategoryFacetSearchResult
  ) => void;
}

export default function FilterSuggestions(props: IFilterSuggestionsProps) {
  const {controller, onClickFilterSuggestion} = props;

  const [state, setState] = useState(controller.state);

  useEffect(() => {
    controller.subscribe(() => {
      setState(controller.state);
    });
  }, [controller]);

  if (state.values.length === 0) {
    return null;
  }

  const renderFilterSuggestionButton = (
    value: RegularFacetSearchResult | CategoryFacetSearchResult
  ) => {
    return (
      <button onClick={() => onClickFilterSuggestion(controller, value)}>
        Search for <em>{state.query}</em>{' '}
        {controller.type === 'hierarchical' ? 'in' : 'with'}{' '} 1
        <b>{state.displayName}</b> <em>{value.displayValue}</em> ({value.count}{' '}
        products)
      </button>
    );
  };

  return (
    <div className="FilterSuggestions">
      <p>
        <b>{state.displayName}</b> suggestions
      </p>
      <ul>
        {state.values.map((value) => (
          <li
            key={
              'path' in value
                ? [...value.path, value.rawValue].join(';')
                : value.rawValue
            }
          >
            {renderFilterSuggestionButton(value)}
          </li>
        ))}
      </ul>
    </div>
  );
}
1 This component supports both hierarchical and regular facets.

Implement an instant products component

To display instant products in your search box components, implement a component that displays the instant products using the InstantProducts controller.

// src/components/instant-products/instant-products.tsx

import {
  InstantProducts as HeadlessInstantProducts,
  Product,
} from '@coveo/headless/commerce';
import {useEffect, useState} from 'react';

interface IInstantProductProps {
  controller: HeadlessInstantProducts;
  navigate: (pathName: string) => void;
}

export default function InstantProducts(props: IInstantProductProps) {
  const {controller, navigate} = props;
  const [state, setState] = useState(controller.state);

  useEffect(
    () => controller.subscribe(() => setState({...controller.state})),
    [controller]
  );

  if (state.products.length === 0 || !state.query) {
    return null;
  }

  const onClickProduct = (product: Product) => {
    controller.interactiveProduct({options: {product}}).select();

    // navigate to the product page
  };

  return (
    <div className="InstantProducts">
      {state.products.length === 0 ? (
        <p className="NoInstantProducts">
          No instant products for query <b>{state.query}</b>
        </p>
      ) : (
        <>
          <p>
            Instant products for query <b>{state.query}</b>
          </p>
          <ul className="InstantProducts">
            {state.products.map((product, index) => (
              <li className="Product" key={index}>
                <button onClick={() => onClickProduct(product)}>
                  {product.ec_name} ({product.ec_product_id})
                </button>
              </li>
            ))}
          </ul>
        </>
      )}
    </div>
  );
}

In your search box and standalone search box components, use the FilterSuggestionsGenerator component to display the filter suggestions and the InstantProducts component to display the instant products.

For example, see the standalone-search-box.tsx file in the sample project.

// src/components/standalone-search-box/standalone-search-box.tsx
// ...

interface IStandaloneSearchBoxProps {
  navigate: (url: string) => void;
  ssbController: HeadlessStandaloneSearchBox;
  instantProductsController: HeadlessInstantProducts;
  filterSuggestionsGeneratorController: HeadlessFilterSuggestionsGenerator;
}
export default function StandaloneSearchBox(props: IStandaloneSearchBoxProps) {
  const {
    navigate,
    ssbController,
    instantProductsController,
    filterSuggestionsGeneratorController,
  } = props;

  const [ssbState, setSsbState] = useState(ssbController.state);
  const [isDropdownVisible, setIsDropdownVisible] = useState(false);

  // ...

  const fetchFilterSuggestions = (value: string) => { 1
    for (const filterSuggestions of filterSuggestionsGeneratorController.filterSuggestions) {
      filterSuggestions.updateQuery(value);
    }
  };

  const clearFilterSuggestions = () => { 2
    for (const filterSuggestions of filterSuggestionsGeneratorController.filterSuggestions) {
      filterSuggestions.clear();
    }
  };

  // ...

  const onSearchBoxInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    // ...
    ssbController.updateText(e.target.value);
    // ...
    fetchFilterSuggestions(e.target.value); 3
    instantProductsController.updateQuery(e.target.value); 4
  };

    const onSearchBoxInputKeyDown = (
    e: React.KeyboardEvent<HTMLInputElement>
  ) => {
    switch (e.key) {
      case 'Escape':
        // ...
        if (ssbState.value !== '') {
          // ...
          clearFilterSuggestions(); 5
          instantProductsController.updateQuery('');
          break;
        }
        break;
      // ...
    }
  };

  const onClickSearchBoxClear = () => {
    // ...
    clearFilterSuggestions();
    instantProductsController.updateQuery(state.value);
  };

  const onFocusSuggestion = (suggestion: Suggestion) => {
    // ...
    fetchFilterSuggestions(suggestion.rawValue);
    instantProductsController.updateQuery(suggestion.rawValue);
  };

  // ...
  const renderDropdown = () => { 6
    return (
      // ...
      <FilterSuggestionsGenerator
        controller={filterSuggestionsGeneratorController}
        onClickFilterSuggestion={( 7
          controller: FilterSuggestions | CategoryFilterSuggestions,
          value: RegularFacetSearchResult | CategoryFacetSearchResult
        ) => {
          hideDropdown();
          const parameters =
            controller.type === 'hierarchical'
              ? controller.getSearchParameters(
                  value as CategoryFacetSearchResult
                )
              : controller.getSearchParameters(
                  value as RegularFacetSearchResult
                );
          navigate(`/search#${parameters}`); 8
        }}
        //...
      />
      <div className="InstantProducts column small">
        <InstantProducts
          controller={instantProductsController}
          navigate={navigate}
        />
      </div>
    )
  };

  return (
    <div className="Searchbox">
      <input
        className="SearchBoxInput"
        onChange={onSearchBoxInputChange} 9
        onKeyDown={onSearchBoxInputKeyDown}
        ref={searchInputRef}
        value={ssbState.value}
      />
      <button
        className="SearchBoxClear"
        disabled={
          ssbState.isLoadingSuggestions || ssbState.isLoading || ssbState.value === ''
        }
        onClick={onClickSearchBoxClear}
        type="reset"
      >
      // ...
      {isDropdownVisible && renderDropdown()} 6
    </div>
  );
  </div>
1 Defines a function to fetch filter suggestions.
2 Similarly, defines a function to clear the filter suggestions.
3 The onSearchBoxInputChange function is called when the user types in the search box. Update it to call fetchFilterSuggestions after the standalone search box controller has updated the engine state with the new query.
4 Similarly, update the instantProductsController with the new query.
5 Use the clearFilterSuggestions functions to clear and fetch the filter suggestions when the user selects the Escape key. Also, update the instantProductsController with an empty query. Similar logic applies below for other user interactions, such as selecting the clear button.
6 The renderDropdown function is called to render the filter suggestions dropdown menu. Display the FilterSuggestionsGenerator and InstantProducts components in the dropdown menu.
7 Define the function to handle the click on a filter suggestion and pass it to the FilterSuggestionsGenerator component. It hides the dropdown menu and navigates to the search page with the chosen filter value selected.
8 Since this is a standalone search box, the navigate function is used to navigate to the search page with the selected filter.
9 When users change the search box input, onSearchBoxInputChange is called, which updates the filter suggestions by calling fetchFilterSuggestions. Also, update the instantProductsController with the new query. Similar logic applies below for other other interactions, such as selecting the Escape key or selecting the clear button.