Building Query Suggestions UI
On this page
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.
-
Make sure your have
@angular/material
installed, or run this in your root directory of your project.Copy$
ng add @angular/material
-
Import
MatInputModule
,MatAutocompleteModule
inside your project, as well as our 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 } }
That’s it!
Use the related categories in the autocomplete
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.