Skip to content

Commit d11742a

Browse files
meili-bors[bot]meili-botserendipty01mdubuscurquiza
authored
Merge #1668
1668: Changes related to the next Meilisearch release (v1.9.0) r=curquiza a=meili-bot Related to this issue: meilisearch/integration-guides#301 This PR: - gathers the changes related to the next Meilisearch release (v1.9.0) so that this package is ready when the official release is out. - should pass the tests against the [latest pre-release of Meilisearch](https://github.com/meilisearch/meilisearch/releases). - might eventually contain test failures until the Meilisearch v1.9.0 is out. ⚠️ This PR should NOT be merged until the next release of Meilisearch (v1.9.0) is out. _This PR is auto-generated for the [pre-release week](https://github.com/meilisearch/integration-guides/blob/main/resources/pre-release-week.md) purpose._ Co-authored-by: meili-bot <[email protected]> Co-authored-by: Shalabh Agarwal <[email protected]> Co-authored-by: Morgane Dubus <[email protected]> Co-authored-by: curquiza <[email protected]> Co-authored-by: Clémentine <[email protected]>
2 parents 4db319c + 3474a94 commit d11742a

File tree

7 files changed

+379
-1
lines changed

7 files changed

+379
-1
lines changed

.code-samples.meilisearch.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,3 +754,22 @@ negative_search_1: |-
754754
client.index('movies').search('-escape')
755755
negative_search_2: |-
756756
client.index('movies').search('-"escape"')
757+
search_parameter_reference_ranking_score_threshold_1: |-
758+
client.index('INDEX_NAME').search('badman', { rankingScoreThreshold: 0.2 })
759+
search_parameter_reference_retrieve_vectors_1: |-
760+
client.index('INDEX_NAME').search('kitchen utensils', {
761+
retrieveVectors: true,
762+
hybrid: { embedder: 'default'}
763+
})
764+
get_similar_post_1: |-
765+
client.index('INDEX_NAME').searchSimilarDocuments({ id: 'TARGET_DOCUMENT_ID'})
766+
search_parameter_guide_matching_strategy_3: |-
767+
client.index('movies').search('white shirt', {
768+
matchingStrategy: 'frequency'
769+
})
770+
search_parameter_reference_distinct_1: |-
771+
client.index('INDEX_NAME').search('QUERY TERMS', { distinct: 'ATTRIBUTE_A' })
772+
distinct_attribute_guide_filterable_1: |-
773+
client.index('products').updateFilterableAttributes(['product_id', 'sku', 'url'])
774+
distinct_attribute_guide_distinct_parameter_1: |-
775+
client.index('products').search('white shirt', { distinct: 'sku' })

src/indexes.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import {
5353
ProximityPrecision,
5454
Embedders,
5555
SearchCutoffMs,
56+
SearchSimilarDocumentsParams,
5657
} from './types';
5758
import { removeUndefinedFromObject } from './utils';
5859
import { HttpRequests } from './http-requests';
@@ -177,6 +178,25 @@ class Index<T extends Record<string, any> = Record<string, any>> {
177178
);
178179
}
179180

181+
/**
182+
* Search for similar documents
183+
*
184+
* @param params - Parameters used to search for similar documents
185+
* @returns Promise containing the search response
186+
*/
187+
async searchSimilarDocuments<
188+
D extends Record<string, any> = T,
189+
S extends SearchParams = SearchParams,
190+
>(params: SearchSimilarDocumentsParams): Promise<SearchResponse<D, S>> {
191+
const url = `indexes/${this.uid}/similar`;
192+
193+
return await this.httpRequest.post(
194+
url,
195+
removeUndefinedFromObject(params),
196+
undefined,
197+
);
198+
}
199+
180200
///
181201
/// INDEX
182202
///

src/types/types.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export type IndexesResults<T> = ResourceResults<T> & {};
6363
export const MatchingStrategies = {
6464
ALL: 'all',
6565
LAST: 'last',
66+
FREQUENCY: 'frequency',
6667
} as const;
6768

