From e1d336c713d9945f3623d741ff7cdced3625151b Mon Sep 17 00:00:00 2001 From: Charlotte Vermandel Date: Mon, 10 Jul 2023 11:31:13 +0200 Subject: [PATCH 1/5] Experimental vector search for MS v1.3.0 --- src/indexes.ts | 1 + src/types/types.ts | 3 +++ tests/get_search.test.ts | 19 +++++++++++++++++++ tests/search.test.ts | 24 ++++++++++++++++++++++++ 4 files changed, 47 insertions(+) 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 15842c68b..e84fed595 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> diff --git a/tests/get_search.test.ts b/tests/get_search.test.ts index 33c19a79e..d8055ec24 100644 --- a/tests/get_search.test.ts +++ b/tests/get_search.test.ts @@ -6,6 +6,7 @@ import { BAD_HOST, MeiliSearch, getClient, + HOST, } from './utils/meilisearch-test-utils' const index = { @@ -423,6 +424,24 @@ describe.each([ 'The filter query parameter should be in string format when using searchGet' ) }) + test.only(`${permission} key: search with vectors`, async () => { + const client = await getClient(permission) + + await fetch(`${HOST}/experimental-features`, { + body: JSON.stringify({ vectorStore: true }), + headers: { + Authorization: 'Bearer masterKey', + '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..0939673a9 100644 --- a/tests/search.test.ts +++ b/tests/search.test.ts @@ -8,8 +8,13 @@ import { MeiliSearch, getClient, datasetWithNests, + HOST, } from './utils/meilisearch-test-utils' +if (typeof fetch === 'undefined') { + require('cross-fetch/polyfill') +} + const index = { uid: 'movies_test', } @@ -767,6 +772,25 @@ describe.each([ expect(response.hits.length).toEqual(0) }) + test(`${permission} key: search with vectors`, async () => { + const client = await getClient(permission) + + await fetch(`${HOST}/experimental-features`, { + body: JSON.stringify({ vectorStore: true }), + headers: { + Authorization: 'Bearer masterKey', + '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 e1bc4dbaefdb4a17fa401b52a73433cb1350a80c Mon Sep 17 00:00:00 2001 From: Charlotte Vermandel Date: Mon, 10 Jul 2023 13:33:04 +0200 Subject: [PATCH 2/5] Add vector search error codes --- src/types/types.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/types/types.ts b/src/types/types.ts index e84fed595..f3a982a99 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -559,12 +559,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', @@ -640,6 +643,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', From f7ee2490e592abd0d53bdf31c6d08c611a620077 Mon Sep 17 00:00:00 2001 From: Charlotte Vermandel Date: Mon, 10 Jul 2023 15:55:23 +0200 Subject: [PATCH 3/5] Use permission as key when enabling the experimental prototype --- tests/get_search.test.ts | 6 ++++-- tests/search.test.ts | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/get_search.test.ts b/tests/get_search.test.ts index d8055ec24..55c3c8d7e 100644 --- a/tests/get_search.test.ts +++ b/tests/get_search.test.ts @@ -7,6 +7,7 @@ import { MeiliSearch, getClient, HOST, + getKey, } from './utils/meilisearch-test-utils' const index = { @@ -424,13 +425,14 @@ describe.each([ 'The filter query parameter should be in string format when using searchGet' ) }) - test.only(`${permission} key: search with vectors`, async () => { + 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 masterKey', + Authorization: `Bearer ${key}`, 'Content-Type': 'application/json', }, method: 'PATCH', diff --git a/tests/search.test.ts b/tests/search.test.ts index 0939673a9..164f5baf3 100644 --- a/tests/search.test.ts +++ b/tests/search.test.ts @@ -9,6 +9,7 @@ import { getClient, datasetWithNests, HOST, + getKey, } from './utils/meilisearch-test-utils' if (typeof fetch === 'undefined') { @@ -774,11 +775,12 @@ describe.each([ 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 masterKey', + Authorization: `Bearer ${key}`, 'Content-Type': 'application/json', }, method: 'PATCH', From a2ca298307f302456b13b9e7de8c0ef98b903bd4 Mon Sep 17 00:00:00 2001 From: Charlotte Vermandel Date: Mon, 10 Jul 2023 16:28:04 +0200 Subject: [PATCH 4/5] Add rankingScore and rankingScoreDetails types --- 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 f3a982a99..f06cfd077 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -91,6 +91,8 @@ export type SearchParams = Query & hitsPerPage?: number page?: number vector?: number[] | null + showRankingScore?: boolean + showRankingScoreDetails?: boolean } // Search parameters for searches made with the GET method @@ -128,6 +130,39 @@ export type MatchesPosition = Partial< export type Hit> = T & { _formatted?: Partial _matchesPosition?: MatchesPosition + _rankingScore?: number + _rankingScoreDetails?: Record +} + +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 164f5baf3..48576c9df 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 52e0e1f0ad8a8c098599bf906b8f32dbde249626 Mon Sep 17 00:00:00 2001 From: Charlotte Vermandel Date: Tue, 11 Jul 2023 11:58:17 +0200 Subject: [PATCH 5/5] Use RankingScoreDetails type to type _rankingScoreDetails --- src/types/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/types.ts b/src/types/types.ts index cd858fbda..54b9eb6fd 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -131,7 +131,7 @@ export type Hit> = T & { _formatted?: Partial _matchesPosition?: MatchesPosition _rankingScore?: number - _rankingScoreDetails?: Record + _rankingScoreDetails?: RakingScoreDetails } export type RakingScoreDetails = {