Skip to content

Commit 20f36a3

Browse files
bidoubiwagmourier
andauthored
Add sort by widget compatibility (#514)
* Make sortby widget compatible * Update README with sortBy usage * Correct words in readme * Fix sort condition * Update README.md Co-authored-by: Guillaume Mourier <[email protected]> * Update README.md * Update README.md * End to end sortby tests (#515) * add sortBy example in playground * Add tests * Add persistent demo URL * Remove temporary meilisearch pkg link * Add recommendation count in playgrounds Co-authored-by: Guillaume Mourier <[email protected]>
1 parent 02eb7cc commit 20f36a3

File tree

13 files changed

+241
-41
lines changed

13 files changed

+241
-41
lines changed

README.md

Lines changed: 102 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,47 @@ This package only guarantees the compatibility with the [version v0.22.0 of Meil
189189

190190
List of all the components that are available in [instantSearch](https://github.com/algolia/instantsearch.js) and their compatibilty with [MeiliSearch](https://github.com/meilisearch/meilisearch/).
191191

192+
### Table Of Widgets
193+
194+
-[InstantSearch](#-instantsearch)
195+
-[index](#-index)
196+
-[SearchBox](#-searchbox)
197+
-[Configure](#-configure)
198+
-[ConfigureRelatedItems](#-configure-related-items)
199+
-[Autocomplete](#-autocomplete)
200+
-[Voice Search](#-voice-search)
201+
-[Insight](#-insight)
202+
-[Middleware](#-middleware)
203+
-[RenderState](#-renderstate)
204+
-[Hits](#-hits)
205+
-[InfiniteHits](#-infinitehits)
206+
-[Highlight](#-highlight)
207+
-[Snippet](#-snippet)
208+
-[Geo Search](#-geo-search)
209+
-[Answers](#-answers)
210+
-[RefinementList](#-refinementlist)
211+
-[HierarchicalMenu](#-hierarchicalmenu)
212+
-[RangeSlider](#-rangeslider)
213+
-[Menu](#-menu)
214+
-[currentRefinements](#-currentrefinements)
215+
-[RangeInput](#-rangeinput)
216+
-[MenuSelect](#-menuselect)
217+
-[ToggleRefinement](#-togglerefinement)
218+
-[NumericMenu](#-numericmenu)
219+
-[RatingMenu](#-ratingmenu)
220+
-[ClearRefinements](#-clearrefinements)
221+
-[Pagination](#-pagination)
222+
-[HitsPerPage](#-hitsperpage)
223+
-[Breadcrumb](#-breadcrumb)
224+
-[Stats](#-stats)
225+
-[Analytics](#-analytics)
226+
-[QueryRuleCustomData](#-queryrulecustomdata)
227+
-[QueryRuleContext](#-queryrulecontext)
228+
-[SortBy](#-sortby)
229+
-[RelevantSort](#-relevantsort)
230+
-[Routing](#-routing)
231+
232+
192233
### ✅ InstantSearch
193234

194235
[instantSearch references](https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/)
@@ -521,7 +562,7 @@ Min and max of attributes are not returned from MeiliSearch and thus **must be s
521562

522563
If the attribute is not in the [`filterableAttributes`](https://docs.meilisearch.com/reference/features/filtering_and_faceted_search.html#configuring-filters) setting list, filtering on this attribute is not possible.
523564

524-
Example:
565+
Example:
525566
Given the attribute `id` that has not been added in `filterableAttributes`:
526567

527568
```js
@@ -738,15 +779,71 @@ The queryRuleContext widget lets you apply ruleContexts based on filters to trig
738779

739780
No compatibility because MeiliSearch does not support Rules.
740781

741-
### SortBy
782+
### SortBy
742783

743784
[Sort by references](https://www.algolia.com/doc/api-reference/widgets/sort-by/js/)
744785

745-
The sortBy widget displays a list of indices, allowing a user to change the way hits are sorted (with replica indices). Another common use case is to let the user switch between different indices.
786+
The `sortBy` widget is used to create multiple sort formulas. Allowing a user to change the way hits are sorted.
746787

747-
No compatibility because MeiliSearch does not support hierarchical facets.
788+
- ✅ container: The CSS Selector or HTMLElement to insert the widget into. _required_
789+
- ✅ items: The list of different sorting possibilities. _required_
790+
- ✅ cssClasses: The CSS classes to override.
791+
- ✅ transformItems: function receiving the items, called before displaying them.
792+
793+
The usage of the `SortBy` widget differs from the one found in Algolia's documentation. In instant-meilisearch the following is possible:
794+
795+
- Sort using different indexes.
796+
- Different `sort` rules on the same index.
797+
798+
The items list is composed of objects containing every sort possibilities you want to provide to your user. Each objects must contain two fields:
799+
- `label`: What is showcased on the user interface ex: `Sort by Ascending Price`
800+
- `value`: The sort formula.
801+
802+
#### Sort formula
803+
804+
A sort formula is expressed like this: `index:attribute:order`.
805+
806+
`index` is mandatory, and when adding `attribute:order`, they must always be added together.
748807

749-
If you'd like to get the "SortBy" feature, please vote for it in the [roadmap]https://roadmap.meilisearch.com/c/32-sort-by?utm_medium=social&utm_source=portal_share).
808+
When sorting on an attribute, the attribute has to be added in the [`SortableAttributes`](#link-to-doc) setting on your index.
809+
810+
Example:
811+
```js
812+
[
813+
{ label: 'Sort By Price', value: 'clothes:price:asc' }
814+
]
815+
```
816+
817+
In this scenario, in the `clothes` index, we want the price to be sorted in an ascending way. For this formula to be valid, `price` must be added in the `sortableAttributes` settings of the `clothes` index.
818+
819+
#### Relevancy
820+
821+
The impact sorting has on the returned hits is determined by the [`ranking-rules`](https://docs.meilisearch.com/learn/core_concepts/relevancy.html#ranking-rules) ordered list of each index. The `sort` ranking-rule position in the list makes sorting documents more or less important than other rules. If you want to change the sort impact on the relevancy, it is possible to change it in the [ranking-rule setting](https://docs.meilisearch.com/reference/api/ranking_rules.html#update-ranking-rules). For example, to favor exhaustivity over relevancy.
822+
823+
See [relevancy guide](https://docs.meilisearch.com/learn/core_concepts/relevancy.html#relevancy).
824+
825+
#### Example
826+
827+
```js
828+
instantsearch.widgets.sortBy({
829+
container: '#sort-by',
830+
items: [
831+
{ value: 'clothes', label: 'Relevant' }, // default index
832+
{
833+
value: 'clothes:price:asc', // Sort on descending price
834+
label: 'Ascending price using query time sort',
835+
},
836+
{
837+
value: 'clothes:price:asc', // Sort on ascending price
838+
label: 'Descending price using query time sort',
839+
},
840+
{
841+
value: 'clothes-sorted', // different index with different ranking rules.
842+
label: 'Custom sort using a different index',
843+
},
844+
],
845+
}),
846+
```
750847

751848
### ❌ RelevantSort
752849

cypress/integration/search-ui.spec.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,20 @@ describe(`${playground} playground test`, () => {
4242
cy.get(HIT_ITEM_CLASS).eq(0).contains('9.99 $')
4343
})
4444

45+
it('Sort by recommendationCound ascending', () => {
46+
const select = `.ais-SortBy-select`
47+
cy.get(select).select('steam-video-games:recommendationCount:asc')
48+
cy.wait(1000)
49+
cy.get(HIT_ITEM_CLASS).eq(0).contains('Rag Doll Kung Fu')
50+
})
51+
52+
it('Sort by default relevancy', () => {
53+
const select = `.ais-SortBy-select`
54+
cy.get(select).select('steam-video-games')
55+
cy.wait(1000)
56+
cy.get(HIT_ITEM_CLASS).eq(0).contains('Counter-Strike')
57+
})
58+
4559
it('click on facets', () => {
4660
const checkbox = `.ais-RefinementList-list .ais-RefinementList-checkbox`
4761
cy.get(checkbox).eq(1).click()

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"test:e2e:all": "sh scripts/e2e.sh",
1313
"test:e2e:watch": "concurrently --kill-others -s first \"NODE_ENV=test yarn playground:javascript\" \"cypress open --env playground=javascript\"",
1414
"test:all": "yarn test:e2e:all && yarn test && test:build",
15+
"cy:open": "cypress open",
1516
"playground:vue": "yarn --cwd ./playgrounds/vue && yarn --cwd ./playgrounds/vue serve",
1617
"playground:react": "yarn --cwd ./playgrounds/react && yarn --cwd ./playgrounds/react start",
1718
"playground:javascript": "yarn --cwd ./playgrounds/javascript && yarn --cwd ./playgrounds/javascript start",

playgrounds/angular/src/app/app.component.html

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,19 @@ <h1 class="header-title">MeiliSearch + Angular InstantSearch</h1>
1010
<div class="search-panel">
1111
<div class="search-panel__filters">
1212
<ais-clear-refinements></ais-clear-refinements>
13+
<ais-sort-by
14+
[items]="[
15+
{ value: 'steam-video-games', label: 'Relevant' },
16+
{
17+
value: 'steam-video-games:recommendationCount:desc',
18+
label: 'Most Recommended'
19+
},
20+
{
21+
value: 'steam-video-games:recommendationCount:asc',
22+
label: 'Least Recommended'
23+
}
24+
]"
25+
></ais-sort-by>
1326
<ais-configure [searchParameters]="{ hitsPerPage: 6 }"></ais-configure>
1427
<h2>Genres</h2>
1528
<ais-refinement-list attribute="genres" ></ais-refinement-list>
@@ -40,8 +53,9 @@ <h2>Misc</h2>
4053
<div class="hit-description">
4154
<ais-highlight attribute="description" [hit]="hit"></ais-highlight>
4255
</div>
43-
<div class="hit-info">${{hit.price}}</div>
44-
<div class="hit-info">{{hit.releaseDate}}</div>
56+
<div class="hit-info">price: ${{hit.price}}</div>
57+
<div class="hit-info">Release date: {{hit.releaseDate}}</div>
58+
<div class="hit-info">Recommendation: {{hit.recommendationCount}}</div>
4559
</li>
4660
</ol>
4761
</ng-template>

playgrounds/angular/src/app/app.component.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { Component } from '@angular/core'
22
import { instantMeiliSearch } from '../../../../src'
33

44
const searchClient = instantMeiliSearch(
5-
'https://ms-9060336c1f95-106.saas.meili.dev',
6-
'5d7e1929728417466fd5a82da5a28beb540d3e5bbaf4e01f742e1fb5fd72bb66'
5+
'https://demo-steam.meilisearch.com/',
6+
'90b03f9c47d0f321afae5ae4c4e4f184f53372a2953ab77bca679ff447ecc15c'
77
)
88

99
@Component({

playgrounds/html/public/index.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@
2525
const search = instantsearch({
2626
indexName: "steam-video-games",
2727
searchClient: instantMeiliSearch(
28-
'https://ms-9060336c1f95-106.saas.meili.dev',
29-
'5d7e1929728417466fd5a82da5a28beb540d3e5bbaf4e01f742e1fb5fd72bb66',
28+
'https://demo-steam.meilisearch.com',
29+
'90b03f9c47d0f321afae5ae4c4e4f184f53372a2953ab77bca679ff447ecc15c',
3030
)
31-
});
31+
});
3232
search.addWidgets([
3333
instantsearch.widgets.searchBox({
3434
container: "#searchbox",

playgrounds/javascript/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ <h2>Search in Steam video games 🎮</h2>
2929

3030
<div class="left-panel">
3131
<div id="clear-refinements"></div>
32-
32+
<div id="sort-by"></div>
3333
<h2>Genres</h2>
3434
<div id="genres-list"></div>
3535
<h2>Players</h2>

playgrounds/javascript/src/app.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,29 @@ import { instantMeiliSearch } from '../../../src/index'
33
const search = instantsearch({
44
indexName: 'steam-video-games',
55
searchClient: instantMeiliSearch(
6-
'https://ms-9060336c1f95-106.saas.meili.dev',
7-
'5d7e1929728417466fd5a82da5a28beb540d3e5bbaf4e01f742e1fb5fd72bb66',
6+
'https://demo-steam.meilisearch.com',
7+
'90b03f9c47d0f321afae5ae4c4e4f184f53372a2953ab77bca679ff447ecc15c',
88
{
99
limitPerRequest: 30,
1010
}
1111
),
1212
})
1313

1414
search.addWidgets([
15+
instantsearch.widgets.sortBy({
16+
container: '#sort-by',
17+
items: [
18+
{ value: 'steam-video-games', label: 'Relevant' },
19+
{
20+
value: 'steam-video-games:recommendationCount:desc',
21+
label: 'Most Recommended',
22+
},
23+
{
24+
value: 'steam-video-games:recommendationCount:asc',
25+
label: 'Least Recommended',
26+
},
27+
],
28+
}),
1529
instantsearch.widgets.searchBox({
1630
container: '#searchbox',
1731
}),
@@ -32,6 +46,7 @@ search.addWidgets([
3246
}),
3347
instantsearch.widgets.configure({
3448
hitsPerPage: 6,
49+
attributesToSnippet: ['description:150'],
3550
}),
3651
instantsearch.widgets.refinementList({
3752
container: '#misc-list',
@@ -51,6 +66,7 @@ search.addWidgets([
5166
</div>
5267
<div class="hit-info">price: {{price}}</div>
5368
<div class="hit-info">release date: {{releaseDate}}</div>
69+
<div class="hit-info">Recommendation: {{recommendationCount}}</div>
5470
</div>
5571
`,
5672
},

playgrounds/react/src/App.js

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@ import {
99
ClearRefinements,
1010
RefinementList,
1111
Configure,
12+
SortBy,
13+
Snippet,
1214
} from 'react-instantsearch-dom'
15+
1316
import './App.css'
1417
import { instantMeiliSearch } from '../../../src/index'
1518

1619
const searchClient = instantMeiliSearch(
17-
'https://ms-9060336c1f95-106.saas.meili.dev',
18-
'5d7e1929728417466fd5a82da5a28beb540d3e5bbaf4e01f742e1fb5fd72bb66',
20+
'https://demo-steam.meilisearch.com/',
21+
'90b03f9c47d0f321afae5ae4c4e4f184f53372a2953ab77bca679ff447ecc15c',
1922
{
2023
paginationTotalHits: 60,
2124
primaryKey: 'id',
@@ -39,6 +42,20 @@ const App = () => (
3942
<Stats />
4043
<div className="left-panel">
4144
<ClearRefinements />
45+
<SortBy
46+
defaultRefinement="steam-video-games"
47+
items={[
48+
{ value: 'steam-video-games', label: 'Relevant' },
49+
{
50+
value: 'steam-video-games:recommendationCount:desc',
51+
label: 'Most Recommended',
52+
},
53+
{
54+
value: 'steam-video-games:recommendationCount:asc',
55+
label: 'Least Recommended',
56+
},
57+
]}
58+
/>
4259
<h2>Genres</h2>
4360
<RefinementList attribute="genres" />
4461
<h2>Players</h2>
@@ -47,7 +64,11 @@ const App = () => (
4764
<RefinementList attribute="platforms" />
4865
<h2>Misc</h2>
4966
<RefinementList attribute="misc" />
50-
<Configure hitsPerPage={6} />
67+
<Configure
68+
hitsPerPage={6}
69+
attributesToSnippet={['description:50']}
70+
snippetEllipsisText={'...'}
71+
/>
5172
</div>
5273
<div className="right-panel">
5374
<SearchBox />
@@ -57,18 +78,27 @@ const App = () => (
5778
</div>
5879
)
5980

60-
const Hit = ({ hit }) => (
61-
<div key={hit.id}>
62-
<div className="hit-name">
63-
<Highlight attribute="name" hit={hit} />
64-
</div>
65-
<img src={hit.image} align="left" alt={hit.name} />
66-
<div className="hit-name">
67-
<Highlight attribute="description" hit={hit} />
81+
const Hit = ({ hit }) => {
82+
return (
83+
<div key={hit.id}>
84+
<div className="hit-name">
85+
<Highlight attribute="name" hit={hit} />
86+
</div>
87+
<img src={hit.image} align="left" alt={hit.name} />
88+
<div className="hit-name">
89+
<Snippet attribute="description" hit={hit} />
90+
</div>
91+
<div className="hit-info">
92+
<b>price:</b> {hit.price}
93+
</div>
94+
<div className="hit-info">
95+
<b>release date:</b> {hit.releaseDate}
96+
</div>
97+
<div className="hit-info">
98+
<b>Recommended:</b> {hit.recommendationCount}
99+
</div>
68100
</div>
69-
<div className="hit-info">price: {hit.price}</div>
70-
<div className="hit-info">release date: {hit.releaseDate}</div>
71-
</div>
72-
)
101+
)
102+
}
73103

74104
export default App

0 commit comments

Comments
 (0)