Introduction
When you create specific landing pages to display search results, it’s handy to be able to redirect users to those dedicated pages, in the situation where the search query matches one of them.
In this tutorial, we’ll see two methods for setting up redirects in an Algolia index: using Rules and using dedicated indices.
Using Rules
The best way to set up redirects is through Rules. You can add add redirect information, as custom data, to any Rule: this data is returned when the Rule is triggered by a search.
Configuring the Rule
Using the API
To add a Rule, you need to use the saveRule
method. When setting a Rule, you need to define a condition and a consequence.
In the example below, we want to redirect whenever the query matches “star wars” exactly. If the query is “star wars lightsaber” or “books star wars”, we don’t want to redirect.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| $rule = array(
'objectID' => 'a-rule-id',
'conditions' => array(array(
'pattern' => 'star wars',
'anchoring' => 'is'
)),
'consequence' => array(
'userData' => array(
'redirect' => 'https://www.google.com/#q=star+wars'
)
)
);
$index->saveRule($rule);
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| rule = {
objectID: 'a-rule-id',
conditions: [{
pattern: 'star wars',
anchoring: 'is'
}],
consequence: {
userData: {
redirect: 'https://www.google.com/#q=star+wars'
}
}
}
index.save_rule('a-rule-id', rule)
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| const rule = {
objectID: 'a-rule-id',
conditions: [{
pattern: 'star wars',
anchoring: 'is'
}],
consequence: {
userData: {
redirect: 'https://www.google.com/#q=star+wars'
}
}
};
index.saveRule(rule).then(() => {
// done
});
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| rule = {
'objectID': 'a-rule-id',
'conditions': [{
'pattern': 'star wars',
'anchoring': 'is'
}],
'consequence': {
'userData': {
'redirect': 'https://www.google.com/#q=star+wars'
}
}
}
response = index.save_rule(rule)
|
1
2
3
4
5
6
7
8
9
10
11
12
13
| let rule = Rule(objectID: "a-rule-id")
.set(\.conditions, to: [
.init(anchoring: .is, pattern: .literal("star wars"))
])
.set(\.consequence, to: Rule.Consequence()
.set(\.userData, to: ["redirect": "https://www.google.com/#q=star+wars"])
)
index.saveRule(rule) { result in
if case .success(let response) = result {
print("Response: \(response)")
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| JObject data = new JObject();
data.Add("redirect", "https://www.google.com/#q=star+wars");
var rule = new Rule
{
ObjectID = "a-rule-id",
Conditions = new List<Condition>
{
new Condition { Anchoring = "is", Pattern = "star wars" }
},
Consequence = new Consequence
{
UserData = data
}
};
index.SaveRule(rule);
|
1
2
3
4
5
6
7
8
9
10
11
12
| Condition condition = new Condition()
.setPattern("star wars")
.setAnchoring("is");
Consequence consequence = new Consequence()
.setUserData(ImmutableMap.of("redirect", "https://www.google.com/#q=star+wars"));
Rule rule = new Rule()
.setObjectID("a-rule-id")
.setConditions(Collections.singletonList(condition))
.setConsequence(consequence);
index.saveRule(rule.getObjectID(), rule);
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| rule := search.Rule{
ObjectID: "a-rule-id",
Condition: []search.RuleCondition{
{
Anchoring: search.Is, Pattern: "star wars"
},
},
Consequence: search.RuleConsequence{
UserData: map[string]string{
"redirect": "https://www.google.com/#q=star+wars",
},
},
}
res, err := index.SaveRule(rule)
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| val ruleToSave = Rule(
objectID = "a-rule-id",
conditions = Some(Seq(Condition(
pattern = "star wars",
anchoring = "is"
))),
consequence = Consequence(
userData = Some(Map("redirect" -> "https://www.google.com/#q=star+wars"))
),
)
client.execute {
save rule ruleToSave inIndex "index_name"
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| @Serializable
data class Custom(val redirect: String)
val custom = Custom("https://www.google.com/#q=star+wars")
val myUserData = Json.encodeToJsonElement(Custom.serializer(), custom).jsonObject
// or
val myUserData = JsonObject(mapOf("redirect" to JsonPrimitive("https://www.google.com/#q=star+wars")))
val rule = Rule(
objectID = ObjectID("a-rule-id"),
conditions = listOf(
Condition(anchoring = Anchoring.Is, pattern = Pattern.Literal("star wars"))
),
consequence = Consequence(userData = myUserData)
)
index.saveRule(rule)
|
Using the dashboard
You can also add your Rules in your Algolia dashboard.
- Go to your dashboard and select your index.
- Click the Rules tab.
- Select Create your first rule or New rule. In the dropdown, click on the Manual Editor option.
- In the Condition(s) section, keep Query toggled on, select Is in the dropdown and enter “star wars” in the input field.
- In the Consequence(s) section:
- Click the Add consequence button and select Return Custom Data.
- In the input field that appears, add the data to return when the user query matches the Rule:
{ "redirect": "https://www.google.com/#q=star+wars" }
- Don’t forget to save your changes.
Now that we have everything set up, we can use the queryRuleCustomData
widget to update the page location anytime we find userData
containing a redirect.
1
2
3
4
5
6
7
8
9
| @Serializable
data class Redirect(val url: String)
val searcher = SearcherSingleIndex(index)
QueryRuleCustomDataConnector<Redirect>(searcher) { redirect ->
redirect?.url?.let {
// perform redirect with URL
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| search.addWidgets([
instantsearch.widgets.queryRuleCustomData({
container,
templates: {
default: '',
},
transformItems(items) {
const match = items.find(data => Boolean(data.redirect));
if (match && match.redirect) {
window.location.href = match.redirect;
}
return [];
},
})
]);
|
1
2
3
4
5
6
7
8
9
10
11
12
13
| import { QueryRuleCustomData } from 'react-instantsearch-dom';
<QueryRuleCustomData
transformItems={items => {
const match = items.find(data => Boolean(data.redirect));
if (match && match.redirect) {
window.location.href = match.redirect;
}
return [];
}}
>
{() => null}
</QueryRuleCustomData>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| <template>
<ais-query-rule-custom-data :transform-items="transformItems" />
</template>
<script>
export default {
methods: {
transformItems(items) {
const match = items.find(data => Boolean(data.redirect));
if (match && match.redirect) {
window.location.href = match.redirect;
}
return [];
},
},
};
</script>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| <ais-query-rule-custom-data [transformItems]="transformItems">
</ais-query-rule-custom-data>
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
})
export class AppComponent {
// ...
transformItems(items) {
const match = items.find(data => Boolean(data.redirect));
if (match && match.redirect) {
window.location.href = match.redirect;
}
return [];
},
}
|
1
2
3
4
5
6
7
8
9
10
11
| let searcher: SingleIndexSearcher = .init(appID: "YourApplicationID",
apiKey: "YourSearchOnlyAPIKey",
indexName: "YourIndexName")
let queryRuleCustomDataConnector = QueryRuleCustomDataConnector<Redirect>(searcher: searcher)
queryRuleCustomDataConnector.interactor.onItemChanged.subscribe(with: self) { (_, redirect) in
if let redirectURL = redirect?.url {
/// perform redirect with URL
}
}
|
Using a dedicated index
An alternative way to set up redirects is to create a dedicated index with the list of redirects and the query terms that trigger them. This index is separate from your regular index, where your searchable records are. Whenever a user performs a search, you have to look in two indices: the regular one to search for items, and the redirect one that determines whether or not the user should be redirected.
This technique can be useful if you cannot use Rules or if you want access to typo tolerance, synonyms or filters.
Using a dedicated index will force you to trigger two queries: one for the results and one for the redirects. It can have an impact on your search operations usage.
Dataset
1
2
3
4
5
6
7
8
9
10
| [
{
"url": "https://www.google.com/#q=star+wars",
"query_terms": ["star wars"]
},
{
"url": "https://www.google.com/#q=star+wars+actors",
"query_terms": ["george lucas", "luke skywalker"]
}
]
|
In this dataset, we give a list of URLs we want to redirect to. For each of them, we define a list of queries that we want to redirect from.
For example, the query “star wars” should redirect to https://www.google.com/#q=star+wars
.
You can download the dataset on our GitHub.
Have look at how to import it in Algolia here
Configuring the index
We want to redirect only if the query exactly matches one of the query_terms
. We’re adding query_terms
to our list of attributesForFaceting
, so we can filter on it.
1
2
3
4
5
| $index->setSettings([
'attributesForFaceting' => [
'query_terms'
]
]);
|
1
2
3
4
5
| index.set_settings(
attributesForFaceting: [
'query_terms'
]
)
|
1
2
3
4
5
6
7
| index.setSettings({
attributesForFaceting: [
'query_terms'
]
}).then(() => {
// done
});
|
1
2
3
4
5
| index.set_settings({
'attributesForFaceting': [
'query_terms'
]
})
|
1
2
3
4
5
| index.setSettings(Settings().set(\.attributesForFaceting, to: ["query_terms"])) { result in
if case .success(let response) = result {
print("Response: \(response)")
}
}
|
1
2
3
4
5
6
7
| final JSONObject res = index.setSettings(
new JSONObject().put(
"attributesForFaceting",
new JSONArray()
.put("query_terms")
)
);
|
1
2
3
4
| IndexSettings settings = new IndexSettings
{
AttributesForFaceting = new List<string> {"query_terms"}
};
|
1
2
| index.setSettings(new IndexSettings()
.setAttributesForFaceting(Collections.singletonList("query_terms")));
|
1
2
3
| res, err := index.SetSettings(search.Settings{
AttributesForFaceting: opt.AttributesForFaceting("query_terms"),
})
|
1
2
3
4
5
6
7
| client.execute {
changeSettings of "myIndex" `with` IndexSettings(
attributesForFaceting = Some(Seq(
"query_terms"
))
)
}
|
1
2
3
4
5
6
| val settings = settings {
attributesForFaceting {
+"query_terms"
}
}
index.setSettings(settings)
|
Creating a custom search client
Now that we have everything set up, we’ll create a custom search client that will send requests to our regular index and to our redirect index in parallel.
The request to the redirect index is sent with:
- an empty query, so we don’t filter with it
- a filter on
query_terms
with the content of the current query, to match only on records that contains the current query exactly
hitsPerPage=1
, as we only need the first record
Whenever we find hits in this index, we redirect:
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
| const client = algoliasearch('YourApplicationID', 'YourSearchOnlyAPIKey');
const redirectIndex = client.initIndex('your_redirect_index_name');
const customSearchClient = {
async search(requests) {
const searches = [client.search(requests)];
if (requests[0].params.query) {
searches.push(
redirectIndex.search('', {
hitsPerPage: 1,
facetFilters: [`query_terms:${requests[0].params.query}`],
})
);
}
const [results, redirect] = await Promise.all(searches);
if (redirect && redirect.hits.length) {
window.location.href = redirect.hits[0].url;
}
return results;
},
};
const search = instantsearch({
indexName: 'instant_search',
searchClient: customSearchClient,
});
|
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
| const client = algoliasearch('YourApplicationID', 'YourSearchOnlyAPIKey');
const redirectIndex = client.initIndex('your_redirect_index_name');
const customSearchClient = {
async search(requests) {
const searches = [client.search(requests)];
if (requests[0].params.query) {
searches.push(
redirectIndex.search('', {
hitsPerPage: 1,
facetFilters: [`query_terms:${requests[0].params.query}`],
})
);
}
const [results, redirect] = await Promise.all(searches);
if (redirect && redirect.hits.length) {
window.location.href = redirect.hits[0].url;
}
return results;
},
};
const App = () => (
<InstantSearch
indexName="instant_search"
searchClient={customSearchClient}
>
{/* Widgets */}
</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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| <template>
<ais-instant-search
index-name="instant_search"
:search-client="customSearchClient"
>
<!-- Widgets -->
</ais-instant-search>
</template>
<script>
import algoliasearch from 'algoliasearch/lite';
const client = algoliasearch('YourApplicationID', 'YourSearchOnlyAPIKey');
const redirectIndex = client.initIndex('your_redirect_index_name');
export default {
data() {
return {
customSearchClient: {
async search(requests) {
const searches = [client.search(requests)];
if (requests[0].params.query) {
searches.push(
redirectIndex.search('', {
hitsPerPage: 1,
facetFilters: [`query_terms:${requests[0].params.query}`],
})
);
}
const [results, redirect] = await Promise.all(searches);
if (redirect && redirect.hits.length) {
window.location.href = redirect.hits[0].url;
}
return results;
},
},
};
},
};
</script>
|
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
| import algoliasearch from 'algoliasearch/lite';
const client = algoliasearch('YourApplicationID', 'YourSearchOnlyAPIKey');
const redirectIndex = client.initIndex('your_redirect_index_name');
const customSearchClient = {
async search(requests) {
const searches = [client.search(requests)];
if (requests[0].params.query) {
searches.push(
redirectIndex.search('', {
hitsPerPage: 1,
facetFilters: [`query_terms:${requests[0].params.query}`],
})
);
}
const [results, redirect] = await Promise.all(searches);
if (redirect && redirect.hits.length) {
window.location.href = redirect.hits[0].url;
}
return results;
},
};
@Component({
template: `
<ais-instantsearch [config]="config">
<!-- Widgets -->
</ais-instantsearch>
`,
})
export class AppComponent {
config = {
indexName: 'instant_search',
searchClient: customSearchClient,
}
}
|