6869
export type MatchingStrategies =
@@ -124,8 +125,11 @@ export type SearchParams = Query &
124125
vector?: number[] | null;
125126
showRankingScore?: boolean;
126127
showRankingScoreDetails?: boolean;
128+
rankingScoreThreshold?: number;
127129
attributesToSearchOn?: string[] | null;
128130
hybrid?: HybridSearch;
131+
distinct?: string;
132+
retrieveVectors?: boolean;
129133
};
130134

131135
// Search parameters for searches made with the GET method
@@ -145,6 +149,9 @@ export type SearchRequestGET = Pagination &
145149
attributesToSearchOn?: string | null;
146150
hybridEmbedder?: string;
147151
hybridSemanticRatio?: number;
152+
rankingScoreThreshold?: number;
153+
distinct?: string;
154+
retrieveVectors?: boolean;
148155
};
149156

150157
export type MultiSearchQuery = SearchParams & { indexUid: string };
@@ -262,6 +269,18 @@ export type FieldDistribution = {
262269
[field: string]: number;
263270
};
264271

272+
export type SearchSimilarDocumentsParams = {
273+
id: string | number;
274+
offset?: number;
275+
limit?: number;
276+
filter?: Filter;
277+
embedder?: string;
278+
attributesToRetrieve?: string[];
279+
showRankingScore?: boolean;
280+
showRankingScoreDetails?: boolean;
281+
rankingScoreThreshold?: number;
282+
};
283+
265284
/*
266285
** Documents
267286
*/
@@ -294,6 +313,7 @@ export type DocumentsQuery<T = Record<string, any>> = ResourceQuery & {
294313
filter?: Filter;
295314
limit?: number;
296315
offset?: number;
316+
retrieveVectors?: boolean;
297317
};
298318

299319
export type DocumentQuery<T = Record<string, any>> = {
@@ -1010,6 +1030,14 @@ export const ErrorStatusCode = {
10101030

10111031
/** @see https://www.meilisearch.com/docs/reference/errors/error_codes#invalid_facet_search_facet_query */
10121032
INVALID_FACET_SEARCH_FACET_QUERY: 'invalid_facet_search_facet_query',
1033+
1034+
/** @see https://www.meilisearch.com/docs/reference/errors/error_codes#invalid_search_ranking_score_threshold */
1035+
INVALID_SEARCH_RANKING_SCORE_THRESHOLD:
1036+
'invalid_search_ranking_score_threshold',
1037+
1038+
/** @see https://www.meilisearch.com/docs/reference/errors/error_codes#invalid_similar_ranking_score_threshold */
1039+
INVALID_SIMILAR_RANKING_SCORE_THRESHOLD:
1040+
'invalid_similar_ranking_score_threshold',
10131041
};
10141042

10151043
export type ErrorStatusCode =

tests/documents.test.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,92 @@ Hint: It might not be working because maybe you're not up to date with the Meili
218218
expect(documents.results.length).toEqual(dataset.length);
219219
});
220220

