diff --git a/README.md b/README.md index f78f2192..5a0f537a 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ This package only guarantees the compatibility with the [version v4 of InstantSe **Supported MeiliSearch versions**: -This package only guarantees the compatibility with the [version v0.21.0 of MeiliSearch](https://github.com/meilisearch/MeiliSearch/releases/tag/v0.21.0). +This package only guarantees the compatibility with the [version v0.22.0 of MeiliSearch](https://github.com/meilisearch/MeiliSearch/releases/tag/v0.22.0). **Node / NPM versions**: @@ -189,6 +189,47 @@ This package only guarantees the compatibility with the [version v0.21.0 of Meil 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/). +### Table Of Widgets + +- ✅ [InstantSearch](#-instantsearch) +- ❌[index](#-index) +- ✅ [SearchBox](#-searchbox) +- ✅ [Configure](#-configure) +- ❌[ConfigureRelatedItems](#-configure-related-items) +- ❌[Autocomplete](#-autocomplete) +- ✅ [Voice Search](#-voice-search) +- ✅ [Insight](#-insight) +- ✅ [Middleware](#-middleware) +- ✅ [RenderState](#-renderstate) +- ✅ [Hits](#-hits) +- ✅ [InfiniteHits](#-infinitehits) +- ✅ [Highlight](#-highlight) +- ✅ [Snippet](#-snippet) +- ❌[Geo Search](#-geo-search) +- ❌[Answers](#-answers) +- ✅ [RefinementList](#-refinementlist) +- ❌[HierarchicalMenu](#-hierarchicalmenu) +- ✅ [RangeSlider](#-rangeslider) +- ✅ [Menu](#-menu) +- ✅ [currentRefinements](#-currentrefinements) +- ✅ [RangeInput](#-rangeinput) +- ✅ [MenuSelect](#-menuselect) +- ✅ [ToggleRefinement](#-togglerefinement) +- ✅ [NumericMenu](#-numericmenu) +- ❌[RatingMenu](#-ratingmenu) +- ✅ [ClearRefinements](#-clearrefinements) +- ✅ [Pagination](#-pagination) +- ✅ [HitsPerPage](#-hitsperpage) +- ❌[Breadcrumb](#-breadcrumb) +- ✅ [Stats](#-stats) +- ❌[Analytics](#-analytics) +- ❌[QueryRuleCustomData](#-queryrulecustomdata) +- ❌[QueryRuleContext](#-queryrulecontext) +- ✅ [SortBy](#-sortby) +- ❌[RelevantSort](#-relevantsort) +- ✅ [Routing](#-routing) + + ### ✅ InstantSearch [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 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. -Example: +Example: Given the attribute `id` that has not been added in `filterableAttributes`: ```js @@ -738,15 +779,71 @@ The queryRuleContext widget lets you apply ruleContexts based on filters to trig No compatibility because MeiliSearch does not support Rules. -### ❌ SortBy +### ✅ SortBy [Sort by references](https://www.algolia.com/doc/api-reference/widgets/sort-by/js/) -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. +The `SortBy` widget is used to create multiple sort formulas. Allowing a user to change the way hits are sorted. -No compatibility because MeiliSearch does not support hierarchical facets. +- ✅ container: The CSS Selector or HTMLElement to insert the widget into. _required_ +- ✅ items: The list of different sorting possibilities. _required_ +- ✅ cssClasses: The CSS classes to override. +- ✅ transformItems: function receiving the items, called before displaying them. + +The usage of the `SortBy` widget differs from the one found in Algolia's documentation. In instant-meilisearch the following is possible: + +- Sort using different indexes. +- Different `sort` rules on the same index. + +The items list is composed of objects containing every sort possibility you want to provide to your user. Each object must contain two fields: + - `label`: What is showcased on the user interface ex: `Sort by Ascending Price` + - `value`: The sort formula. + +#### Sort formula + +A sort formula is expressed like this: `index:attribute:order`. + +`index` is mandatory, and when adding `attribute:order`, they must always be added together. -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). +When sorting on an attribute, the attribute has to be added to the [`sortableAttributes`](https://docs.meilisearch.com/reference/api/sortable_attributes.html) setting on your index. + +Example: +```js +[ + { label: 'Sort By Price', value: 'clothes:price:asc' } +] +``` + +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 to the `sortableAttributes` settings of the `clothes` index. + +#### Relevancy + +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/learn/core_concepts/relevancy.html#relevancy). For example, to favor exhaustivity over relevancy. + +See [relevancy guide](https://docs.meilisearch.com/learn/core_concepts/relevancy.html#relevancy). + +#### Example + +```js + instantsearch.widgets.sortBy({ + container: '#sort-by', + items: [ + { value: 'clothes', label: 'Relevant' }, // default index + { + value: 'clothes:price:asc', // Sort on descending price + label: 'Ascending price using query time sort', + }, + { + value: 'clothes:price:asc', // Sort on ascending price + label: 'Descending price using query time sort', + }, + { + value: 'clothes-sorted', // different index with different ranking rules. + label: 'Custom sort using a different index', + }, + ], + }), +``` ### ❌ RelevantSort diff --git a/cypress/integration/search-ui.spec.js b/cypress/integration/search-ui.spec.js index 4bba28aa..ce9300ef 100644 --- a/cypress/integration/search-ui.spec.js +++ b/cypress/integration/search-ui.spec.js @@ -42,6 +42,20 @@ describe(`${playground} playground test`, () => { cy.get(HIT_ITEM_CLASS).eq(0).contains('9.99 $') }) + it('Sort by recommendationCound ascending', () => { + const select = `.ais-SortBy-select` + cy.get(select).select('steam-video-games:recommendationCount:asc') + cy.wait(1000) + cy.get(HIT_ITEM_CLASS).eq(0).contains('Rag Doll Kung Fu') + }) + + it('Sort by default relevancy', () => { + const select = `.ais-SortBy-select` + cy.get(select).select('steam-video-games') + cy.wait(1000) + cy.get(HIT_ITEM_CLASS).eq(0).contains('Counter-Strike') + }) + it('click on facets', () => { const checkbox = `.ais-RefinementList-list .ais-RefinementList-checkbox` cy.get(checkbox).eq(1).click() diff --git a/package.json b/package.json index ec0b95aa..69872bdb 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "test:e2e:all": "sh scripts/e2e.sh", "test:e2e:watch": "concurrently --kill-others -s first \"NODE_ENV=test yarn playground:javascript\" \"cypress open --env playground=javascript\"", "test:all": "yarn test:e2e:all && yarn test && test:build", + "cy:open": "cypress open", "playground:vue": "yarn --cwd ./playgrounds/vue && yarn --cwd ./playgrounds/vue serve", "playground:react": "yarn --cwd ./playgrounds/react && yarn --cwd ./playgrounds/react start", "playground:javascript": "yarn --cwd ./playgrounds/javascript && yarn --cwd ./playgrounds/javascript start", @@ -52,7 +53,7 @@ "url": "https://github.com/meilisearch/instant-meilisearch.git" }, "dependencies": { - "meilisearch": "^0.20.0" + "meilisearch": "^0.20.1" }, "devDependencies": { "@babel/cli": "^7.14.8", diff --git a/playgrounds/angular/src/app/app.component.html b/playgrounds/angular/src/app/app.component.html index 3ea37be5..4f62caa7 100644 --- a/playgrounds/angular/src/app/app.component.html +++ b/playgrounds/angular/src/app/app.component.html @@ -10,6 +10,19 @@

MeiliSearch + Angular InstantSearch

+

Genres

@@ -40,8 +53,9 @@

Misc

-
${{hit.price}}
-
{{hit.releaseDate}}
+
price: ${{hit.price}}
+
Release date: {{hit.releaseDate}}
+
Recommendation: {{hit.recommendationCount}}
diff --git a/playgrounds/angular/src/app/app.component.ts b/playgrounds/angular/src/app/app.component.ts index 58310951..054ddbad 100644 --- a/playgrounds/angular/src/app/app.component.ts +++ b/playgrounds/angular/src/app/app.component.ts @@ -2,8 +2,8 @@ import { Component } from '@angular/core' import { instantMeiliSearch } from '../../../../src' const searchClient = instantMeiliSearch( - 'https://ms-9060336c1f95-106.saas.meili.dev', - '5d7e1929728417466fd5a82da5a28beb540d3e5bbaf4e01f742e1fb5fd72bb66' + 'https://demo-steam.meilisearch.com/', + '90b03f9c47d0f321afae5ae4c4e4f184f53372a2953ab77bca679ff447ecc15c' ) @Component({ diff --git a/playgrounds/html/public/index.html b/playgrounds/html/public/index.html index 894e4ce1..4e535289 100644 --- a/playgrounds/html/public/index.html +++ b/playgrounds/html/public/index.html @@ -25,10 +25,10 @@ const search = instantsearch({ indexName: "steam-video-games", searchClient: instantMeiliSearch( - 'https://ms-9060336c1f95-106.saas.meili.dev', - '5d7e1929728417466fd5a82da5a28beb540d3e5bbaf4e01f742e1fb5fd72bb66', + 'https://demo-steam.meilisearch.com', + '90b03f9c47d0f321afae5ae4c4e4f184f53372a2953ab77bca679ff447ecc15c', ) - }); + }); search.addWidgets([ instantsearch.widgets.searchBox({ container: "#searchbox", diff --git a/playgrounds/javascript/index.html b/playgrounds/javascript/index.html index 547eda71..474a85d1 100644 --- a/playgrounds/javascript/index.html +++ b/playgrounds/javascript/index.html @@ -29,7 +29,7 @@

Search in Steam video games 🎮

- +

Genres

Players

diff --git a/playgrounds/javascript/src/app.js b/playgrounds/javascript/src/app.js index 739f77c6..9d01f27f 100644 --- a/playgrounds/javascript/src/app.js +++ b/playgrounds/javascript/src/app.js @@ -3,8 +3,8 @@ import { instantMeiliSearch } from '../../../src/index' const search = instantsearch({ indexName: 'steam-video-games', searchClient: instantMeiliSearch( - 'https://ms-9060336c1f95-106.saas.meili.dev', - '5d7e1929728417466fd5a82da5a28beb540d3e5bbaf4e01f742e1fb5fd72bb66', + 'https://demo-steam.meilisearch.com', + '90b03f9c47d0f321afae5ae4c4e4f184f53372a2953ab77bca679ff447ecc15c', { limitPerRequest: 30, } @@ -12,6 +12,20 @@ const search = instantsearch({ }) search.addWidgets([ + instantsearch.widgets.sortBy({ + container: '#sort-by', + items: [ + { value: 'steam-video-games', label: 'Relevant' }, + { + value: 'steam-video-games:recommendationCount:desc', + label: 'Most Recommended', + }, + { + value: 'steam-video-games:recommendationCount:asc', + label: 'Least Recommended', + }, + ], + }), instantsearch.widgets.searchBox({ container: '#searchbox', }), @@ -32,6 +46,7 @@ search.addWidgets([ }), instantsearch.widgets.configure({ hitsPerPage: 6, + attributesToSnippet: ['description:150'], }), instantsearch.widgets.refinementList({ container: '#misc-list', @@ -51,6 +66,7 @@ search.addWidgets([
price: {{price}}
release date: {{releaseDate}}
+
Recommendation: {{recommendationCount}}
`, }, diff --git a/playgrounds/react/src/App.js b/playgrounds/react/src/App.js index 5b538aff..0d5cd59e 100644 --- a/playgrounds/react/src/App.js +++ b/playgrounds/react/src/App.js @@ -9,13 +9,16 @@ import { ClearRefinements, RefinementList, Configure, + SortBy, + Snippet, } from 'react-instantsearch-dom' + import './App.css' import { instantMeiliSearch } from '../../../src/index' const searchClient = instantMeiliSearch( - 'https://ms-9060336c1f95-106.saas.meili.dev', - '5d7e1929728417466fd5a82da5a28beb540d3e5bbaf4e01f742e1fb5fd72bb66', + 'https://demo-steam.meilisearch.com/', + '90b03f9c47d0f321afae5ae4c4e4f184f53372a2953ab77bca679ff447ecc15c', { paginationTotalHits: 60, primaryKey: 'id', @@ -39,6 +42,20 @@ const App = () => (
+

Genres

Players

@@ -47,7 +64,11 @@ const App = () => (

Misc

- +
@@ -57,18 +78,27 @@ const App = () => (
) -const Hit = ({ hit }) => ( -
-
- -
- {hit.name} -
- +const Hit = ({ hit }) => { + return ( +
+
+ +
+ {hit.name} +
+ +
+
+ price: {hit.price} +
+
+ release date: {hit.releaseDate} +
+
+ Recommended: {hit.recommendationCount} +
-
price: {hit.price}
-
release date: {hit.releaseDate}
-
-) + ) +} export default App diff --git a/playgrounds/vue/src/App.vue b/playgrounds/vue/src/App.vue index 589019c7..949fa0ac 100644 --- a/playgrounds/vue/src/App.vue +++ b/playgrounds/vue/src/App.vue @@ -17,6 +17,19 @@ Clear all filters +

Genres

Players

@@ -40,8 +53,11 @@
-
price: {{ item.price }}
-
release date: {{ item.releaseDate }}
+
Price: {{ item.price }}
+
Release date: {{ item.releaseDate }}
+
+ Recommended: {{ item.recommendationCount }} +
@@ -66,12 +82,21 @@ import { instantMeiliSearch } from '../../../src/index' export default { data() { return { + recommendation: '', searchClient: instantMeiliSearch( - 'https://ms-9060336c1f95-106.saas.meili.dev', - '5d7e1929728417466fd5a82da5a28beb540d3e5bbaf4e01f742e1fb5fd72bb66' + 'https://demo-steam.meilisearch.com', + '90b03f9c47d0f321afae5ae4c4e4f184f53372a2953ab77bca679ff447ecc15c' ), } }, + methods: { + order: function (event, searchParameters, refine) { + refine({ + ...searchParameters, + sort: this.recommendation, + }) + }, + }, } diff --git a/src/adapter/to-meilisearch-params.ts b/src/adapter/to-meilisearch-params.ts index 90fe9507..04bd967f 100644 --- a/src/adapter/to-meilisearch-params.ts +++ b/src/adapter/to-meilisearch-params.ts @@ -51,7 +51,7 @@ export const adaptToMeiliSearchParams: AdaptToMeiliSearchParams = function ( filters = '', numericFilters = [], }, - { paginationTotalHits, placeholderSearch } + { paginationTotalHits, placeholderSearch, sort } ) { const limit = paginationTotalHits const meilisearchFilters = facetFiltersToMeiliSearchFilter(facetFilters) @@ -63,8 +63,9 @@ export const adaptToMeiliSearchParams: AdaptToMeiliSearchParams = function ( ...(facets?.length && { facetsDistribution: facets }), ...(attributesToCrop && { attributesToCrop }), ...(attributesToRetrieve && { attributesToRetrieve }), - ...(filter && { filter: filter }), + ...(filter.length && { filter: filter }), attributesToHighlight: attributesToHighlight || ['*'], limit: (!placeholderSearch && query === '') || !limit ? 0 : limit, + ...(sort?.length && { sort: [sort] }), } } diff --git a/src/client/index.ts b/src/client/index.ts index b5f808db..e19f6d24 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -13,10 +13,10 @@ export function instantMeiliSearch( search: async function (instantSearchRequests) { try { const isSearchRequest = instantSearchRequests[0] - const { - params: instantSearchParams, - indexName: indexUid, - } = isSearchRequest + const { params: instantSearchParams, indexName } = isSearchRequest + + // Split index name and possible sorting rules + const [indexUid, ...sortByArray] = indexName.split(':') const { paginationTotalHits, primaryKey, placeholderSearch } = options const { page, hitsPerPage } = instantSearchParams @@ -28,6 +28,7 @@ export function instantMeiliSearch( placeholderSearch: placeholderSearch !== false, // true by default hitsPerPage: hitsPerPage === undefined ? 20 : hitsPerPage, // 20 is the MeiliSearch's default limit value. `hitsPerPage` can be changed with `InsantSearch.configure`. page: page || 0, // default page is 0 if none is provided + sort: sortByArray.join(':') || '', } // Adapt IS params to MeiliSearch params @@ -35,7 +36,6 @@ export function instantMeiliSearch( instantSearchParams, context ) - const cachedFacet = cacheFilters(msSearchParams.filter) // Executes the search with MeiliSearch diff --git a/src/types/types.ts b/src/types/types.ts index 04e9014c..9fd74094 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -23,6 +23,7 @@ export type IMSearchParams = Omit< export type ISSearchParams = Omit & { query?: string facetFilters?: Filter + sort?: string } export type ISSearchRequest = { @@ -65,6 +66,7 @@ export type InstantMeiliSearchContext = { primaryKey: string | undefined client: MStypes.MeiliSearch placeholderSearch: boolean + sort?: string } export type FormattedHit = { diff --git a/yarn.lock b/yarn.lock index d6ad827a..63127723 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5240,10 +5240,10 @@ mdn-data@2.0.4: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA== -meilisearch@^0.20.0: - version "0.20.0" - resolved "https://registry.yarnpkg.com/meilisearch/-/meilisearch-0.20.0.tgz#42899fec7a2ddefcd035e30ed5dd47aa65a6727f" - integrity sha512-J+0GIyNVnH6dAM0lmwhWvYFO0Zru5djfbU2bteHIF1gqFP89uPyaOH7oIq5ntZSs/9Z6ogkD2/dLQSLYp4uizg== +meilisearch@^0.20.1: + version "0.20.1" + resolved "https://registry.yarnpkg.com/meilisearch/-/meilisearch-0.20.1.tgz#db46aee790483e55a0f2ef1f9efc24532067ba48" + integrity sha512-5IGTiM3Bbc9gHUxqzsLBJdQgi2NVZxbEfwnc95tKIpkbFWr6fZpKL+jp747u/eQkvbAo3JtOPnMikCAwTQ3uhw== dependencies: cross-fetch "^3.1.4"