Skip to content
This repository was archived by the owner on May 3, 2022. It is now read-only.

Commit 8ac9ab3

Browse files
committed
initial commit
0 parents  commit 8ac9ab3

File tree

7 files changed

+7724
-0
lines changed

7 files changed

+7724
-0
lines changed

README.md

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
# Dynamic Faceting using Query Rules
2+
3+
Prerequisites: Algolia, react-instantsearch, Query Rules, facets
4+
5+
## The goal
6+
7+
We want to add facets only when they are relevant to the search, when there's a manually manageable number of facets to add.
8+
9+
We want to create a interface like [this one](https://preview.algolia.com/dynamic-faceting/) (which is using InstantSearch.js). The code for that interface is [here](https://github.com/algolia/demo-dynamic-faceting).
10+
11+
## The strategy
12+
13+
1. add all the possible facets to your indexing configuration as regular
14+
2. add query rules for the situations in which you want certain facets to show up
15+
3. render the facets decided by the query rule
16+
17+
## Adding facets
18+
19+
This is exactly like normally, but this time you also keep track of which facets are possible. Here I've started from the "ecommerce" dataset provided, and indexed it like in [indexing.js](./indexing/index.js) (run `yarn index` to index to your app).
20+
21+
then we decide which queries (no typo-tolerance) you want these facets to show up. For example:
22+
23+
```js
24+
const dynamicFacets = [
25+
{
26+
query: 'iphone',
27+
facets: [
28+
{ attribute: 'category', widgetName: 'RefinementList' },
29+
{ attribute: 'type', widgetName: 'Menu' },
30+
],
31+
objectID: 'dynamic_query_iphone',
32+
},
33+
{
34+
query: 'smartphone',
35+
facets: [
36+
{ attribute: 'brand', widgetName: 'Menu' },
37+
{ attribute: 'price', widgetName: 'RangeInput' },
38+
],
39+
objectID: 'dynamic_query_smartphone',
40+
},
41+
];
42+
```
43+
44+
## Adding query rules
45+
46+
Next we need to transform that list into query rules and add them to the index.
47+
48+
```js
49+
const algoliasearch = require('algoliasearch');
50+
const client = algoliasearch('your_app_id', 'your_admin_api_key');
51+
const index = client.initIndex('your_index');
52+
53+
index
54+
.batchRules(
55+
dynamicFacets.map(({ objectID, query, facets }) => ({
56+
condition: {
57+
pattern: query,
58+
anchoring: 'contains',
59+
},
60+
consequence: {
61+
userData: {
62+
type: 'dynamic_facets',
63+
facets,
64+
},
65+
},
66+
objectID,
67+
}))
68+
)
69+
.then(console.log);
70+
```
71+
72+
These query rules can also be added via the dashboard. make sure to choose the same options as the ones added here.
73+
74+
## Dynamically displaying facets
75+
76+
In our query rules we made four possible dynamic facets: categories, type, brand, price. We now want to display these in a React InstantSearch app. First let's create a start:
77+
78+
```jsx
79+
import React from 'react';
80+
import ReactDOM from 'react-dom';
81+
import {
82+
InstantSearch,
83+
Hits,
84+
SearchBox,
85+
Pagination,
86+
} from 'react-instantsearch-dom';
87+
88+
const App = () => (
89+
<InstantSearch
90+
appId="your_app_id"
91+
apiKey="your_search_api_key"
92+
indexName="your_index"
93+
>
94+
<SearchBox />
95+
<div
96+
style={{
97+
display: 'grid',
98+
gridTemplateColumns: '20% 75%',
99+
gridGap: '5%',
100+
}}
101+
>
102+
<div>We will add dynamic facets here</div>
103+
<Hits />
104+
</div>
105+
<div style={{ textAlign: 'center' }}>
106+
<Pagination />
107+
</div>
108+
</InstantSearch>
109+
);
110+
111+
ReactDOM.render(<App />, document.getElementById('root'));
112+
```
113+
114+
Now we want to create the dynamic facets. We will make a wrapper component which will read the facets to apply from the query rule state:
115+
116+
```js
117+
import { Component } from 'react';
118+
import PropTypes from 'prop-types';
119+
import {
120+
connectStateResults,
121+
RefinementList,
122+
Menu,
123+
RangeInput,
124+
} from 'react-instantsearch-dom';
125+
126+
const DynamicWidgets = {
127+
RefinementList,
128+
Menu,
129+
RangeInput,
130+
};
131+
132+
class DynamicFacets extends Component {
133+
static propTypes = {
134+
limit: PropTypes.number,
135+
searchState: PropTypes.object,
136+
searchResults: PropTypes.object,
137+
};
138+
139+
static defaultProps = {
140+
limit: 10,
141+
};
142+
143+
render() {
144+
if (this.props.searchResults && this.props.searchResults.userData) {
145+
// get the data returned by the query rule
146+
const uniques = this.props.searchResults.userData
147+
// only get the dynamic facet type
148+
.filter(({ type }) => type === 'dynamic_facets')
149+
// only add one widget per attribute
150+
.reduce((acc, { facets }) => {
151+
facets.forEach(({ attribute, widgetName }) =>
152+
acc.set(attribute, widgetName)
153+
);
154+
return acc;
155+
}, new Map());
156+
157+
const facets = [...uniques]
158+
// limit it
159+
.slice(0, this.props.limit)
160+
// turn the name of the widget into its value
161+
.map(([attribute, widgetName]) => ({
162+
attribute,
163+
widgetName,
164+
Widget: DynamicWidgets[widgetName],
165+
}));
166+
167+
if (facets.length > 0) {
168+
return this.props.children({ facets });
169+
}
170+
}
171+
return null;
172+
}
173+
}
174+
175+
export default connectStateResults(DynamicFacets);
176+
```
177+
178+
This component essentially gives us access to the dynamic facets applying here. We can now make use of these facets and render them where we used to have `<div>We will add dynamic facets here</div>`.
179+
180+
```jsx
181+
<DynamicFacets>
182+
{({ facets }) =>
183+
facets.map(({ attribute, widgetName }) => (
184+
<pre key={attribute}>
185+
{JSON.stringify({ attribute, widgetName }, null, 2)}
186+
</pre>
187+
))
188+
}
189+
</DynamicFacets>
190+
```
191+
192+
So what we have now is a component which tells us which widgets to render with which attribute. The next step is to actually render them:
193+
194+
```jsx
195+
<DynamicFacets>
196+
{({ facets }) =>
197+
facets.map(({ attribute, Widget }) => (
198+
<Panel header={attribute} key={attribute}>
199+
<Widget attribute={attribute} key={attribute} />
200+
</Panel>
201+
))
202+
}
203+
</DynamicFacets>
204+
```
205+
206+
This gives us a final result of:
207+
208+
[![these widgets ]()]()

indexing/index.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
const algoliasearch = require('algoliasearch');
2+
const fetch = require('node-fetch');
3+
const chunk = require('lodash/chunk');
4+
require('dotenv').config();
5+
6+
const appId = process.env.REACT_APP_ALGOLIA_APP_ID;
7+
const apiKey = process.env.ALGOLIA_ADMIN_API_KEY;
8+
const indexName = process.env.REACT_APP_ALGOLIA_INDEX_NAME;
9+
10+
async function main() {
11+
const client = algoliasearch(appId, apiKey);
12+
const index = client.initIndex(indexName);
13+
14+
const source =
15+
'https://cdn.rawgit.com/algolia/datasets/master/ecommerce/bestbuy_seo.json';
16+
17+
const data = await fetch(source).then(res => res.json());
18+
19+
console.log('data fetched', data.length);
20+
21+
const chunks = chunk(data);
22+
23+
await Promise.all(chunks.map(chunk => index.saveObjects(chunk)));
24+
console.log('data indexed');
25+
26+
await index.setSettings({
27+
searchableAttributes: [
28+
'name',
29+
'shortDescription',
30+
'manufacturer',
31+
'categories',
32+
],
33+
attributesForFaceting: ['categories', 'type', 'manufacturer', 'salePrice'],
34+
customRanking: ['desc(bestSellingRank)'],
35+
});
36+
37+
console.log('basic settings set');
38+
39+
const dynamicFacets = [
40+
{
41+
query: 'iphone',
42+
facets: [
43+
{ attribute: 'categories', widgetName: 'RefinementList' },
44+
{ attribute: 'type', widgetName: 'Menu' },
45+
],
46+
objectID: 'dynamic_query_iphone',
47+
},
48+
{
49+
query: 'smartphone',
50+
facets: [
51+
{ attribute: 'manufacturer', widgetName: 'Menu' },
52+
{ attribute: 'salePrice', widgetName: 'RangeInput' },
53+
],
54+
objectID: 'dynamic_query_smartphone',
55+
},
56+
];
57+
58+
await index.batchRules(
59+
dynamicFacets.map(({ objectID, query, facets }) => ({
60+
condition: {
61+
pattern: query,
62+
anchoring: 'contains',
63+
},
64+
consequence: {
65+
userData: {
66+
type: 'dynamic_facets',
67+
facets,
68+
},
69+
},
70+
objectID,
71+
}))
72+
);
73+
74+
console.log('rules added');
75+
}
76+
77+
main();

package.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "dynamic-guide",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"license": "MIT",
6+
"scripts": {
7+
"index": "node indexing",
8+
"start": "react-scripts start"
9+
},
10+
"dependencies": {
11+
"algoliasearch": "^3.30.0",
12+
"dotenv": "^6.0.0",
13+
"lodash": "^4.17.10",
14+
"node-fetch": "^2.2.0",
15+
"prop-types": "^15.6.2",
16+
"react": "^16.4.2",
17+
"react-dom": "^16.4.2",
18+
"react-instantsearch": "^5.2.2"
19+
},
20+
"devDependencies": {
21+
"react-scripts": "^1.1.4"
22+
}
23+
}

public/index.html

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<meta http-equiv="X-UA-Compatible" content="ie=edge">
7+
<title>Dynamic faceting guide</title>
8+
<style>
9+
body {
10+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
11+
}
12+
.twoCol {
13+
display: grid;
14+
grid-template-columns: 20% 75%;
15+
grid-gap: 5%;
16+
}
17+
.ais-Panel {
18+
margin-bottom: 0.5em;
19+
}
20+
.ais-Pagination {
21+
margin-top: 1em;
22+
}
23+
.ais-SearchBox {
24+
margin-bottom: 1em;
25+
}
26+
</style>
27+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/instantsearch.css@7.1.0/themes/algolia-min.css">
28+
</head>
29+
<body>
30+
<main id="root"></main>
31+
</body>
32+
</html>

0 commit comments

Comments
 (0)