Guides / Building Search UI / UI & UX patterns / Query Suggestions

Building Query Suggestions UI

You are reading the documentation for Angular InstantSearch v3, which is in beta. You can find the v2 documentation here.

Overview

In order to help users with their search, Algolia provides a feature called Query Suggestions. This feature creates an index with the best queries done by your users. This index can be used to provide your users with suggestions as they are typing into the ais-search-box. The advantage of this feature is that, once you’ve configured the generation of the index, it’s a matter of querying another index to provide the user with suggestions. This can easily be done by implementing a multi-index search.

In this guide we will cover the use case where a search box displays a list of suggestions along with the associated categories. Once the user select a suggestion both the query and the category will be applied.

We won’t cover in too much details how to integrate an autocomplete with Angular InstantSearch. The autocomplete guide has already a dedicated section on that topic.

Refine your results with the suggestions

Step 1: set up our boilerplate for displaying results

Let’s start with a regular instant_search boilerplate as generated by create-instantsearch-app.

1
2
3
4
5
6
7
8
9
10
11
<!-- app.component.html -->
<ais-instantsearch [config]="configResults">
  <ais-configure [searchParameters]="searchParameters"></ais-configure>
  <ais-hits>
    <ng-template let-hits="hits" let-results="results">
      <div *ngFor="let hit of hits">
        <ais-highlight attribute="name" [hit]="hit"></ais-highlight>
      </div>
    </ng-template>
  </ais-hits>
</ais-instantsearch
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { Component } from '@angular/core';
import algoliasearch from 'algoliasearch';

const searchClient = algoliasearch(
  'latency',
  '6be0576ff61c053d5f9a3225e2a90f76'
);

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  public configResults = {
    indexName: 'instant_search',
    searchClient,
  };

  public searchParameters = { query: '' };
}

Step 2: fetching suggestions

We will be using the Autocomplete component from Angular Meterial, which we’ll connect with the autocomplete connector. You can find more information in the guide on autocomplete

Let’s create component AutocompleteComponent extending the BaseWidget class with the autocomplete connector.

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
// autocomplete.component.js
import {
  Component,
  Inject,
  forwardRef,
} from "@angular/core";
import { BaseWidget, NgAisInstantSearch } from "angular-instantsearch";
import { connectAutocomplete } from "instantsearch.js/es/connectors";

@Component({
  selector: "app-autocomplete",
  template: ``
})
export class AutocompleteComponent extends BaseWidget {
  state: {
    query: string;
    refine: Function;
    indices: object[];
  };

  constructor(
    @Inject(forwardRef(() => NgAisInstantSearch))
    public instantSearchParent
  ) {
    super("AutocompleteComponent");
  }

  public ngOnInit() {
    this.createWidget(connectAutocomplete, {});
    super.ngOnInit();
  }
}

Now let’s add another InstantSearch instance that will query the suggestions index instant_search_demo_query_suggestions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- app.component.html -->
<ais-instantsearch [config]="configSuggestions">
  <app-autocomplete></app-autocomplete>
</ais-instantsearch

<ais-instantsearch [config]="configResults">
  <ais-configure [searchParameters]="searchParameters"></ais-configure>
  <ais-hits>
    <ng-template let-hits="hits" let-results="results">
      <div *ngFor="let hit of hits">
        <ais-highlight attribute="name" [hit]="hit"></ais-highlight>
      </div>
    </ng-template>
  </ais-hits>
</ais-instantsearch
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
// app.component.ts
import { Component } from '@angular/core';
import algoliasearch from 'algoliasearch';

const searchClient = algoliasearch(
  'latency',
  '6be0576ff61c053d5f9a3225e2a90f76'
);

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  public configSuggestions = {
    indexName: 'instant_search_demo_query_suggestions',
    searchClient,
  };
  public configResults = {
    indexName: 'instant_search',
    searchClient,
  };

  public searchParameters = { query: '' };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { NgAisModule } from 'angular-instantsearch';

import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

import { AutocompleteComponent } from './autocomplete.component'

