Usage

The Coveo Headless library acts as a middle-layer for applications, opening a line of communication between the UI elements and the Coveo Platform.

A project built on top of Headless will typically involve two main building-blocks: the engine, which manages the state of the search interface and communicates with the Coveo Platform, and the controllers, which dispatch actions to the engine based on user interactions.

To create a starter Angular, React, or Vue.js project with a Coveo Headless-powered search page, check out the Coveo CLI. The CLI can handle several steps for you.

This article provides an overview of the core Headless concepts.

Install Headless

Use npm to install the Headless library.

npm install @coveo/headless

Headless requires Node.js version 12 or above.

Configure a Headless Engine

In order to start building an application on top of the Headless library, you must first initialize a Headless engine using one of the builder functions, each of which is dedicated to a specific use case. Each builder function is imported from a dedicated sub-package that has the relevant exports for its use case. The available engine builder functions are:

  • buildSearchEngine (imported from '@coveo/headless')

  • buildRecommendationEngine (imported from '@coveo/headless/recommendation')

  • buildProductRecommendationEngine (imported from '@coveo/headless/product-recommendation').

You will specify your search endpoint configuration through this instance (i.e., where to send Coveo search requests, and how to authenticate them).

For testing purposes, you can pass the sample configuration for the engine builder function that you’re calling:

// app/Engine.ts
 
import { buildSearchEngine, getSampleSearchEngineConfiguration } from '@coveo/headless';
// import {buildRecommendationEngine, getSampleRecommendationEngineConfiguration} from '@coveo/headless/recommendation';
// import {buildProductRecommendationEngine, getSampleProductRecommendationEngineConfiguration} from '@coveo/headless/product-recommendation';
 
export const headlessEngine = buildSearchEngine({
  configuration: getSampleSearchEngineConfiguration()
});
 
// export const recommendationEngine = buildRecommendationEngine({
//   configuration: getSampleRecommendationEngineConfiguration()
// });
 
// export const productRecommendationEngine = buildProductRecommendationEngine({
//   configuration: getSampleProductRecommendationEngineConfiguration()
// });

However, most of the time, your initialization and export will look like this:

// app/Engine.ts
 
import { buildSearchEngine } from '@coveo/headless';
 
export const headlessEngine = buildSearchEngine({
  configuration: {
    organizationId: '<ORGANIZATION_ID>', (1)
    accessToken: '<ACCESS_TOKEN>', (2)
    renewAccessToken: <CALLBACK>, (3)
  }
});

where:

1 <ORGANIZATION_ID> (string) is the unique identifier of your Coveo organization (e.g., mycoveoorganizationa1b23c).
2 <ACCESS_TOKEN> (string) is an API key or search token that grants the Allowed access level on the Execute Queries domain, as well as the Push access level on the Analytics Data domain in the target organization.
3 <CALLBACK> (function) returns a new access token, usually by fetching it from a backend service that can generate search tokens. The engine will automatically run this function when the current access token expires (i.e., when the engine detects a 419 Authentication Timeout HTTP code). You don’t need to specify a renewAccessToken callback if your application is using API key authentication (which is typically not recommended, but can be legitimate in a some scenarios).

For more information on the Headless engines, see the reference documentation.

Use Headless Controllers

A Headless controller is an abstraction that simplifies the implementation of a specific UI feature or component. In other words, controllers provide intuitive programming interfaces for interacting with the Headless engine’s state.

For most use cases, we recommend that you use controllers to interact with the state.

EXAMPLE

To implement a search box UI component, you decide to use the SearchBox controller, available in the Headless Search Engine.

This controller exposes various public methods such as updateText and submit. You write code to ensure that when the end user types in the search box, the Searchbox controller’s updateText method is called. Under the hood, calling this method dispatches some actions. The Headless Search Engine’s reducers react to those actions by updating the state as needed.

In this case, the query property of the state is updated. Then, the controller fetches new query suggestions from the Search API, and updates the querySuggestState property.

For detailed information on the various Headless controllers, see the reference documentation.

Initialize a Controller Instance

You can initialize a Headless Controller instance by calling its builder function.

A controller’s builder function always requires a Headless engine instance as a first argument.

// src/Components/MySearchBox.ts
 
import { SearchBox, buildSearchBox } from '@coveo/headless';
import { engine } from '../Engine';
 
const mySearchBox: SearchBox = buildSearchBox(engine);

Many builder functions also accept, and sometimes require, an options object as a second argument.

// src/Components/MyStandaloneSearchBox.ts
 
import { SearchBox, SearchBoxOptions, buildSearchBox } from '@coveo/headless';
import { engine } from '../Engine';
 
const options: SearchBoxOptions = { (1)
  numberOfSuggestions: 3,
  isStandalone: true
}
 
const myStandaloneSearchBox: SearchBox = buildSearchBox(engine, { options });
1 None of the SearchBox controller options are required, but you can use them to tailor the controller instance to your needs.
// src/Components/MyAuthorFacet.ts
 
import { Facet, FacetOptions, buildFacet } from '@coveo/headless';
import { engine } from '../Engine';
 
