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

Build a Query Suggestions UI with Angular InstantSearch

Angular InstantSearch isn’t compatible with Angular’s Ivy view engine. We’re investigating how best to support this. For more information and to vote for Algolia’s support of Angular 16 and beyond, see the GitHub issue Algolia Support for Angular InstantSearch

To help users with their search, Algolia provides Query Suggestions.

This feature creates an index with the best queries done by your users.

You can use this index to provide your users with suggestions as they’re 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 users with suggestions. You can do this by implementing a multi-index search.

This guide shows how to use a search box to display a list of suggestions and their associated categories. Once users select a suggestion, Algolia will apply the query and the category. This guide doesn’t cover the details of how to integrate autocomplete with Angular InstantSearch (however, the Autocomplete guide has a dedicated section on that topic).

You can find the source code for this tutorial on GitHub.

Refine your results with the suggestions

Step one: set up your boilerplate for displaying results

Start with a regular InstantSearch 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 two: fetching suggestions

Use the Angular Material component to connect with the autocomplete connector. You can find more information in the autocomplete guide

Create an AutocompleteComponent component to extend the BaseWidget class with the connectAutocomplete connector as follows:

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 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 { }

Your AutocompleteComponent can now fetch the suggestions from instant_search_demo_query_suggestions.

The next step is to display these suggestions attractively using Angular Material Autocomplete.

Step three: import and populate Angular Material Autocomplete with suggestions

To import everything you need in your app to use Angular Material Autocomplete in your code:

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

    $
    
     ng add @angular/material
    
  2. Import MatInputModule, MatAutocompleteModule inside your project, as well as your 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
       }
     }
    

A typical use of autocomplete is to display both relevant categories and suggestions. Then when a user selects 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. You must update your render function to display the categories with the suggestions. For simplicity and brevity of the code, assume that all suggestions have categories, but this isn’t 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 you can display the categories, use them to refine the main search (using the same strategy as the query). 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
// 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]
      };
    }
  }
}

Now when a suggestion is selected both the query and the category are applied to the main search.

Did you find this page helpful?