221+
test(`${permission} key: Get documents with retrieveVectors to true`, async () => {
222+
const client = await getClient(permission);
223+
const adminKey = await getKey('Admin');
224+
225+
await fetch(`${HOST}/experimental-features`, {
226+
body: JSON.stringify({ vectorStore: true }),
227+
headers: {
228+
Authorization: `Bearer ${adminKey}`,
229+
'Content-Type': 'application/json',
230+
},
231+
method: 'PATCH',
232+
});
233+
234+
const { taskUid } = await client
235+
.index(indexPk.uid)
236+
.addDocuments(dataset);
237+
await client.index(indexPk.uid).waitForTask(taskUid);
238+
239+
// Get documents with POST
240+
const documentsPost = await client
241+
.index(indexPk.uid)
242+
.getDocuments<Book>({ retrieveVectors: true });
243+
244+
expect(documentsPost.results.length).toEqual(dataset.length);
245+
expect(documentsPost.results[0]).toHaveProperty('_vectors');
246+
247+
// Get documents with GET
248+
const res = await fetch(
249+
`${HOST}/indexes/${indexPk.uid}/documents?retrieveVectors=true`,
250+
{
251+
headers: {
252+
Authorization: `Bearer ${adminKey}`,
253+
'Content-Type': 'application/json',
254+
},
255+
method: 'GET',
256+
},
257+
);
258+
const documentsGet = await res.json();
259+
260+
expect(documentsGet.results.length).toEqual(dataset.length);
261+
expect(documentsGet.results[0]).toHaveProperty('_vectors');
262+
});
263+
264+
test(`${permission} key: Get documents without retrieveVectors`, async () => {
265+
const client = await getClient(permission);
266+
const adminKey = await getKey('Admin');
267+
268+
await fetch(`${HOST}/experimental-features`, {
269+
body: JSON.stringify({ vectorStore: true }),
270+
headers: {
271+
Authorization: `Bearer ${adminKey}`,
272+
'Content-Type': 'application/json',
273+
},
274+
method: 'PATCH',
275+
});
276+
277+
const { taskUid } = await client
278+
.index(indexPk.uid)
279+
.addDocuments(dataset);
280+
await client.index(indexPk.uid).waitForTask(taskUid);
281+
282+
// Get documents with POST
283+
const documentsPost = await client
284+
.index(indexPk.uid)
285+
.getDocuments<Book>();
286+
287+
expect(documentsPost.results.length).toEqual(dataset.length);
288+
expect(documentsPost.results[0]).not.toHaveProperty('_vectors');
289+
290+
// Get documents with GET
291+
const res = await fetch(
292+
`${HOST}/indexes/${indexPk.uid}/documents?retrieveVectors=false`,
293+
{
294+
headers: {
295+
Authorization: `Bearer ${adminKey}`,
296+
'Content-Type': 'application/json',
297+
},
298+
method: 'GET',
299+
},
300+
);
301+
const documentsGet = await res.json();
302+
303+
expect(documentsGet.results.length).toEqual(dataset.length);
304+
expect(documentsGet.results[0]).not.toHaveProperty('_vectors');
305+
});
306+
221307
test(`${permission} key: Replace documents from index that has NO primary key`, async () => {
222308
const client = await getClient(permission);
223309
const { taskUid: addDocTask } = await client

tests/embedders.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,39 @@ const index = {
1414
uid: 'movies_test',
1515
};
1616

17+
const datasetSimilarSearch = [
18+
{
19+
title: 'Shazam!',
20+
release_year: 2019,
21+
id: '287947',
22+
_vectors: { manual: [0.8, 0.4, -0.5] },
23+
},
24+
{
25+
title: 'Captain Marvel',
26+
release_year: 2019,
27+
id: '299537',
28+
_vectors: { manual: [0.6, 0.8, -0.2] },
29+
},
30+
{
31+
title: 'Escape Room',
32+
release_year: 2019,
33+
id: '522681',
34+
_vectors: { manual: [0.1, 0.6, 0.8] },
35+
},
36+
{
37+
title: 'How to Train Your Dragon: The Hidden World',
38+
release_year: 2019,
39+
id: '166428',
40+
_vectors: { manual: [0.7, 0.7, -0.4] },
41+
},
42+
{
43+
title: 'All Quiet on the Western Front',
44+
release_year: 1930,
45+
id: '143',
46+
_vectors: { manual: [-0.5, 0.3, 0.85] },
47+
},
48+
];
49+
1750
jest.setTimeout(100 * 1000);
1851

1952
afterAll(() => {
@@ -223,6 +256,38 @@ describe.each([{ permission: 'Master' }, { permission: 'Admin' }])(
223256

224257
expect(response).toEqual(null);
225258
});
259+
260+
test(`${permission} key: search for similar documents`, async () => {
261+
const client = await getClient(permission);
262+
263+
const newEmbedder: Embedders = {
264+
manual: {
265+
source: 'userProvided',
266+
dimensions: 3,
267+
},
268+
};
269+
const { taskUid: updateEmbeddersTask }: EnqueuedTask = await client
270+
.index(index.uid)
271+
.updateEmbedders(newEmbedder);
272+
273+
await client.waitForTask(updateEmbeddersTask);
274+
275+
const { taskUid: documentAdditionTask } = await client
276+
.index(index.uid)
277+
.addDocuments(datasetSimilarSearch);
278+
279+
await client.waitForTask(documentAdditionTask);
280+
281+
const response = await client.index(index.uid).searchSimilarDocuments({
282+
id: '143',
283+
});
284+
285+
expect(response).toHaveProperty('hits');
286+
expect(response.hits.length).toEqual(4);
287+
expect(response).toHaveProperty('offset', 0);
288+
expect(response).toHaveProperty('limit', 20);
289+
expect(response).toHaveProperty('estimatedTotalHits', 4);
290+
});
226291
},
227292
);
228293

tests/get_search.test.ts

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -483,11 +483,85 @@ describe.each([
483483

484484
test(`${permission} key: search without vectors`, async () => {
485485
const client = await getClient(permission);
486-
const response = await client.index(index.uid).search('prince', {});
486+
const response = await client.index(index.uid).searchGet('prince', {});
487487

488488
expect(response).not.toHaveProperty('semanticHitCount');
489489
});
490490

491+
test(`${permission} key: search with rankingScoreThreshold filter`, async () => {
492+
const client = await getClient(permission);
493+
494+
const response = await client.index(index.uid).searchGet('prince', {
495+
showRankingScore: true,
496+
rankingScoreThreshold: 0.8,
497+
});
498+
499+
const hit = response.hits[0];
500+
501+
expect(response).toHaveProperty('hits', expect.any(Array));
502+
expect(response).toHaveProperty('query', 'prince');
503+
expect(hit).toHaveProperty('_rankingScore');
504+
expect(hit['_rankingScore']).toBeGreaterThanOrEqual(0.8);
505+
506+
const response2 = await client.index(index.uid).search('prince', {
507+
showRankingScore: true,
508+
rankingScoreThreshold: 0.9,
509+
});
510+
511+
expect(response2.hits.length).toBeLessThanOrEqual(0);
512+
});
513+
514+
test(`${permission} key: search with distinct`, async () => {
515+
const client = await getClient(permission);
516+
const response = await client
517+
.index(index.uid)
518+
.search('', { distinct: 'genre' });
519+
520+
expect(response.hits.length).toEqual(4);
521+
});
522+
523+
test(`${permission} key: search with retrieveVectors to true`, async () => {
524+
const client = await getClient(permission);
525+
const adminKey = await getKey('Admin');
526+
527+
await fetch(`${HOST}/experimental-features`, {
528+
body: JSON.stringify({ vectorStore: true }),
529+
headers: {
530+
Authorization: `Bearer ${adminKey}`,
531+
'Content-Type': 'application/json',
532+
},
533+
method: 'PATCH',
534+
});
535+
536+
const response = await client.index(index.uid).searchGet('prince', {
537+
retrieveVectors: true,
538+
});
539+
540+
expect(response).toHaveProperty('hits', expect.any(Array));
541+
expect(response).toHaveProperty('query', 'prince');
542+
expect(response.hits[0]).toHaveProperty('_vectors');
543+
});
544+
545+
test(`${permission} key: search without retrieveVectors`, async () => {
546+
const client = await getClient(permission);
547+
const adminKey = await getKey('Admin');
548+
549+
await fetch(`${HOST}/experimental-features`, {
550+
body: JSON.stringify({ vectorStore: true }),
551+
headers: {
552+
Authorization: `Bearer ${adminKey}`,
553+
'Content-Type': 'application/json',
554+
},
555+
method: 'PATCH',
556+
});
557+
558+
const response = await client.index(index.uid).searchGet('prince');
559+
560+
expect(response).toHaveProperty('hits', expect.any(Array));
561+
expect(response).toHaveProperty('query', 'prince');
562+
expect(response.hits[0]).not.toHaveProperty('_vectors');
563+
});
564+
491565
test(`${permission} key: Try to search on deleted index and fail`, async () => {
492566
const client = await getClient(permission);
493567
const masterClient = await getClient('Master');

0 commit comments

Comments
 (0)