From 15c183c07c1662e9d59c7bec914a5a57a93bde36 Mon Sep 17 00:00:00 2001 From: meili-bot <74670311+meili-bot@users.noreply.github.com> Date: Wed, 5 Jul 2023 15:05:55 -0300 Subject: [PATCH 1/9] Update README.md From 4338c47a7de98a6d6ce107c4c52792cb5ce35b91 Mon Sep 17 00:00:00 2001 From: cvermand <33010418+bidoubiwa@users.noreply.github.com> Date: Mon, 10 Jul 2023 16:04:59 +0200 Subject: [PATCH 2/9] Add sort order for facet values for Meilisearch v1.3 (#1536) * Add sort order for facet values for Meilisearch v1.3 * Change type casting of the settings object in settings tests --- src/types/types.ts | 4 + tests/__snapshots__/faceting.test.ts.snap | 75 +++ tests/__snapshots__/settings.test.ts.snap | 773 ++++++++++++++++++++++ tests/faceting.test.ts | 13 +- tests/settings.test.ts | 140 +--- 5 files changed, 875 insertions(+), 130 deletions(-) create mode 100644 tests/__snapshots__/faceting.test.ts.snap create mode 100644 tests/__snapshots__/settings.test.ts.snap diff --git a/src/types/types.ts b/src/types/types.ts index 15842c68b..afa7e04a8 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -255,9 +255,13 @@ export type TypoTolerance = { } } | null +export type FacetOrder = 'alpha' | 'count' + export type Faceting = { maxValuesPerFacet?: number | null + sortFacetValuesBy?: Record | null } + export type PaginationSettings = { maxTotalHits?: number | null } diff --git a/tests/__snapshots__/faceting.test.ts.snap b/tests/__snapshots__/faceting.test.ts.snap new file mode 100644 index 000000000..6c0e9262b --- /dev/null +++ b/tests/__snapshots__/faceting.test.ts.snap @@ -0,0 +1,75 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test on faceting Admin key: Get default faceting object 1`] = ` +Object { + "maxValuesPerFacet": 100, + "sortFacetValuesBy": Object { + "*": "alpha", + }, +} +`; + +exports[`Test on faceting Admin key: Reset faceting 1`] = ` +Object { + "maxValuesPerFacet": 100, + "sortFacetValuesBy": Object { + "*": "alpha", + }, +} +`; + +exports[`Test on faceting Admin key: Update faceting at null 1`] = ` +Object { + "maxValuesPerFacet": 100, + "sortFacetValuesBy": Object { + "*": "alpha", + }, +} +`; + +exports[`Test on faceting Admin key: Update faceting settings 1`] = ` +Object { + "maxValuesPerFacet": 12, + "sortFacetValuesBy": Object { + "*": "alpha", + "test": "count", + }, +} +`; + +exports[`Test on faceting Master key: Get default faceting object 1`] = ` +Object { + "maxValuesPerFacet": 100, + "sortFacetValuesBy": Object { + "*": "alpha", + }, +} +`; + +exports[`Test on faceting Master key: Reset faceting 1`] = ` +Object { + "maxValuesPerFacet": 100, + "sortFacetValuesBy": Object { + "*": "alpha", + }, +} +`; + +exports[`Test on faceting Master key: Update faceting at null 1`] = ` +Object { + "maxValuesPerFacet": 100, + "sortFacetValuesBy": Object { + "*": "alpha", + }, +} +`; + +exports[`Test on faceting Master key: Update faceting settings 1`] = ` +Object { + "maxValuesPerFacet": 12, + "sortFacetValuesBy": Object { + "*": "alpha", + "test": "count", + }, +} +`; diff --git a/tests/__snapshots__/settings.test.ts.snap b/tests/__snapshots__/settings.test.ts.snap new file mode 100644 index 000000000..fdc4721df --- /dev/null +++ b/tests/__snapshots__/settings.test.ts.snap @@ -0,0 +1,773 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test on settings Admin key: Get default settings of an index 1`] = ` +Object { + "displayedAttributes": Array [ + "*", + ], + "distinctAttribute": null, + "faceting": Object { + "maxValuesPerFacet": 100, + "sortFacetValuesBy": Object { + "*": "alpha", + }, + }, + "filterableAttributes": Array [], + "pagination": Object { + "maxTotalHits": 1000, + }, + "rankingRules": Array [ + "words", + "typo", + "proximity", + "attribute", + "sort", + "exactness", + ], + "searchableAttributes": Array [ + "*", + ], + "sortableAttributes": Array [], + "stopWords": Array [], + "synonyms": Object {}, + "typoTolerance": Object { + "disableOnAttributes": Array [], + "disableOnWords": Array [], + "enabled": true, + "minWordSizeForTypos": Object { + "oneTypo": 5, + "twoTypos": 9, + }, + }, +} +`; + +exports[`Test on settings Admin key: Get default settings of empty index with primary key 1`] = ` +Object { + "displayedAttributes": Array [ + "*", + ], + "distinctAttribute": null, + "faceting": Object { + "maxValuesPerFacet": 100, + "sortFacetValuesBy": Object { + "*": "alpha", + }, + }, + "filterableAttributes": Array [], + "pagination": Object { + "maxTotalHits": 1000, + }, + "rankingRules": Array [ + "words", + "typo", + "proximity", + "attribute", + "sort", + "exactness", + ], + "searchableAttributes": Array [ + "*", + ], + "sortableAttributes": Array [], + "stopWords": Array [], + "synonyms": Object {}, + "typoTolerance": Object { + "disableOnAttributes": Array [], + "disableOnWords": Array [], + "enabled": true, + "minWordSizeForTypos": Object { + "oneTypo": 5, + "twoTypos": 9, + }, + }, +} +`; + +exports[`Test on settings Admin key: Reset settings 1`] = ` +Object { + "displayedAttributes": Array [ + "*", + ], + "distinctAttribute": null, + "faceting": Object { + "maxValuesPerFacet": 100, + "sortFacetValuesBy": Object { + "*": "alpha", + }, + }, + "filterableAttributes": Array [], + "pagination": Object { + "maxTotalHits": 1000, + }, + "rankingRules": Array [ + "words", + "typo", + "proximity", + "attribute", + "sort", + "exactness", + ], + "searchableAttributes": Array [ + "*", + ], + "sortableAttributes": Array [], + "stopWords": Array [], + "synonyms": Object {}, + "typoTolerance": Object { + "disableOnAttributes": Array [], + "disableOnWords": Array [], + "enabled": true, + "minWordSizeForTypos": Object { + "oneTypo": 5, + "twoTypos": 9, + }, + }, +} +`; + +exports[`Test on settings Admin key: Reset settings of empty index 1`] = ` +Object { + "displayedAttributes": Array [ + "*", + ], + "distinctAttribute": null, + "faceting": Object { + "maxValuesPerFacet": 100, + "sortFacetValuesBy": Object { + "*": "alpha", + }, + }, + "filterableAttributes": Array [], + "pagination": Object { + "maxTotalHits": 1000, + }, + "rankingRules": Array [ + "words", + "typo", + "proximity", + "attribute", + "sort", + "exactness", + ], + "searchableAttributes": Array [ + "*", + ], + "sortableAttributes": Array [], + "stopWords": Array [], + "synonyms": Object {}, + "typoTolerance": Object { + "disableOnAttributes": Array [], + "disableOnWords": Array [], + "enabled": true, + "minWordSizeForTypos": Object { + "oneTypo": 5, + "twoTypos": 9, + }, + }, +} +`; + +exports[`Test on settings Admin key: Update searchableAttributes settings on empty index 1`] = ` +Object { + "displayedAttributes": Array [ + "*", + ], + "distinctAttribute": null, + "faceting": Object { + "maxValuesPerFacet": 100, + "sortFacetValuesBy": Object { + "*": "alpha", + }, + }, + "filterableAttributes": Array [], + "pagination": Object { + "maxTotalHits": 1000, + }, + "rankingRules": Array [ + "words", + "typo", + "proximity", + "attribute", + "sort", + "exactness", + ], + "searchableAttributes": Array [ + "title", + ], + "sortableAttributes": Array [], + "stopWords": Array [], + "synonyms": Object {}, + "typoTolerance": Object { + "disableOnAttributes": Array [], + "disableOnWords": Array [], + "enabled": true, + "minWordSizeForTypos": Object { + "oneTypo": 5, + "twoTypos": 9, + }, + }, +} +`; + +exports[`Test on settings Admin key: Update searchableAttributes settings on empty index with a primary key 1`] = ` +Object { + "displayedAttributes": Array [ + "*", + ], + "distinctAttribute": null, + "faceting": Object { + "maxValuesPerFacet": 100, + "sortFacetValuesBy": Object { + "*": "alpha", + }, + }, + "filterableAttributes": Array [], + "pagination": Object { + "maxTotalHits": 1000, + }, + "rankingRules": Array [ + "words", + "typo", + "proximity", + "attribute", + "sort", + "exactness", + ], + "searchableAttributes": Array [ + "title", + ], + "sortableAttributes": Array [], + "stopWords": Array [], + "synonyms": Object {}, + "typoTolerance": Object { + "disableOnAttributes": Array [], + "disableOnWords": Array [], + "enabled": true, + "minWordSizeForTypos": Object { + "oneTypo": 5, + "twoTypos": 9, + }, + }, +} +`; + +exports[`Test on settings Admin key: Update settings 1`] = ` +Object { + "displayedAttributes": Array [ + "title", + ], + "distinctAttribute": "title", + "faceting": Object { + "maxValuesPerFacet": 50, + "sortFacetValuesBy": Object { + "*": "alpha", + }, + }, + "filterableAttributes": Array [ + "title", + ], + "pagination": Object { + "maxTotalHits": 1000, + }, + "rankingRules": Array [ + "id:asc", + "typo", + ], + "searchableAttributes": Array [ + "title", + ], + "sortableAttributes": Array [ + "title", + ], + "stopWords": Array [ + "the", + ], + "synonyms": Object { + "harry": Array [ + "potter", + ], + }, + "typoTolerance": Object { + "disableOnAttributes": Array [ + "comment", + ], + "disableOnWords": Array [ + "prince", + ], + "enabled": false, + "minWordSizeForTypos": Object { + "oneTypo": 1, + "twoTypos": 100, + }, + }, +} +`; + +exports[`Test on settings Admin key: Update settings on empty index with primary key 1`] = ` +Object { + "displayedAttributes": Array [ + "*", + ], + "distinctAttribute": "title", + "faceting": Object { + "maxValuesPerFacet": 100, + "sortFacetValuesBy": Object { + "*": "alpha", + }, + }, + "filterableAttributes": Array [], + "pagination": Object { + "maxTotalHits": 1000, + }, + "rankingRules": Array [ + "title:asc", + "typo", + ], + "searchableAttributes": Array [ + "*", + ], + "sortableAttributes": Array [], + "stopWords": Array [ + "the", + ], + "synonyms": Object {}, + "typoTolerance": Object { + "disableOnAttributes": Array [], + "disableOnWords": Array [], + "enabled": true, + "minWordSizeForTypos": Object { + "oneTypo": 5, + "twoTypos": 9, + }, + }, +} +`; + +exports[`Test on settings Admin key: Update settings with all null values 1`] = ` +Object { + "displayedAttributes": Array [ + "*", + ], + "distinctAttribute": null, + "faceting": Object { + "maxValuesPerFacet": 100, + "sortFacetValuesBy": Object { + "*": "alpha", + }, + }, + "filterableAttributes": Array [], + "pagination": Object { + "maxTotalHits": 1000, + }, + "rankingRules": Array [ + "words", + "typo", + "proximity", + "attribute", + "sort", + "exactness", + ], + "searchableAttributes": Array [ + "*", + ], + "sortableAttributes": Array [], + "stopWords": Array [], + "synonyms": Object {}, + "typoTolerance": Object { + "disableOnAttributes": Array [], + "disableOnWords": Array [], + "enabled": true, + "minWordSizeForTypos": Object { + "oneTypo": 5, + "twoTypos": 9, + }, + }, +} +`; + +exports[`Test on settings Master key: Get default settings of an index 1`] = ` +Object { + "displayedAttributes": Array [ + "*", + ], + "distinctAttribute": null, + "faceting": Object { + "maxValuesPerFacet": 100, + "sortFacetValuesBy": Object { + "*": "alpha", + }, + }, + "filterableAttributes": Array [], + "pagination": Object { + "maxTotalHits": 1000, + }, + "rankingRules": Array [ + "words", + "typo", + "proximity", + "attribute", + "sort", + "exactness", + ], + "searchableAttributes": Array [ + "*", + ], + "sortableAttributes": Array [], + "stopWords": Array [], + "synonyms": Object {}, + "typoTolerance": Object { + "disableOnAttributes": Array [], + "disableOnWords": Array [], + "enabled": true, + "minWordSizeForTypos": Object { + "oneTypo": 5, + "twoTypos": 9, + }, + }, +} +`; + +exports[`Test on settings Master key: Get default settings of empty index with primary key 1`] = ` +Object { + "displayedAttributes": Array [ + "*", + ], + "distinctAttribute": null, + "faceting": Object { + "maxValuesPerFacet": 100, + "sortFacetValuesBy": Object { + "*": "alpha", + }, + }, + "filterableAttributes": Array [], + "pagination": Object { + "maxTotalHits": 1000, + }, + "rankingRules": Array [ + "words", + "typo", + "proximity", + "attribute", + "sort", + "exactness", + ], + "searchableAttributes": Array [ + "*", + ], + "sortableAttributes": Array [], + "stopWords": Array [], + "synonyms": Object {}, + "typoTolerance": Object { + "disableOnAttributes": Array [], + "disableOnWords": Array [], + "enabled": true, + "minWordSizeForTypos": Object { + "oneTypo": 5, + "twoTypos": 9, + }, + }, +} +`; + +exports[`Test on settings Master key: Reset settings 1`] = ` +Object { + "displayedAttributes": Array [ + "*", + ], + "distinctAttribute": null, + "faceting": Object { + "maxValuesPerFacet": 100, + "sortFacetValuesBy": Object { + "*": "alpha", + }, + }, + "filterableAttributes": Array [], + "pagination": Object { + "maxTotalHits": 1000, + }, + "rankingRules": Array [ + "words", + "typo", + "proximity", + "attribute", + "sort", + "exactness", + ], + "searchableAttributes": Array [ + "*", + ], + "sortableAttributes": Array [], + "stopWords": Array [], + "synonyms": Object {}, + "typoTolerance": Object { + "disableOnAttributes": Array [], + "disableOnWords": Array [], + "enabled": true, + "minWordSizeForTypos": Object { + "oneTypo": 5, + "twoTypos": 9, + }, + }, +} +`; + +exports[`Test on settings Master key: Reset settings of empty index 1`] = ` +Object { + "displayedAttributes": Array [ + "*", + ], + "distinctAttribute": null, + "faceting": Object { + "maxValuesPerFacet": 100, + "sortFacetValuesBy": Object { + "*": "alpha", + }, + }, + "filterableAttributes": Array [], + "pagination": Object { + "maxTotalHits": 1000, + }, + "rankingRules": Array [ + "words", + "typo", + "proximity", + "attribute", + "sort", + "exactness", + ], + "searchableAttributes": Array [ + "*", + ], + "sortableAttributes": Array [], + "stopWords": Array [], + "synonyms": Object {}, + "typoTolerance": Object { + "disableOnAttributes": Array [], + "disableOnWords": Array [], + "enabled": true, + "minWordSizeForTypos": Object { + "oneTypo": 5, + "twoTypos": 9, + }, + }, +} +`; + +exports[`Test on settings Master key: Update searchableAttributes settings on empty index 1`] = ` +Object { + "displayedAttributes": Array [ + "*", + ], + "distinctAttribute": null, + "faceting": Object { + "maxValuesPerFacet": 100, + "sortFacetValuesBy": Object { + "*": "alpha", + }, + }, + "filterableAttributes": Array [], + "pagination": Object { + "maxTotalHits": 1000, + }, + "rankingRules": Array [ + "words", + "typo", + "proximity", + "attribute", + "sort", + "exactness", + ], + "searchableAttributes": Array [ + "title", + ], + "sortableAttributes": Array [], + "stopWords": Array [], + "synonyms": Object {}, + "typoTolerance": Object { + "disableOnAttributes": Array [], + "disableOnWords": Array [], + "enabled": true, + "minWordSizeForTypos": Object { + "oneTypo": 5, + "twoTypos": 9, + }, + }, +} +`; + +exports[`Test on settings Master key: Update searchableAttributes settings on empty index with a primary key 1`] = ` +Object { + "displayedAttributes": Array [ + "*", + ], + "distinctAttribute": null, + "faceting": Object { + "maxValuesPerFacet": 100, + "sortFacetValuesBy": Object { + "*": "alpha", + }, + }, + "filterableAttributes": Array [], + "pagination": Object { + "maxTotalHits": 1000, + }, + "rankingRules": Array [ + "words", + "typo", + "proximity", + "attribute", + "sort", + "exactness", + ], + "searchableAttributes": Array [ + "title", + ], + "sortableAttributes": Array [], + "stopWords": Array [], + "synonyms": Object {}, + "typoTolerance": Object { + "disableOnAttributes": Array [], + "disableOnWords": Array [], + "enabled": true, + "minWordSizeForTypos": Object { + "oneTypo": 5, + "twoTypos": 9, + }, + }, +} +`; + +exports[`Test on settings Master key: Update settings 1`] = ` +Object { + "displayedAttributes": Array [ + "title", + ], + "distinctAttribute": "title", + "faceting": Object { + "maxValuesPerFacet": 50, + "sortFacetValuesBy": Object { + "*": "alpha", + }, + }, + "filterableAttributes": Array [ + "title", + ], + "pagination": Object { + "maxTotalHits": 1000, + }, + "rankingRules": Array [ + "id:asc", + "typo", + ], + "searchableAttributes": Array [ + "title", + ], + "sortableAttributes": Array [ + "title", + ], + "stopWords": Array [ + "the", + ], + "synonyms": Object { + "harry": Array [ + "potter", + ], + }, + "typoTolerance": Object { + "disableOnAttributes": Array [ + "comment", + ], + "disableOnWords": Array [ + "prince", + ], + "enabled": false, + "minWordSizeForTypos": Object { + "oneTypo": 1, + "twoTypos": 100, + }, + }, +} +`; + +exports[`Test on settings Master key: Update settings on empty index with primary key 1`] = ` +Object { + "displayedAttributes": Array [ + "*", + ], + "distinctAttribute": "title", + "faceting": Object { + "maxValuesPerFacet": 100, + "sortFacetValuesBy": Object { + "*": "alpha", + }, + }, + "filterableAttributes": Array [], + "pagination": Object { + "maxTotalHits": 1000, + }, + "rankingRules": Array [ + "title:asc", + "typo", + ], + "searchableAttributes": Array [ + "*", + ], + "sortableAttributes": Array [], + "stopWords": Array [ + "the", + ], + "synonyms": Object {}, + "typoTolerance": Object { + "disableOnAttributes": Array [], + "disableOnWords": Array [], + "enabled": true, + "minWordSizeForTypos": Object { + "oneTypo": 5, + "twoTypos": 9, + }, + }, +} +`; + +exports[`Test on settings Master key: Update settings with all null values 1`] = ` +Object { + "displayedAttributes": Array [ + "*", + ], + "distinctAttribute": null, + "faceting": Object { + "maxValuesPerFacet": 100, + "sortFacetValuesBy": Object { + "*": "alpha", + }, + }, + "filterableAttributes": Array [], + "pagination": Object { + "maxTotalHits": 1000, + }, + "rankingRules": Array [ + "words", + "typo", + "proximity", + "attribute", + "sort", + "exactness", + ], + "searchableAttributes": Array [ + "*", + ], + "sortableAttributes": Array [], + "stopWords": Array [], + "synonyms": Object {}, + "typoTolerance": Object { + "disableOnAttributes": Array [], + "disableOnWords": Array [], + "enabled": true, + "minWordSizeForTypos": Object { + "oneTypo": 5, + "twoTypos": 9, + }, + }, +} +`; diff --git a/tests/faceting.test.ts b/tests/faceting.test.ts index e8bbe61fe..64457f55b 100644 --- a/tests/faceting.test.ts +++ b/tests/faceting.test.ts @@ -38,18 +38,21 @@ describe.each([{ permission: 'Master' }, { permission: 'Admin' }])( const response = await client.index(index.uid).getFaceting() - expect(response).toEqual({ maxValuesPerFacet: 100 }) + expect(response).toMatchSnapshot() }) test(`${permission} key: Update faceting settings`, async () => { const client = await getClient(permission) - const newFaceting = { maxValuesPerFacet: 12 } + const newFaceting = { + maxValuesPerFacet: 12, + sortFacetValuesBy: { test: 'count' as 'count' }, + } const task = await client.index(index.uid).updateFaceting(newFaceting) await client.index(index.uid).waitForTask(task.taskUid) const response = await client.index(index.uid).getFaceting() - expect(response).toEqual(newFaceting) + expect(response).toMatchSnapshot() }) test(`${permission} key: Update faceting at null`, async () => { @@ -61,7 +64,7 @@ describe.each([{ permission: 'Master' }, { permission: 'Admin' }])( const response = await client.index(index.uid).getFaceting() - expect(response).toEqual({ maxValuesPerFacet: 100 }) + expect(response).toMatchSnapshot() }) test(`${permission} key: Reset faceting`, async () => { @@ -80,7 +83,7 @@ describe.each([{ permission: 'Master' }, { permission: 'Admin' }])( const response = await client.index(index.uid).getFaceting() - expect(response).toEqual({ maxValuesPerFacet: 100 }) + expect(response).toMatchSnapshot() }) } ) diff --git a/tests/settings.test.ts b/tests/settings.test.ts index 97864fdd4..d4b6cfd69 100644 --- a/tests/settings.test.ts +++ b/tests/settings.test.ts @@ -1,4 +1,4 @@ -import { ErrorStatusCode } from '../src/types' +import { ErrorStatusCode, Settings } from '../src/types' import { clearAllIndexes, config, @@ -16,48 +16,6 @@ const indexAndPK = { primaryKey: 'id', } -const defaultRankingRules = [ - 'words', - 'typo', - 'proximity', - 'attribute', - 'sort', - 'exactness', -] - -const defaultSettings = { - filterableAttributes: [], - sortableAttributes: [], - distinctAttribute: null, - searchableAttributes: ['*'], - displayedAttributes: ['*'], - rankingRules: [ - 'words', - 'typo', - 'proximity', - 'attribute', - 'sort', - 'exactness', - ], - stopWords: [], - synonyms: {}, - typoTolerance: { - enabled: true, - minWordSizeForTypos: { - oneTypo: 5, - twoTypos: 9, - }, - disableOnWords: [], - disableOnAttributes: [], - }, - pagination: { - maxTotalHits: 1000, - }, - faceting: { - maxValuesPerFacet: 100, - }, -} - jest.setTimeout(100 * 1000) afterAll(() => { @@ -88,15 +46,7 @@ describe.each([{ permission: 'Master' }, { permission: 'Admin' }])( const response = await client.index(index.uid).getSettings() - expect(response).toHaveProperty('rankingRules', defaultRankingRules) - expect(response).toHaveProperty('distinctAttribute', null) - expect(response).toHaveProperty('searchableAttributes', ['*']) - expect(response).toHaveProperty('displayedAttributes', ['*']) - expect(response).toHaveProperty('sortableAttributes', []) - expect(response).toHaveProperty('stopWords', []) - expect(response).toHaveProperty('synonyms', {}) - expect(response).toHaveProperty('faceting', { maxValuesPerFacet: 100 }) - expect(response).toHaveProperty('pagination', { maxTotalHits: 1000 }) + expect(response).toMatchSnapshot() }) test(`${permission} key: Get default settings of empty index with primary key`, async () => { @@ -104,20 +54,12 @@ describe.each([{ permission: 'Master' }, { permission: 'Admin' }])( const response = await client.index(indexAndPK.uid).getSettings() - expect(response).toHaveProperty('rankingRules', defaultRankingRules) - expect(response).toHaveProperty('distinctAttribute', null) - expect(response).toHaveProperty('searchableAttributes', ['*']) - expect(response).toHaveProperty('displayedAttributes', ['*']) - expect(response).toHaveProperty('sortableAttributes', []) - expect(response).toHaveProperty('stopWords', []) - expect(response).toHaveProperty('synonyms', {}) - expect(response).toHaveProperty('faceting', { maxValuesPerFacet: 100 }) - expect(response).toHaveProperty('pagination', { maxTotalHits: 1000 }) + expect(response).toMatchSnapshot() }) test(`${permission} key: Update settings`, async () => { const client = await getClient(permission) - const newSettings = { + const newSettings: Settings = { filterableAttributes: ['title'], sortableAttributes: ['title'], distinctAttribute: 'title', @@ -140,6 +82,9 @@ describe.each([{ permission: 'Master' }, { permission: 'Admin' }])( }, faceting: { maxValuesPerFacet: 50, + sortFacetValuesBy: { + '*': 'alpha', + }, }, } // Add the settings @@ -150,7 +95,7 @@ describe.each([{ permission: 'Master' }, { permission: 'Admin' }])( const response = await client.index(index.uid).getSettings() // tests - expect(response).toEqual(newSettings) + expect(response).toMatchSnapshot() }) test(`${permission} key: Update settings with all null values`, async () => { @@ -175,6 +120,7 @@ describe.each([{ permission: 'Master' }, { permission: 'Admin' }])( }, faceting: { maxValuesPerFacet: null, + sortFacetValuesBy: null, }, pagination: { maxTotalHits: null, @@ -188,7 +134,7 @@ describe.each([{ permission: 'Master' }, { permission: 'Admin' }])( const response = await client.index(index.uid).getSettings() // tests - expect(response).toEqual(defaultSettings) + expect(response).toMatchSnapshot() }) test(`${permission} key: Update settings on empty index with primary key`, async () => { @@ -205,17 +151,7 @@ describe.each([{ permission: 'Master' }, { permission: 'Admin' }])( const response = await client.index(indexAndPK.uid).getSettings() - expect(response).toHaveProperty('rankingRules', newSettings.rankingRules) - expect(response).toHaveProperty( - 'distinctAttribute', - newSettings.distinctAttribute - ) - expect(response).toHaveProperty('searchableAttributes', ['*']) - expect(response).toHaveProperty('displayedAttributes', ['*']) - expect(response).toHaveProperty('stopWords', newSettings.stopWords) - expect(response).toHaveProperty('synonyms', {}) - expect(response).toHaveProperty('faceting', { maxValuesPerFacet: 100 }) - expect(response).toHaveProperty('pagination', { maxTotalHits: 1000 }) + expect(response).toMatchSnapshot() }) test(`${permission} key: Reset settings`, async () => { @@ -225,15 +161,7 @@ describe.each([{ permission: 'Master' }, { permission: 'Admin' }])( const response = await client.index(index.uid).getSettings() - expect(response).toHaveProperty('rankingRules', defaultRankingRules) - expect(response).toHaveProperty('distinctAttribute', null) - expect(response).toHaveProperty('searchableAttributes', ['*']) - expect(response).toHaveProperty('displayedAttributes', ['*']) - expect(response).toHaveProperty('sortableAttributes', []) - expect(response).toHaveProperty('stopWords', []) - expect(response).toHaveProperty('synonyms', {}) - expect(response).toHaveProperty('faceting', { maxValuesPerFacet: 100 }) - expect(response).toHaveProperty('pagination', { maxTotalHits: 1000 }) + expect(response).toMatchSnapshot() }) test(`${permission} key: Reset settings of empty index`, async () => { @@ -243,14 +171,7 @@ describe.each([{ permission: 'Master' }, { permission: 'Admin' }])( const response = await client.index(indexAndPK.uid).getSettings() - expect(response).toHaveProperty('rankingRules', defaultRankingRules) - expect(response).toHaveProperty('distinctAttribute', null) - expect(response).toHaveProperty('searchableAttributes', ['*']) - expect(response).toHaveProperty('displayedAttributes', ['*']) - expect(response).toHaveProperty('stopWords', []) - expect(response).toHaveProperty('synonyms', {}) - expect(response).toHaveProperty('faceting', { maxValuesPerFacet: 100 }) - expect(response).toHaveProperty('pagination', { maxTotalHits: 1000 }) + expect(response).toMatchSnapshot() }) test(`${permission} key: Update searchableAttributes settings on empty index`, async () => { @@ -263,20 +184,7 @@ describe.each([{ permission: 'Master' }, { permission: 'Admin' }])( const response = await client.index(index.uid).getSettings() - expect(response).toHaveProperty('rankingRules', defaultRankingRules) - expect(response).toHaveProperty( - 'distinctAttribute', - defaultSettings.distinctAttribute - ) - expect(response).toHaveProperty( - 'searchableAttributes', - newSettings.searchableAttributes - ) - expect(response).toHaveProperty('displayedAttributes', expect.any(Array)) - expect(response).toHaveProperty('stopWords', defaultSettings.stopWords) - expect(response).toHaveProperty('synonyms', {}) - expect(response).toHaveProperty('faceting', { maxValuesPerFacet: 100 }) - expect(response).toHaveProperty('pagination', { maxTotalHits: 1000 }) + expect(response).toMatchSnapshot() }) test(`${permission} key: Update searchableAttributes settings on empty index with a primary key`, async () => { @@ -294,25 +202,7 @@ describe.each([{ permission: 'Master' }, { permission: 'Admin' }])( // Fetch settings const response = await client.index(indexAndPK.uid).getSettings() - // Compare searchableAttributes - expect(response).toHaveProperty( - 'searchableAttributes', - newSettings.searchableAttributes - ) - expect(response).toHaveProperty('rankingRules', defaultRankingRules) - expect(response).toHaveProperty( - 'distinctAttribute', - defaultSettings.distinctAttribute - ) - expect(response).toHaveProperty( - 'searchableAttributes', - newSettings.searchableAttributes - ) - expect(response).toHaveProperty('displayedAttributes', expect.any(Array)) - expect(response).toHaveProperty('stopWords', defaultSettings.stopWords) - expect(response).toHaveProperty('synonyms', {}) - expect(response).toHaveProperty('faceting', { maxValuesPerFacet: 100 }) - expect(response).toHaveProperty('pagination', { maxTotalHits: 1000 }) + expect(response).toMatchSnapshot() }) } ) From e9e1f000a58ae626f99ae7a3ab09c3b00cdf54ae Mon Sep 17 00:00:00 2001 From: cvermand <33010418+bidoubiwa@users.noreply.github.com> Date: Mon, 10 Jul 2023 16:58:43 +0200 Subject: [PATCH 3/9] Experimental vector search for MS v1.3.0 (#1535) * Experimental vector search for MS v1.3.0 * Add vector search error codes * Use permission as key when enabling the experimental prototype --- src/indexes.ts | 1 + src/types/types.ts | 13 +++++++++++-- tests/get_search.test.ts | 21 +++++++++++++++++++++ tests/search.test.ts | 26 ++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/indexes.ts b/src/indexes.ts index a5145253e..2ba54d7b3 100644 --- a/src/indexes.ts +++ b/src/indexes.ts @@ -137,6 +137,7 @@ class Index = Record> { attributesToRetrieve: options?.attributesToRetrieve?.join(','), attributesToCrop: options?.attributesToCrop?.join(','), attributesToHighlight: options?.attributesToHighlight?.join(','), + vector: options?.vector?.join(','), } return await this.httpRequest.get>( diff --git a/src/types/types.ts b/src/types/types.ts index afa7e04a8..6e47ebe5b 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -90,6 +90,7 @@ export type SearchParams = Query & matchingStrategy?: MatchingStrategies hitsPerPage?: number page?: number + vector?: number[] | null } // Search parameters for searches made with the GET method @@ -105,6 +106,7 @@ export type SearchRequestGET = Pagination & attributesToHighlight?: string attributesToCrop?: string showMatchesPosition?: boolean + vector?: string | null } export type MultiSearchQuery = SearchParams & { indexUid: string } @@ -142,6 +144,7 @@ export type SearchResponse< facetDistribution?: FacetDistribution query: string facetStats?: FacetStats + vector: number[] } & (undefined extends S ? Partial : true extends IsFinitePagination> @@ -560,12 +563,15 @@ export const enum ErrorStatusCode { /** @see https://www.meilisearch.com/docs/reference/errors/error_codes#invalid_document_offset */ INVALID_DOCUMENT_OFFSET = 'invalid_document_offset', - /** @see https://www.meilisearch.com/docs/reference/errors/error_codes#invalid_document_offset */ + /** @see https://www.meilisearch.com/docs/reference/errors/error_codes#invalid_document_filter */ INVALID_DOCUMENT_FILTER = 'invalid_document_filter', - /** @see https://www.meilisearch.com/docs/reference/errors/error_codes#invalid_document_offset */ + /** @see https://www.meilisearch.com/docs/reference/errors/error_codes#missing_document_filter */ MISSING_DOCUMENT_FILTER = 'missing_document_filter', + /** @see https://www.meilisearch.com/docs/reference/errors/error_codes#invalid_document_vectors_field */ + INVALID_DOCUMENT_VECTORS_FIELD = 'invalid_document_vectors_field', + /** @see https://www.meilisearch.com/docs/reference/errors/error_codes#payload_too_large */ PAYLOAD_TOO_LARGE = 'payload_too_large', @@ -641,6 +647,9 @@ export const enum ErrorStatusCode { /** @see https://www.meilisearch.com/docs/reference/errors/error_codes#invalid_search_matching_strategy */ INVALID_SEARCH_MATCHING_STRATEGY = 'invalid_search_matching_strategy', + /** @see https://www.meilisearch.com/docs/reference/errors/error_codes#invalid_search_vector */ + INVALID_SEARCH_VECTOR = 'invalid_search_vector', + /** @see https://www.meilisearch.com/docs/reference/errors/error_codes#bad_request */ BAD_REQUEST = 'bad_request', diff --git a/tests/get_search.test.ts b/tests/get_search.test.ts index 33c19a79e..55c3c8d7e 100644 --- a/tests/get_search.test.ts +++ b/tests/get_search.test.ts @@ -6,6 +6,8 @@ import { BAD_HOST, MeiliSearch, getClient, + HOST, + getKey, } from './utils/meilisearch-test-utils' const index = { @@ -423,6 +425,25 @@ describe.each([ 'The filter query parameter should be in string format when using searchGet' ) }) + test(`${permission} key: search with vectors`, async () => { + const client = await getClient(permission) + const key = await getKey(permission) + + await fetch(`${HOST}/experimental-features`, { + body: JSON.stringify({ vectorStore: true }), + headers: { + Authorization: `Bearer ${key}`, + 'Content-Type': 'application/json', + }, + method: 'PATCH', + }) + + const response = await client + .index(emptyIndex.uid) + .searchGet('', { vector: [1] }) + + expect(response.vector).toEqual([1]) + }) test(`${permission} key: Try to search on deleted index and fail`, async () => { const client = await getClient(permission) diff --git a/tests/search.test.ts b/tests/search.test.ts index 7edd89360..164f5baf3 100644 --- a/tests/search.test.ts +++ b/tests/search.test.ts @@ -8,8 +8,14 @@ import { MeiliSearch, getClient, datasetWithNests, + HOST, + getKey, } from './utils/meilisearch-test-utils' +if (typeof fetch === 'undefined') { + require('cross-fetch/polyfill') +} + const index = { uid: 'movies_test', } @@ -767,6 +773,26 @@ describe.each([ expect(response.hits.length).toEqual(0) }) + test(`${permission} key: search with vectors`, async () => { + const client = await getClient(permission) + const key = await getKey(permission) + + await fetch(`${HOST}/experimental-features`, { + body: JSON.stringify({ vectorStore: true }), + headers: { + Authorization: `Bearer ${key}`, + 'Content-Type': 'application/json', + }, + method: 'PATCH', + }) + + const response = await client + .index(emptyIndex.uid) + .search('', { vector: [1] }) + + expect(response.vector).toEqual([1]) + }) + test(`${permission} key: Try to search on deleted index and fail`, async () => { const client = await getClient(permission) const masterClient = await getClient('Master') From 7c395ed683a883d5f2c5ceff4b93d838ca1a9d57 Mon Sep 17 00:00:00 2001 From: cvermand <33010418+bidoubiwa@users.noreply.github.com> Date: Tue, 11 Jul 2023 11:31:02 +0200 Subject: [PATCH 4/9] Add attributesToSearchOn in search parameters (#1538) --- src/indexes.ts | 1 + src/types/types.ts | 5 ++ tests/__snapshots__/get_search.test.ts.snap | 73 +++++++++++++++++++ tests/__snapshots__/search.test.ts.snap | 79 +++++++++++++++++++++ tests/get_search.test.ts | 20 ++++++ tests/search.test.ts | 20 ++++++ 6 files changed, 198 insertions(+) create mode 100644 tests/__snapshots__/get_search.test.ts.snap create mode 100644 tests/__snapshots__/search.test.ts.snap diff --git a/src/indexes.ts b/src/indexes.ts index 2ba54d7b3..ee55e6994 100644 --- a/src/indexes.ts +++ b/src/indexes.ts @@ -138,6 +138,7 @@ class Index = Record> { attributesToCrop: options?.attributesToCrop?.join(','), attributesToHighlight: options?.attributesToHighlight?.join(','), vector: options?.vector?.join(','), + attributesToSearchOn: options?.attributesToSearchOn?.join(','), } return await this.httpRequest.get>( diff --git a/src/types/types.ts b/src/types/types.ts index 6e47ebe5b..c996994de 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -91,6 +91,7 @@ export type SearchParams = Query & hitsPerPage?: number page?: number vector?: number[] | null + attributesToSearchOn?: string[] | null } // Search parameters for searches made with the GET method @@ -107,6 +108,7 @@ export type SearchRequestGET = Pagination & attributesToCrop?: string showMatchesPosition?: boolean vector?: string | null + attributesToSearchOn?: string | null } export type MultiSearchQuery = SearchParams & { indexUid: string } @@ -650,6 +652,9 @@ export const enum ErrorStatusCode { /** @see https://www.meilisearch.com/docs/reference/errors/error_codes#invalid_search_vector */ INVALID_SEARCH_VECTOR = 'invalid_search_vector', + /** @see https://www.meilisearch.com/docs/reference/errors/error_codes#invalid_search_attributes_to_search_on */ + INVALID_SEARCH_ATTRIBUTES_TO_SEARCH_ON = 'invalid_search_attributes_to_search_on', + /** @see https://www.meilisearch.com/docs/reference/errors/error_codes#bad_request */ BAD_REQUEST = 'bad_request', diff --git a/tests/__snapshots__/get_search.test.ts.snap b/tests/__snapshots__/get_search.test.ts.snap new file mode 100644 index 000000000..1017d2302 --- /dev/null +++ b/tests/__snapshots__/get_search.test.ts.snap @@ -0,0 +1,73 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test on GET search Admin key: search on attributesToSearchOn set to null 1`] = ` +Object { + "estimatedTotalHits": 2, + "hits": Array [ + Object { + "comment": "A french book about a prince that walks on little cute planets", + "genre": "adventure", + "id": 456, + "title": "Le Petit Prince", + }, + Object { + "comment": "The best book", + "genre": "fantasy", + "id": 4, + "title": "Harry Potter and the Half-Blood Prince", + }, + ], + "limit": 20, + "offset": 0, + "processingTimeMs": 0, + "query": "prince", +} +`; + +exports[`Test on GET search Master key: search on attributesToSearchOn set to null 1`] = ` +Object { + "estimatedTotalHits": 2, + "hits": Array [ + Object { + "comment": "A french book about a prince that walks on little cute planets", + "genre": "adventure", + "id": 456, + "title": "Le Petit Prince", + }, + Object { + "comment": "The best book", + "genre": "fantasy", + "id": 4, + "title": "Harry Potter and the Half-Blood Prince", + }, + ], + "limit": 20, + "offset": 0, + "processingTimeMs": 0, + "query": "prince", +} +`; + +exports[`Test on GET search Search key: search on attributesToSearchOn set to null 1`] = ` +Object { + "estimatedTotalHits": 2, + "hits": Array [ + Object { + "comment": "A french book about a prince that walks on little cute planets", + "genre": "adventure", + "id": 456, + "title": "Le Petit Prince", + }, + Object { + "comment": "The best book", + "genre": "fantasy", + "id": 4, + "title": "Harry Potter and the Half-Blood Prince", + }, + ], + "limit": 20, + "offset": 0, + "processingTimeMs": 0, + "query": "prince", +} +`; diff --git a/tests/__snapshots__/search.test.ts.snap b/tests/__snapshots__/search.test.ts.snap new file mode 100644 index 000000000..340d04d11 --- /dev/null +++ b/tests/__snapshots__/search.test.ts.snap @@ -0,0 +1,79 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test on POST search Admin key: search on attributesToSearchOn set to null 1`] = ` +Object { + "estimatedTotalHits": 2, + "hits": Array [ + Object { + "comment": "A french book about a prince that walks on little cute planets", + "genre": "adventure", + "id": 456, + "isNull": null, + "isTrue": true, + "title": "Le Petit Prince", + }, + Object { + "comment": "The best book", + "genre": "fantasy", + "id": 4, + "title": "Harry Potter and the Half-Blood Prince", + }, + ], + "limit": 20, + "offset": 0, + "processingTimeMs": 0, + "query": "prince", +} +`; + +exports[`Test on POST search Master key: search on attributesToSearchOn set to null 1`] = ` +Object { + "estimatedTotalHits": 2, + "hits": Array [ + Object { + "comment": "A french book about a prince that walks on little cute planets", + "genre": "adventure", + "id": 456, + "isNull": null, + "isTrue": true, + "title": "Le Petit Prince", + }, + Object { + "comment": "The best book", + "genre": "fantasy", + "id": 4, + "title": "Harry Potter and the Half-Blood Prince", + }, + ], + "limit": 20, + "offset": 0, + "processingTimeMs": 0, + "query": "prince", +} +`; + +exports[`Test on POST search Search key: search on attributesToSearchOn set to null 1`] = ` +Object { + "estimatedTotalHits": 2, + "hits": Array [ + Object { + "comment": "A french book about a prince that walks on little cute planets", + "genre": "adventure", + "id": 456, + "isNull": null, + "isTrue": true, + "title": "Le Petit Prince", + }, + Object { + "comment": "The best book", + "genre": "fantasy", + "id": 4, + "title": "Harry Potter and the Half-Blood Prince", + }, + ], + "limit": 20, + "offset": 0, + "processingTimeMs": 0, + "query": "prince", +} +`; diff --git a/tests/get_search.test.ts b/tests/get_search.test.ts index 55c3c8d7e..b6585b192 100644 --- a/tests/get_search.test.ts +++ b/tests/get_search.test.ts @@ -141,6 +141,26 @@ describe.each([ ) }) + test(`${permission} key: search on attributesToSearchOn`, async () => { + const client = await getClient(permission) + + const response = await client.index(index.uid).searchGet('prince', { + attributesToSearchOn: ['id'], + }) + + expect(response.hits.length).toEqual(0) + }) + + test(`${permission} key: search on attributesToSearchOn set to null`, async () => { + const client = await getClient(permission) + + const response = await client.index(index.uid).searchGet('prince', { + attributesToSearchOn: null, + }) + + expect(response).toMatchSnapshot() + }) + test(`${permission} key: search with options`, async () => { const client = await getClient(permission) const response = await client diff --git a/tests/search.test.ts b/tests/search.test.ts index 164f5baf3..3fc770c7c 100644 --- a/tests/search.test.ts +++ b/tests/search.test.ts @@ -272,6 +272,26 @@ describe.each([ ) }) + test(`${permission} key: search on attributesToSearchOn`, async () => { + const client = await getClient(permission) + + const response = await client.index(index.uid).search('prince', { + attributesToSearchOn: ['id'], + }) + + expect(response.hits.length).toEqual(0) + }) + + test(`${permission} key: search on attributesToSearchOn set to null`, async () => { + const client = await getClient(permission) + + const response = await client.index(index.uid).search('prince', { + attributesToSearchOn: null, + }) + + expect(response).toMatchSnapshot() + }) + test(`${permission} key: search with array options`, async () => { const client = await getClient(permission) From 13846f03a2b9160be6e022956e81f869ff8d815a Mon Sep 17 00:00:00 2001 From: cvermand <33010418+bidoubiwa@users.noreply.github.com> Date: Tue, 11 Jul 2023 11:35:08 +0200 Subject: [PATCH 5/9] Add total field in getTasks response (#1539) --- src/types/types.ts | 1 + tests/task.test.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/types/types.ts b/src/types/types.ts index c996994de..9ba927481 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -408,6 +408,7 @@ type CursorResults = { limit: number from: number next: number + total: number } export type TasksResults = CursorResults diff --git a/tests/task.test.ts b/tests/task.test.ts index a2eb4057c..050003b2c 100644 --- a/tests/task.test.ts +++ b/tests/task.test.ts @@ -95,6 +95,7 @@ describe.each([{ permission: 'Master' }, { permission: 'Admin' }])( const tasks = await client.getTasks() expect(tasks.results).toBeInstanceOf(Array) + expect(tasks.total).toBeDefined() expect(tasks.results[0].uid).toEqual(enqueuedTask.taskUid) }) From b61a8e56b2bf570a1854c17d4491178e33ee2d8b Mon Sep 17 00:00:00 2001 From: cvermand <33010418+bidoubiwa@users.noreply.github.com> Date: Wed, 12 Jul 2023 16:54:30 +0200 Subject: [PATCH 6/9] Add the searchForFacetValues method for Meilisearch v1.3.0 (#1513) * Add the searchForFacetValue method * Update version for the next release (v0.33.0-prototype-search-for-facet-values.0) (#1514) * Update package.json * Update src/package-version.ts * Change name of searchForFacetValue to searchForFacetValues * Add error codes * Remove only label on test * Fix the typing of the search parameters * Remove unecessary INVALID_SEARCH_FACET error * Fix tests * Update returned fields from hits to facetHits and query to facetQuery * Update version for the next release (vv0.33.0-prototype-search-for-facet-values.1) (#1515) * Update package.json * Update src/package-version.ts --------- Co-authored-by: cvermand <33010418+bidoubiwa@users.noreply.github.com> * Apply suggestions from code review * Update README.md Co-authored-by: Bruno Casali --------- Co-authored-by: meili-bot <74670311+meili-bot@users.noreply.github.com> Co-authored-by: Bruno Casali --- README.md | 8 + src/indexes.ts | 23 ++ src/types/types.ts | 27 +++ tests/__snapshots__/facet_search.test.ts.snap | 205 ++++++++++++++++++ tests/facet_search.test.ts | 106 +++++++++ 5 files changed, 369 insertions(+) create mode 100644 tests/__snapshots__/facet_search.test.ts.snap create mode 100644 tests/facet_search.test.ts diff --git a/README.md b/README.md index 6c9ef1cc5..198b2fac5 100644 --- a/README.md +++ b/README.md @@ -418,6 +418,14 @@ client.multiSearch(queries?: MultiSearchParams, config?: Partial): Prom `multiSearch` uses the `POST` method when performing its request to Meilisearch. +### Search For Facet Values + +#### [Search for facet values](#) + +```ts +client.index('myIndex').searchForFacetValues(params: SearchForFacetValuesParams, config?: Partial): Promise +``` + ### Documents #### [Add or replace multiple documents](https://www.meilisearch.com/docs/reference/api/documents#add-or-replace-documents) diff --git a/src/indexes.ts b/src/indexes.ts index ee55e6994..ea732da5b 100644 --- a/src/indexes.ts +++ b/src/indexes.ts @@ -45,6 +45,8 @@ import { ContentType, DocumentsIds, DocumentsDeletionQuery, + SearchForFacetValuesParams, + SearchForFacetValuesResponse, } from './types' import { removeUndefinedFromObject } from './utils' import { HttpRequests } from './http-requests' @@ -148,6 +150,27 @@ class Index = Record> { ) } + /** + * Search for facet values + * + * @param params - Parameters used to search on the facets + * @param config - Additional request configuration options + * @returns Promise containing the search response + */ + async searchForFacetValues( + params: SearchForFacetValuesParams, + config?: Partial + ): Promise { + const url = `indexes/${this.uid}/facet-search` + + return await this.httpRequest.post( + url, + removeUndefinedFromObject(params), + undefined, + config + ) + } + /// /// INDEX /// diff --git a/src/types/types.ts b/src/types/types.ts index 9ba927481..41aaa8922 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -78,6 +78,22 @@ export type Crop = { cropMarker?: string } +// `facetName` becomes mandatory when using `searchForFacetValues` +export type SearchForFacetValuesParams = Omit & { + facetName: string +} + +export type FacetHit = { + value: string + count: number +} + +export type SearchForFacetValuesResponse = { + facetHits: FacetHit[] + facetQuery: string | null + processingTimeMs: number +} + export type SearchParams = Query & Pagination & Highlight & @@ -90,6 +106,8 @@ export type SearchParams = Query & matchingStrategy?: MatchingStrategies hitsPerPage?: number page?: number + facetName?: string + facetQuery?: string vector?: number[] | null attributesToSearchOn?: string[] | null } @@ -835,6 +853,15 @@ export const enum ErrorStatusCode { /** @see https://www.meilisearch.com/docs/reference/errors/error_codes#invalid_api_key_offset */ INVALID_API_KEY_OFFSET = 'invalid_api_key_offset', + + /** @see https://www.meilisearch.com/docs/reference/errors/error_codes#invalid_facet_search_facet_name */ + INVALID_FACET_SEARCH_FACET_NAME = 'invalid_facet_search_facet_name', + + /** @see https://www.meilisearch.com/docs/reference/errors/error_codes#missing_facet_search_facet_name */ + MISSING_FACET_SEARCH_FACET_NAME = 'missing_facet_search_facet_name', + + /** @see https://www.meilisearch.com/docs/reference/errors/error_codes#invalid_facet_search_facet_query */ + INVALID_FACET_SEARCH_FACET_QUERY = 'invalid_facet_search_facet_query', } export type TokenIndexRules = { diff --git a/tests/__snapshots__/facet_search.test.ts.snap b/tests/__snapshots__/facet_search.test.ts.snap new file mode 100644 index 000000000..17c3d1bf0 --- /dev/null +++ b/tests/__snapshots__/facet_search.test.ts.snap @@ -0,0 +1,205 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test on POST search Admin key: basic facet value search 1`] = ` +Object { + "facetHits": Array [ + Object { + "count": 1, + "value": "action", + }, + Object { + "count": 2, + "value": "adventure", + }, + ], + "facetQuery": "a", + "processingTimeMs": 0, +} +`; + +exports[`Test on POST search Admin key: facet value search with filter 1`] = ` +Object { + "facetHits": Array [ + Object { + "count": 1, + "value": "action", + }, + ], + "facetQuery": "a", + "processingTimeMs": 0, +} +`; + +exports[`Test on POST search Admin key: facet value search with no facet query 1`] = ` +Object { + "facetHits": Array [ + Object { + "count": 1, + "value": "action", + }, + Object { + "count": 2, + "value": "adventure", + }, + Object { + "count": 1, + "value": "comedy", + }, + Object { + "count": 2, + "value": "romance", + }, + ], + "facetQuery": null, + "processingTimeMs": 0, +} +`; + +exports[`Test on POST search Admin key: facet value search with search query 1`] = ` +Object { + "facetHits": Array [ + Object { + "count": 1, + "value": "adventure", + }, + ], + "facetQuery": "a", + "processingTimeMs": 0, +} +`; + +exports[`Test on POST search Master key: basic facet value search 1`] = ` +Object { + "facetHits": Array [ + Object { + "count": 1, + "value": "action", + }, + Object { + "count": 2, + "value": "adventure", + }, + ], + "facetQuery": "a", + "processingTimeMs": 0, +} +`; + +exports[`Test on POST search Master key: facet value search with filter 1`] = ` +Object { + "facetHits": Array [ + Object { + "count": 1, + "value": "action", + }, + ], + "facetQuery": "a", + "processingTimeMs": 0, +} +`; + +exports[`Test on POST search Master key: facet value search with no facet query 1`] = ` +Object { + "facetHits": Array [ + Object { + "count": 1, + "value": "action", + }, + Object { + "count": 2, + "value": "adventure", + }, + Object { + "count": 1, + "value": "comedy", + }, + Object { + "count": 2, + "value": "romance", + }, + ], + "facetQuery": null, + "processingTimeMs": 0, +} +`; + +exports[`Test on POST search Master key: facet value search with search query 1`] = ` +Object { + "facetHits": Array [ + Object { + "count": 1, + "value": "adventure", + }, + ], + "facetQuery": "a", + "processingTimeMs": 0, +} +`; + +exports[`Test on POST search Search key: basic facet value search 1`] = ` +Object { + "facetHits": Array [ + Object { + "count": 1, + "value": "action", + }, + Object { + "count": 2, + "value": "adventure", + }, + ], + "facetQuery": "a", + "processingTimeMs": 0, +} +`; + +exports[`Test on POST search Search key: facet value search with filter 1`] = ` +Object { + "facetHits": Array [ + Object { + "count": 1, + "value": "action", + }, + ], + "facetQuery": "a", + "processingTimeMs": 0, +} +`; + +exports[`Test on POST search Search key: facet value search with no facet query 1`] = ` +Object { + "facetHits": Array [ + Object { + "count": 1, + "value": "action", + }, + Object { + "count": 2, + "value": "adventure", + }, + Object { + "count": 1, + "value": "comedy", + }, + Object { + "count": 2, + "value": "romance", + }, + ], + "facetQuery": null, + "processingTimeMs": 0, +} +`; + +exports[`Test on POST search Search key: facet value search with search query 1`] = ` +Object { + "facetHits": Array [ + Object { + "count": 1, + "value": "adventure", + }, + ], + "facetQuery": "a", + "processingTimeMs": 0, +} +`; diff --git a/tests/facet_search.test.ts b/tests/facet_search.test.ts new file mode 100644 index 000000000..e20866b1f --- /dev/null +++ b/tests/facet_search.test.ts @@ -0,0 +1,106 @@ +import { + clearAllIndexes, + config, + getClient, +} from './utils/meilisearch-test-utils' + +const index = { + uid: 'movies_test', +} + +const dataset = [ + { + id: 123, + title: 'Pride and Prejudice', + genres: ['romance', 'action'], + }, + { + id: 456, + title: 'Le Petit Prince', + genres: ['adventure', 'comedy'], + }, + { + id: 2, + title: 'Le Rouge et le Noir', + genres: 'romance', + }, + { + id: 1, + title: 'Alice In Wonderland', + genres: ['adventure'], + }, +] + +describe.each([ + { permission: 'Master' }, + { permission: 'Admin' }, + { permission: 'Search' }, +])('Test on POST search', ({ permission }) => { + beforeAll(async () => { + await clearAllIndexes(config) + const client = await getClient('Master') + const newFilterableAttributes = ['genres', 'title'] + await client.createIndex(index.uid) + await client.index(index.uid).updateSettings({ + filterableAttributes: newFilterableAttributes, + }) + const { taskUid } = await client.index(index.uid).addDocuments(dataset) + await client.waitForTask(taskUid) + }) + + test(`${permission} key: basic facet value search`, async () => { + const client = await getClient(permission) + + const params = { + facetQuery: 'a', + facetName: 'genres', + } + const response = await client.index(index.uid).searchForFacetValues(params) + + expect(response).toMatchSnapshot() + }) + + test(`${permission} key: facet value search with no facet query`, async () => { + const client = await getClient(permission) + + const params = { + facetName: 'genres', + } + const response = await client.index(index.uid).searchForFacetValues(params) + + expect(response).toMatchSnapshot() + }) + + test(`${permission} key: facet value search with filter`, async () => { + const client = await getClient(permission) + + const params = { + facetName: 'genres', + facetQuery: 'a', + filter: ['genres = action'], + } + + const response = await client.index(index.uid).searchForFacetValues(params) + + expect(response).toMatchSnapshot() + }) + + test(`${permission} key: facet value search with search query`, async () => { + const client = await getClient(permission) + + const params = { + facetName: 'genres', + facetQuery: 'a', + q: 'Alice', + } + const response = await client.index(index.uid).searchForFacetValues(params) + + expect(response).toMatchSnapshot() + }) +}) + +jest.setTimeout(100 * 1000) + +afterAll(() => { + return clearAllIndexes(config) +}) From 560bfdac0b95210c2eb33a702b066ec7d359caf2 Mon Sep 17 00:00:00 2001 From: Charlotte Vermandel Date: Wed, 12 Jul 2023 17:32:29 +0200 Subject: [PATCH 7/9] Temporarely skip tests due to bug in vector get search --- tests/get_search.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/get_search.test.ts b/tests/get_search.test.ts index b6585b192..5a2beac45 100644 --- a/tests/get_search.test.ts +++ b/tests/get_search.test.ts @@ -445,7 +445,7 @@ describe.each([ 'The filter query parameter should be in string format when using searchGet' ) }) - test(`${permission} key: search with vectors`, async () => { + test.skip(`${permission} key: search with vectors`, async () => { const client = await getClient(permission) const key = await getKey(permission) From 0cf87045a7cacfd950680fe7a34ec1dbe5113a5d Mon Sep 17 00:00:00 2001 From: cvermand <33010418+bidoubiwa@users.noreply.github.com> Date: Wed, 12 Jul 2023 17:39:14 +0200 Subject: [PATCH 8/9] Add rankingScore and rankingScoreDetails types (#1537) * Experimental vector search for MS v1.3.0 * Add vector search error codes * Use permission as key when enabling the experimental prototype * Add rankingScore and rankingScoreDetails types * Use RankingScoreDetails type to type _rankingScoreDetails --- src/types/types.ts | 35 ++++++++++++++++++++++++++++++++++ tests/search.test.ts | 45 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/src/types/types.ts b/src/types/types.ts index 41aaa8922..5d11fe2f1 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -109,6 +109,8 @@ export type SearchParams = Query & facetName?: string facetQuery?: string vector?: number[] | null + showRankingScore?: boolean + showRankingScoreDetails?: boolean attributesToSearchOn?: string[] | null } @@ -148,6 +150,39 @@ export type MatchesPosition = Partial< export type Hit> = T & { _formatted?: Partial _matchesPosition?: MatchesPosition + _rankingScore?: number + _rankingScoreDetails?: RakingScoreDetails +} + +export type RakingScoreDetails = { + words?: { + order: number + matchingWords: number + maxMatchingWords: number + score: number + } + typo?: { + order: number + typoCount: number + maxTypoCount: number + score: number + } + proximity?: { + order: number + score: number + } + attribute?: { + order: number + attributes_ranking_order: number + attributes_query_word_order: number + score: number + } + exactness?: { + order: number + matchType: string + score: number + } + [key: string]: Record | undefined } export type Hits> = Array> diff --git a/tests/search.test.ts b/tests/search.test.ts index 3fc770c7c..222f36323 100644 --- a/tests/search.test.ts +++ b/tests/search.test.ts @@ -257,6 +257,51 @@ describe.each([ expect(hit.id).toEqual(1) }) + test(`${permission} key: search with _showRankingScore enabled`, async () => { + const client = await getClient(permission) + + const response = await client.index(index.uid).search('prince', { + showRankingScore: true, + }) + + const hit = response.hits[0] + + expect(response).toHaveProperty('hits', expect.any(Array)) + expect(response).toHaveProperty('query', 'prince') + expect(hit).toHaveProperty('_rankingScore') + }) + + test(`${permission} key: search with showRankingScoreDetails enabled`, async () => { + const client = await getClient(permission) + const key = await getKey(permission) + + await fetch(`${HOST}/experimental-features`, { + body: JSON.stringify({ scoreDetails: true }), + headers: { + Authorization: `Bearer ${key}`, + 'Content-Type': 'application/json', + }, + method: 'PATCH', + }) + + const response = await client.index(index.uid).search('prince', { + showRankingScoreDetails: true, + }) + + const hit = response.hits[0] + + expect(response).toHaveProperty('hits', expect.any(Array)) + expect(response).toHaveProperty('query', 'prince') + expect(hit).toHaveProperty('_rankingScoreDetails') + expect(Object.keys(hit._rankingScoreDetails || {})).toEqual([ + 'words', + 'typo', + 'proximity', + 'attribute', + 'exactness', + ]) + }) + test(`${permission} key: search with array options`, async () => { const client = await getClient(permission) From e9f6865e620852775f7aff9cd62806ee128c92b7 Mon Sep 17 00:00:00 2001 From: Charlotte Vermandel Date: Tue, 18 Jul 2023 11:11:22 +0200 Subject: [PATCH 9/9] Set vector field in search response to optionnal --- src/types/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/types.ts b/src/types/types.ts index 5d11fe2f1..99af63fa4 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -196,10 +196,10 @@ export type SearchResponse< > = { hits: Hits processingTimeMs: number - facetDistribution?: FacetDistribution query: string + facetDistribution?: FacetDistribution facetStats?: FacetStats - vector: number[] + vector?: number[] } & (undefined extends S ? Partial : true extends IsFinitePagination>