@NgModule({
  declarations: [
    AppComponent,
    AutocompleteComponent
  ],
  imports: [
    NgAisModule.forRoot(),
    BrowserModule,
    BrowserAnimationsModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Our AutocompleteComponent is now able to fetch the suggestions from instant_search_demo_query_suggestions. The next step is to display theses suggestions in a nice way thanks to Angular Meterial Autocomplete.

Step 3: import and populate Angular Material Autocomplete with suggestions

Let’s import everything we need in our app to use Angular Meterial Autocomplete in our code.

  1. Make sure your have @angular/material installed, or run this in your root directory of your project.

    $
    
     ng add @angular/material
    
  2. Import MatInputModule, MatAutocompleteModule inside your project, as well as our newly created AutocompleteComponent.

    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
    
     import { BrowserModule } from '@angular/platform-browser';
     import { NgModule } from '@angular/core';
     import { NgAisModule } from 'angular-instantsearch';
     import { AppComponent } from './app.component';
     import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
    
     import { AutocompleteComponent } from './autocomplete.component'
     import { MatInputModule, MatAutocompleteModule } from '@angular/material';
    
     @NgModule({
       declarations: [
         AppComponent,
         AutocompleteComponent
       ],
       imports: [
         NgAisModule.forRoot(),
         BrowserModule,
         BrowserAnimationsModule,
         MatInputModule,
         MatAutocompleteModule,
       ],
       providers: [],
       bootstrap: [AppComponent]
     })
     export class AppModule { }
    
  3. Use MatInputModule, MatAutocompleteModule inside AutocompleteComponent.

    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
    
     // autocomplete.component.js
     import {
       Component,
       Inject,
       forwardRef,
       EventEmitter,
       Output
     } from "@angular/core";
     import { BaseWidget, NgAisInstantSearch } from "angular-instantsearch";
     import { connectAutocomplete } from "instantsearch.js/es/connectors";
    
     @Component({
       selector: "app-autocomplete",
       template: `
         <div>
           <input
             matInput
             [matAutocomplete]="auto"
             (keyup)="handleChange($event)"
             style="width: 100%; padding: 10px"
           />
           <mat-autocomplete
             #auto="matAutocomplete"
             style="margin-top: 30px; max-height: 600px"
           >
             <div *ngFor="let index of state.indices || []">
               <mat-option
                 *ngFor="let hit of index.hits"
                 [value]="hit.query"
                 (click)="this.onQuerySuggestionClick.emit(hit.query)"
               >
                 {{ hit.query }}
               </mat-option>
             </div>
           </mat-autocomplete>
         </div>
       `
     })
     export class AutocompleteComponent extends BaseWidget {
       state: {
         query: string;
         refine: Function;
         indices: object[];
       };
    
       @Output() onQuerySuggestionClick: EventEmitter<any> = new EventEmitter();
    
       constructor(
         @Inject(forwardRef(() => NgAisInstantSearch))
         public instantSearchParent
       ) {
         super("AutocompleteComponent");
       }
    
       public handleChange($event: KeyboardEvent) {
         this.state.refine(($event.target as HTMLInputElement).value);
       }
    
       public ngOnInit() {
         this.createWidget(connectAutocomplete, {});
         super.ngOnInit();
       }
     }
    
  4. refresh results based on selected query

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
     <!-- app.component.html -->
     <ais-instantsearch [config]="configSuggestions">
       <app-autocomplete (onQuerySuggestionClick)="setQuery($event)"></app-autocomplete>
     </ais-instantsearch
    
     <ais-instantsearch [config]="configResults">
       <ais-configure [searchParameters]="searchParameters"></ais-configure>
       <ais-hits>
         <ng-template let-hits="hits" let-results="results">
           <div *ngFor="let hit of hits">
             <ais-highlight attribute="name" [hit]="hit"></ais-highlight>
           </div>
         </ng-template>
       </ais-hits>
     </ais-instantsearch
    
    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
    
     import { Component } from '@angular/core';
     import algoliasearch from 'algoliasearch';
    
     const searchClient = algoliasearch(
       'latency',
       '6be0576ff61c053d5f9a3225e2a90f76'
     );
    
     @Component({
       selector: 'app-root',
       templateUrl: './app.component.html',
       styleUrls: ['./app.component.css']
     })
     export class AppComponent {
       public configSuggestions = {
         indexName: 'instant_search_demo_query_suggestions',
         searchClient,
       };
       public configResults = {
         indexName: 'instant_search',
         searchClient,
       };
    
       public searchParameters = { query: '' };
    
       public setQuery(query : string) {
         this.searchParameters.query = query
       }
     }
    

That’s it!

A common pattern with an autocomplete of suggestions is to display the relevant categories along with the suggestions. Then when a user select a suggestion both the suggestion and the associated category are used to refine the search. For this example the relevant categories are stored on the suggestions records. We have to update our render function to display the categories with the suggestions. For simplicity and brevity of the code we assume that all suggestions have categories, but this is not the case in the actual dataset. Take a look at the complete example to see the actual implementation.

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
// autocomplete.component.ts
import {
  Component,
  Inject,
  forwardRef,
  EventEmitter,
  Output
} from "@angular/core";
import { BaseWidget, NgAisInstantSearch } from "angular-instantsearch";
import { connectAutocomplete } from "instantsearch.js/es/connectors";

@Component({
  selector: "app-autocomplete",
  template: `
    <div>
      <input
        matInput
        [matAutocomplete]="auto"
        (keyup)="handleChange($event)"
        style="width: 100%; padding: 10px"
      />
      <mat-autocomplete
        #auto="matAutocomplete"
        style="margin-top: 30px; max-height: 600px"
      >
        <div *ngFor="let index of state.indices || []">
          <mat-option
            *ngFor="let hit of index.hits"
            [value]="hit.query"
            (click)="this.onQuerySuggestionClick.emit({query: hit.query, category: hasCategory(hit) ? getCategory(hit) : null })"
          >
            {{ hit.query }}
            <span>
              in
              <em *ngIf="hasCategory(hit)"> {{ getCategory(hit) }} </em>
              <em *ngIf="!hasCategory(hit)"> All categories </em>
            </span>
          </mat-option>
        </div>
      </mat-autocomplete>
    </div>
  `
})
export class AutocompleteComponent extends BaseWidget {
  state: {
    query: string;
    refine: Function;
    indices: object[];
  };

  @Output() onQuerySuggestionClick: EventEmitter<any> = new EventEmitter();

  constructor(
    @Inject(forwardRef(() => NgAisInstantSearch))
    public instantSearchParent
  ) {
    super("AutocompleteComponent");
  }

  hasCategory(hit) {
    return (
      hit.instant_search &&
      hit.instant_search.facets &&
      hit.instant_search.facets.exact_matches &&
      hit.instant_search.facets.exact_matches.categories &&
      hit.instant_search.facets.exact_matches.categories.length
    );
  }

  getCategory(hit) {
    const [category] = hit.instant_search.facets.exact_matches.categories;
    return category.value;
  }

  public handleChange($event: KeyboardEvent) {
    this.state.refine(($event.target as HTMLInputElement).value);
  }

  public ngOnInit() {
    this.createWidget(connectAutocomplete, {});
    super.ngOnInit();
  }
}

Now that we are able to display the categories we can use them to refine the main search. We’re going to use the same strategy as the query. We’ll use the ais-configure widget with disjunctiveFacets and disjunctiveFacetsRefinement. These two parameters are the same ones as internally used in a refinement list.

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
// app.component.ts
import { Component } from '@angular/core';
import algoliasearch from 'algoliasearch';

const searchClient = algoliasearch(
  "latency",
  "6be0576ff61c053d5f9a3225e2a90f76"
);

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class AppComponent {
  public configSuggestions = {
    indexName: "instant_search_demo_query_suggestions",
    searchClient
  };
  public configResults = {
    indexName: "instant_search",
    searchClient
  };

  public searchParameters : {
    query: string;
    disjunctiveFacets?: string[];
    disjunctiveFacetsRefinements?: object;
  } = { query: ""};

  setQuery({ query, category }: { query: string; category: string }) {
    this.searchParameters.query = query;
    if (category) {
      this.searchParameters.disjunctiveFacets = ["categories"];
      this.searchParameters.disjunctiveFacetsRefinements = {
        categories: [category]
      };
    }
  }
}

That’s it! Now when a suggestion is selected both the query and the category are applied to the main search.

Did you find this page helpful?