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
On this page
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 the user 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 the user selects a suggestion, the engine 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:
-
Make sure you have
@angular/material
installed, or run this in the root directory of your project.Copy$
ng add @angular/material
-
Import
MatInputModule
,MatAutocompleteModule
inside your project, as well as your newly createdAutocompleteComponent
.Copy1 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 { }
-
Use
MatInputModule
,MatAutocompleteModule
insideAutocompleteComponent
.Copy1 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(); } }
-
Refresh results based on selected query
Copy1 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>
Copy1 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 } }
Use the related categories in the autocomplete
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.