diff --git a/.changeset/red-fans-cross.md b/.changeset/red-fans-cross.md new file mode 100644 index 00000000..8c03cda8 --- /dev/null +++ b/.changeset/red-fans-cross.md @@ -0,0 +1,5 @@ +--- +"@meilisearch/instant-meilisearch": minor +--- + +Add support for multiple sort attributes diff --git a/README.md b/README.md index edbf219a..f7ed40ca 100644 --- a/README.md +++ b/README.md @@ -1032,6 +1032,19 @@ Example: 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. +#### Sort by multiple attributes +When sorting by mutiple fields sort formula is expressed like this: `index:attribute:order,attribute2:order`. + +Example: +```js +[ + { label: 'Sort By Price And Title', value: 'clothes:price:asc,title:asc' } +] +``` + +⚠️ Attributes with comma in their name are not allowed. + + #### 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. diff --git a/packages/instant-meilisearch/__tests__/sort.test.ts b/packages/instant-meilisearch/__tests__/sort.test.ts new file mode 100644 index 00000000..0469cf6b --- /dev/null +++ b/packages/instant-meilisearch/__tests__/sort.test.ts @@ -0,0 +1,51 @@ +import { + searchClient, + dataset, + Movies, + meilisearchClient, +} from './assets/utils' + +describe('Sort browser test', () => { + beforeAll(async () => { + const deleteTask = await meilisearchClient.deleteIndex('movies') + await meilisearchClient.waitForTask(deleteTask.taskUid) + await meilisearchClient.index('movies').updateSettings({ + sortableAttributes: ['release_date', 'title'], + }) + + const documentsTask = await meilisearchClient + .index('movies') + .addDocuments(dataset) + await meilisearchClient.index('movies').waitForTask(documentsTask.taskUid) + }) + + test('sort-by one field', async () => { + const response = await searchClient.search([ + { + indexName: 'movies:release_date:desc', + params: { + query: '', + hitsPerPage: 1, + }, + }, + ]) + + const hits = response.results[0].hits + expect(hits.length).toBe(1) + }) + + test('sort-by mutiple fields', async () => { + const response = await searchClient.search([ + { + indexName: 'movies:release_date:desc,title:asc', + params: { + query: '', + hitsPerPage: 1, + }, + }, + ]) + + const hits = response.results[0].hits + expect(hits.length).toBe(1) + }) +}) diff --git a/packages/instant-meilisearch/src/adapter/search-request-adapter/search-params-adapter.ts b/packages/instant-meilisearch/src/adapter/search-request-adapter/search-params-adapter.ts index 6066ca60..cb3e8b2a 100644 --- a/packages/instant-meilisearch/src/adapter/search-request-adapter/search-params-adapter.ts +++ b/packages/instant-meilisearch/src/adapter/search-request-adapter/search-params-adapter.ts @@ -166,7 +166,7 @@ export function MeiliParamsCreator(searchContext: SearchContext) { }, addSort() { if (sort?.length) { - meiliSearchParams.sort = [sort] + meiliSearchParams.sort = Array.isArray(sort) ? sort : [sort] } }, addGeoSearchRules() { diff --git a/packages/instant-meilisearch/src/contexts/search-context.ts b/packages/instant-meilisearch/src/contexts/search-context.ts index 310c93d4..6fa050e8 100644 --- a/packages/instant-meilisearch/src/contexts/search-context.ts +++ b/packages/instant-meilisearch/src/contexts/search-context.ts @@ -5,6 +5,7 @@ import { } from '../types' import { createPaginationState } from './pagination-context' +import { createSortState } from './sort-context' /** * @param {AlgoliaMultipleQueriesQuery} searchRequest @@ -25,10 +26,12 @@ export function createSearchContext( instantSearchParams?.page ) + const sortState = createSortState(sortByArray.join(':')) + const searchContext: SearchContext = { ...options, ...instantSearchParams, - sort: sortByArray.join(':') || '', + sort: sortState, indexUid, pagination: paginationState, placeholderSearch: options.placeholderSearch !== false, // true by default diff --git a/packages/instant-meilisearch/src/contexts/sort-context.ts b/packages/instant-meilisearch/src/contexts/sort-context.ts new file mode 100644 index 00000000..9b387000 --- /dev/null +++ b/packages/instant-meilisearch/src/contexts/sort-context.ts @@ -0,0 +1,10 @@ +/** + * @param {string} rawSort + * @returns {string[]} + */ +export function createSortState(rawSort: string): string[] { + return rawSort + .split(',') + .map((sort) => sort.trim()) + .filter((sort) => !!sort) +} diff --git a/packages/instant-meilisearch/src/types/types.ts b/packages/instant-meilisearch/src/types/types.ts index b314da83..2bb14616 100644 --- a/packages/instant-meilisearch/src/types/types.ts +++ b/packages/instant-meilisearch/src/types/types.ts @@ -71,7 +71,7 @@ export type SearchContext = Omit & keepZeroFacets: boolean insideBoundingBox?: InsideBoundingBox cropMarker?: string - sort?: string + sort?: string | string[] primaryKey?: string matchingStrategy?: MatchingStrategies }