const options: FacetOptions = { field: "author" }; (1)
 
const myAuthorFacet: Facet = buildFacet(engine, { options });
1 Specifying a field value in the options object is required to initialize a Facet controller instance.

The options that are available on each controller are detailed in the reference documentation.

Interact With a Controller

The different Headless engines expose different controllers, and each controller exposes a public interface which you can use to interact with its Headless engine’s state.

When you call a method on a controller instance, one or more actions are dispatched. The target Headless engine’s reducers listen to those actions, and react to them by altering the state as necessary.

Calling some of the SearchBox controller’s methods.

import { SearchBox, buildSearchBox } from '@coveo/headless';
import { engine } from '../Engine';
const mySearchBox: SearchBox = buildSearchBox(engine);
 
searchBox.updateText('hello'); (1)
 
searchBox.selectSuggestion(searchBox.state.suggestions[0].value) (2)
1 Dispatches actions to update the query to hello, and fetch new query suggestions.
2 Dispatches actions to set the query to the value of the first query suggestion (e.g., hello world), and execute that query.

The methods that are available on each controller are detailed in the reference documentation.

Subscribe to State Changes

You can use the subscribe method on a controller instance to listen to its state changes.

The listener function you pass when calling the subscribe method will be executed every time an action is dispatched.

import { SearchBox, buildSearchBox } from '@coveo/headless';
import { engine } from '../Engine';
const mySearchBox: SearchBox = buildSearchBox(engine);
 
// ...
 
function onSearchBoxUpdate() {
  const state = mySearchBox.state;
  // ...Do something with the updated SearchBox state...
}
 
const unsubscribe = mySearchBox.subscribe(onSearchBoxUpdate); (1)
 
searchBox.updateText('hello'); (2)
 
// ...
 
unsubscribe(); (3)
1 Every time the portion of the state that’s relevant for the SearchBox controller changes, the onSearchBoxUpdate function will be called.
2 This will trigger a SearchBox state change.
3 The subscribe method returns a function for unregistering the listener, so you can call unsubscribe to stop listening to state updates (e.g., when the controller is deleted).

You can also call the subscribe method from your Headless engine instance to listen to all state changes.

import { engine } from '../Engine';
 
function onStateUpdate() {
  const state = engine.state;
  // ...Do something with the updated state...
}
 
const unsubscribe = engine.subscribe(onStateUpdate);
 
// ...
 
unsubscribe();

Initialize Your Interface

Once you’ve initialized your engine and controllers, you may want to set initial search parameters, if needed, before synchronizing the search parameters with values retrieved from the URL and finally triggering the first request. If your application does not need to modify initial search parameters, then the next step would be to trigger the first request.

In any case, be sure to modify initial search parameters, and then to synchronize with the URL, only after the engine and controllers have been initialized, lest you face timing problems. To avoid this kind of issue, modern frameworks expose purpose-built lifecycle methods. In React, there is componentDidMount, in Vue.js, there is mounted, and in Angular there is ngAfterViewInit.

// app/SearchPage.tsx
import { headlessEngine } from './Engine'; (1)
import { mySearchBox } from './Components/MySearchBox.ts'; (2)
import { myAuthorFacet } from './Components/MyAuthorFacet.ts';
// ...
 
export default class App extends React.Component {
 
  render() {
    return (
      <div>
        <!--- ... --->
          <mySearchBox /> (3)
        <!--- ... --->
          <myAuthorFacet />
        <!--- ... --->
      <div>
    )
  }
 
  componentDidMount() {
    this.engine.executeFirstSearch(); (4)
  }
 
  // ...
}
1 Import the engine you created above.
2 Import the search box and author facet you created above. Recall that you pass the engine to each of those components.
3 Use your components to build your interface.
4 The componentDidMount method allows you to wait until the mySearchBox and myAuthorFacet components have been initialized and have registered their state on the engine. You can therefore execute the first search. If you wanted to modify search parameters and to synchronize them with values retrieved from the URL, you would do it in this block, before executing the first search.

Dispatch Actions

You will often use controller methods to interact with the state. However, you may sometimes want to dispatch actions on your own. You can create actions using action loaders and dispatch them using the Headless engine’s dispatch method.

import { engine } from './engine';
import { loadFieldActions } from '@coveo/headless';
 
const FieldActionCreators = loadFieldActions(engine); (1)
const action = FieldActionCreators.registerFieldsToInclude(['field1', 'field2']); (2)
 
engine.dispatch(action); (3)
1 The action loader loadFieldActions allows you to use field actions. Calling this function will add the necessary reducers to the engine, if they haven’t been added already, and return an object holding the relevant action creator functions.
2 To create a dispatchable action, use the action creators that were loaded in the previous line. In this case, the registerFieldsToInclude method takes field names as parameters and returns an action that, when dispatched, will cause those fields to be returned as part of each search result.
3 Dispatch the action using the engine’s dispatch method.

Every action dispatch triggers the corresponding listener function. Consequently, you should only perform costly operations, such as rendering a UI component, when the state they depend on has changed.

For more information on the various actions you can dispatch, see the reference documentation.

What's Next for Me?