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) +})