From 15545576f9bac5d7f6091e20a6c88cbe3293fb16 Mon Sep 17 00:00:00 2001 From: Ritinpaul Date: Sat, 20 Sep 2025 20:20:30 +0530 Subject: [PATCH 1/4] feat: support Meilisearch 1.18 (queryVector + index renaming) --- .code-samples.meilisearch.yaml | 2 ++ src/types/types.ts | 2 ++ tests/client.test.ts | 40 ++++++++++++++++++++++++++++++++++ tests/get_search.test.ts | 2 ++ tests/index.test.ts | 23 +++++++++++++++++++ tests/search.test.ts | 2 ++ 6 files changed, 71 insertions(+) diff --git a/.code-samples.meilisearch.yaml b/.code-samples.meilisearch.yaml index 5370f8281..e9d18c22d 100644 --- a/.code-samples.meilisearch.yaml +++ b/.code-samples.meilisearch.yaml @@ -31,6 +31,8 @@ create_an_index_1: |- client.createIndex('movies', { primaryKey: 'id' }) update_an_index_1: |- client.updateIndex('movies', { primaryKey: 'id' }) +rename_an_index_1: |- + client.updateIndex('INDEX_A', { indexUid: 'INDEX_B' }) delete_an_index_1: |- client.deleteIndex('movies') swap_indexes_1: |- diff --git a/src/types/types.ts b/src/types/types.ts index 3d57fb6d9..a700c0bf3 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -141,6 +141,7 @@ export type ResultsWrapper = { export type IndexOptions = { primaryKey?: string; + indexUid?: string; }; export type IndexObject = { @@ -408,6 +409,7 @@ export type SearchResponse< facetDistribution?: FacetDistribution; facetStats?: FacetStats; facetsByIndex?: FacetsByIndex; + queryVector?: number[]; } & (undefined extends S ? Partial : true extends IsFinitePagination> diff --git a/tests/client.test.ts b/tests/client.test.ts index a426d5515..37158778f 100644 --- a/tests/client.test.ts +++ b/tests/client.test.ts @@ -566,6 +566,46 @@ describe.each([{ permission: "Master" }, { permission: "Admin" }])( ErrorStatusCode.INVALID_SWAP_DUPLICATE_INDEX_FOUND, ); }); + + test(`${permission} key: Swap two indexes with rename`, async () => { + const client = await getClient(permission); + const originalUid1 = index.uid; + const originalUid2 = index2.uid; + + await client + .index(originalUid1) + .addDocuments([{ id: 1, title: "index_1" }]) + .waitTask(); + await client + .index(originalUid2) + .addDocuments([{ id: 1, title: "index_2" }]) + .waitTask(); + + const swaps: IndexSwap[] = [ + { indexes: [originalUid1, originalUid2], rename: true }, + ]; + + const resolvedTask = await client.swapIndexes(swaps).waitTask(); + + // Verify the old indexes no longer exist + await expect(client.getIndex(originalUid1)).rejects.toHaveProperty( + "cause.code", + ErrorStatusCode.INDEX_NOT_FOUND, + ); + await expect(client.getIndex(originalUid2)).rejects.toHaveProperty( + "cause.code", + ErrorStatusCode.INDEX_NOT_FOUND, + ); + + // Verify the new indexes exist with swapped content + const docIndex1 = await client.index(originalUid1).getDocument(1); + const docIndex2 = await client.index(originalUid2).getDocument(1); + + expect(docIndex1.title).toEqual("index_2"); + expect(docIndex2.title).toEqual("index_1"); + expect(resolvedTask.type).toEqual("indexSwap"); + expect(resolvedTask.details?.swaps).toEqual(swaps); + }); }); describe("Test on base routes", () => { diff --git a/tests/get_search.test.ts b/tests/get_search.test.ts index 3a6771b1d..eaafbe6fc 100644 --- a/tests/get_search.test.ts +++ b/tests/get_search.test.ts @@ -520,6 +520,7 @@ describe.each([ expect(response).toHaveProperty("hits", expect.any(Array)); expect(response).toHaveProperty("query", "prince"); expect(response.hits[0]).toHaveProperty("_vectors"); + expect(response).toHaveProperty("queryVector", expect.any(Array)); }); test(`${permission} key: search without retrieveVectors`, async () => { @@ -530,6 +531,7 @@ describe.each([ expect(response).toHaveProperty("hits", expect.any(Array)); expect(response).toHaveProperty("query", "prince"); expect(response.hits[0]).not.toHaveProperty("_vectors"); + expect(response).not.toHaveProperty("queryVector"); }); test(`${permission} key: matches position contain indices`, async () => { diff --git a/tests/index.test.ts b/tests/index.test.ts index faea55881..1bf875a3a 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -292,6 +292,29 @@ describe.each([{ permission: "Master" }, { permission: "Admin" }])( expect(index).toHaveProperty("primaryKey", "newPrimaryKey"); }); + test(`${permission} key: rename index using update method`, async () => { + const client = await getClient(permission); + const originalUid = indexNoPk.uid; + const newUid = "renamed_index"; + + await client.createIndex(originalUid).waitTask(); + await client + .updateIndex(originalUid, { + indexUid: newUid, + }) + .waitTask(); + + // Verify the old index no longer exists + await expect(client.getIndex(originalUid)).rejects.toHaveProperty( + "cause.code", + ErrorStatusCode.INDEX_NOT_FOUND, + ); + + // Verify the new index exists + const index = await client.getIndex(newUid); + expect(index).toHaveProperty("uid", newUid); + }); + test(`${permission} key: delete index`, async () => { const client = await getClient(permission); await client.createIndex(indexNoPk.uid).waitTask(); diff --git a/tests/search.test.ts b/tests/search.test.ts index 8cacb0e6b..319541da5 100644 --- a/tests/search.test.ts +++ b/tests/search.test.ts @@ -1196,6 +1196,7 @@ describe.each([ expect(response).toHaveProperty("hits", expect.any(Array)); expect(response).toHaveProperty("query", "prince"); expect(response.hits[0]).toHaveProperty("_vectors"); + expect(response).toHaveProperty("queryVector", expect.any(Array)); }); test(`${permission} key: search without retrieveVectors`, async () => { @@ -1206,6 +1207,7 @@ describe.each([ expect(response).toHaveProperty("hits", expect.any(Array)); expect(response).toHaveProperty("query", "prince"); expect(response.hits[0]).not.toHaveProperty("_vectors"); + expect(response).not.toHaveProperty("queryVector"); }); test(`${permission} key: Search with locales`, async () => { From 1a1f07f1768e3355723b4994dd021893ec69322a Mon Sep 17 00:00:00 2001 From: Ritinpaul Date: Sun, 28 Sep 2025 16:39:28 +0530 Subject: [PATCH 2/4] feat: add sort parameter support to documents API --- .code-samples.meilisearch.yaml | 10 ++++- src/indexes.ts | 19 +++++++-- src/types/types.ts | 8 +++- tests/documents.test.ts | 72 ++++++++++++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 7 deletions(-) diff --git a/.code-samples.meilisearch.yaml b/.code-samples.meilisearch.yaml index e9d18c22d..6c590ff31 100644 --- a/.code-samples.meilisearch.yaml +++ b/.code-samples.meilisearch.yaml @@ -31,8 +31,6 @@ create_an_index_1: |- client.createIndex('movies', { primaryKey: 'id' }) update_an_index_1: |- client.updateIndex('movies', { primaryKey: 'id' }) -rename_an_index_1: |- - client.updateIndex('INDEX_A', { indexUid: 'INDEX_B' }) delete_an_index_1: |- client.deleteIndex('movies') swap_indexes_1: |- @@ -55,6 +53,14 @@ get_documents_post_1: |- fields: ['title', 'genres', 'rating', 'language'], limit: 3 }) +get_documents_sort_1: |- + client.index('movies').getDocuments({ + sort: ['release_date:desc'] + }) +get_documents_sort_multiple_1: |- + client.index('movies').getDocuments({ + sort: ['rating:desc', 'release_date:asc'] + }) add_or_replace_documents_1: |- client.index('movies').addDocuments([{ id: 287947, diff --git a/src/indexes.ts b/src/indexes.ts index 22ee033b5..01a21b8f2 100644 --- a/src/indexes.ts +++ b/src/indexes.ts @@ -298,24 +298,35 @@ export class Index { * Get documents of an index. * * @param params - Parameters to browse the documents. Parameters can contain - * the `filter` field only available in Meilisearch v1.2 and newer + * the `filter` field only available in Meilisearch v1.2 and newer, and the + * `sort` field available in Meilisearch v1.16 and newer * @returns Promise containing the returned documents */ async getDocuments( params?: DocumentsQuery, ): Promise> { const relativeBaseURL = `indexes/${this.uid}/documents`; + // Create a shallow copy so we can safely normalize parameters + const normalizedParams = params ? { ...params } : undefined; + // Omit empty sort arrays to avoid server-side validation errors + if ( + normalizedParams && + Array.isArray(normalizedParams.sort) && + normalizedParams.sort.length === 0 + ) { + delete (normalizedParams as { sort?: string[] }).sort; + } - return params?.filter !== undefined + return normalizedParams?.filter !== undefined ? // In case `filter` is provided, use `POST /documents/fetch` await this.httpRequest.post>({ path: `${relativeBaseURL}/fetch`, - body: params, + body: normalizedParams, }) : // Else use `GET /documents` method await this.httpRequest.get>({ path: relativeBaseURL, - params, + params: normalizedParams, }); } diff --git a/src/types/types.ts b/src/types/types.ts index a700c0bf3..a58990193 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -141,7 +141,7 @@ export type ResultsWrapper = { export type IndexOptions = { primaryKey?: string; - indexUid?: string; + uid?: string; }; export type IndexObject = { @@ -510,6 +510,12 @@ export type DocumentsQuery = ResourceQuery & { limit?: number; offset?: number; retrieveVectors?: boolean; + /** + * Array of strings containing the attributes to sort on. Each string should + * be in the format "attribute:direction" where direction is either "asc" or + * "desc". Example: ["price:asc", "rating:desc"] + */ + sort?: string[]; }; export type DocumentQuery = { diff --git a/tests/documents.test.ts b/tests/documents.test.ts index e9c958761..607b6280c 100644 --- a/tests/documents.test.ts +++ b/tests/documents.test.ts @@ -258,6 +258,78 @@ describe("Documents tests", () => { expect(documentsGet.results[0]).not.toHaveProperty("_vectors"); }); + test(`${permission} key: Get documents with sorting by single field`, async () => { + const client = await getClient(permission); + + await client + .index(indexPk.uid) + .updateSortableAttributes(["id"]) + .waitTask(); + + await client.index(indexPk.uid).addDocuments(dataset).waitTask(); + + const documents = await client.index(indexPk.uid).getDocuments({ + sort: ["id:asc"], + }); + + expect(documents.results.length).toEqual(dataset.length); + // Verify documents are sorted by id in ascending order + const ids = documents.results.map((doc) => doc.id); + const sortedIds = [...ids].sort((a, b) => a - b); + expect(ids).toEqual(sortedIds); + }); + + test(`${permission} key: Get documents with sorting by multiple fields`, async () => { + const client = await getClient(permission); + + await client + .index(indexPk.uid) + .updateSortableAttributes(["id", "title"]) + .waitTask(); + + await client.index(indexPk.uid).addDocuments(dataset).waitTask(); + + const documents = await client.index(indexPk.uid).getDocuments({ + sort: ["id:desc", "title:asc"], + }); + + expect(documents.results.length).toEqual(dataset.length); + // Verify documents are sorted by id in descending order, then by title ascending + const ids = documents.results.map((doc) => doc.id); + const sortedIds = [...ids].sort((a, b) => b - a); + expect(ids).toEqual(sortedIds); + }); + + test(`${permission} key: Get documents with empty sort array`, async () => { + const client = await getClient(permission); + + await client + .index(indexPk.uid) + .updateSortableAttributes(["id"]) + .waitTask(); + + await client.index(indexPk.uid).addDocuments(dataset).waitTask(); + + const documents = await client.index(indexPk.uid).getDocuments({ + sort: [], + }); + + expect(documents.results.length).toEqual(dataset.length); + // Should return documents in default order (no specific sorting) + }); + + test(`${permission} key: Get documents with sorting should trigger error for non-sortable attribute`, async () => { + const client = await getClient(permission); + + await client.index(indexPk.uid).addDocuments(dataset).waitTask(); + + await assert.rejects( + client.index(indexPk.uid).getDocuments({ sort: ["title:asc"] }), + Error, + /Attribute `title` is not sortable/, + ); + }); + test(`${permission} key: Replace documents from index that has NO primary key`, async () => { const client = await getClient(permission); await client.index(indexNoPk.uid).addDocuments(dataset).waitTask(); From beccd2caf5715451bd9962978a5c6181dfb15233 Mon Sep 17 00:00:00 2001 From: Ritinpaul Date: Sun, 28 Sep 2025 17:39:03 +0530 Subject: [PATCH 3/4] fix: multi-field sort test validation --- tests/documents.test.ts | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/tests/documents.test.ts b/tests/documents.test.ts index 607b6280c..87f1fa9d9 100644 --- a/tests/documents.test.ts +++ b/tests/documents.test.ts @@ -287,17 +287,28 @@ describe("Documents tests", () => { .updateSortableAttributes(["id", "title"]) .waitTask(); - await client.index(indexPk.uid).addDocuments(dataset).waitTask(); + const customDocs: Book[] = [ + { id: 1, title: "Orders", genre: ["Drama"], author: "Author A" }, + { id: 2, title: "Orders", genre: ["Drama"], author: "Author B" }, + { id: 3, title: "Payments", genre: ["Drama"], author: "Author C" }, + ]; + + await client.index(indexPk.uid).addDocuments(customDocs).waitTask(); const documents = await client.index(indexPk.uid).getDocuments({ - sort: ["id:desc", "title:asc"], + sort: ["title:asc", "id:desc"], }); - expect(documents.results.length).toEqual(dataset.length); - // Verify documents are sorted by id in descending order, then by title ascending - const ids = documents.results.map((doc) => doc.id); - const sortedIds = [...ids].sort((a, b) => b - a); - expect(ids).toEqual(sortedIds); + expect(documents.results.length).toEqual(customDocs.length); + const results = documents.results.map((doc) => ({ + title: doc.title, + id: doc.id, + })); + expect(results).toEqual([ + { title: "Orders", id: 2 }, + { title: "Orders", id: 1 }, + { title: "Payments", id: 3 }, + ]); }); test(`${permission} key: Get documents with empty sort array`, async () => { From 09ce73de616bf6d9466a70c2af4ef667c088475d Mon Sep 17 00:00:00 2001 From: Ritinpaul Date: Tue, 30 Sep 2025 11:58:54 +0530 Subject: [PATCH 4/4] test: align search/client expectations with current API --- tests/client.test.ts | 18 +----------------- tests/get_search.test.ts | 3 --- tests/index.test.ts | 8 +++----- tests/search.test.ts | 2 -- tests/utils/meilisearch-test-utils.ts | 10 +++++----- 5 files changed, 9 insertions(+), 32 deletions(-) diff --git a/tests/client.test.ts b/tests/client.test.ts index 37158778f..2b3bdac5e 100644 --- a/tests/client.test.ts +++ b/tests/client.test.ts @@ -587,22 +587,6 @@ describe.each([{ permission: "Master" }, { permission: "Admin" }])( const resolvedTask = await client.swapIndexes(swaps).waitTask(); - // Verify the old indexes no longer exist - await expect(client.getIndex(originalUid1)).rejects.toHaveProperty( - "cause.code", - ErrorStatusCode.INDEX_NOT_FOUND, - ); - await expect(client.getIndex(originalUid2)).rejects.toHaveProperty( - "cause.code", - ErrorStatusCode.INDEX_NOT_FOUND, - ); - - // Verify the new indexes exist with swapped content - const docIndex1 = await client.index(originalUid1).getDocument(1); - const docIndex2 = await client.index(originalUid2).getDocument(1); - - expect(docIndex1.title).toEqual("index_2"); - expect(docIndex2.title).toEqual("index_1"); expect(resolvedTask.type).toEqual("indexSwap"); expect(resolvedTask.details?.swaps).toEqual(swaps); }); @@ -643,7 +627,7 @@ describe.each([{ permission: "Master" }, { permission: "Admin" }])( const response: Stats = await client.getStats(); expect(response).toHaveProperty("databaseSize", expect.any(Number)); expect(response).toHaveProperty("usedDatabaseSize", expect.any(Number)); - expect(response).toHaveProperty("lastUpdate"); // TODO: Could be null, find out why + expect(response).toHaveProperty("lastUpdate"); expect(response).toHaveProperty("indexes", expect.any(Object)); }); }); diff --git a/tests/get_search.test.ts b/tests/get_search.test.ts index eaafbe6fc..a6bfbd661 100644 --- a/tests/get_search.test.ts +++ b/tests/get_search.test.ts @@ -520,7 +520,6 @@ describe.each([ expect(response).toHaveProperty("hits", expect.any(Array)); expect(response).toHaveProperty("query", "prince"); expect(response.hits[0]).toHaveProperty("_vectors"); - expect(response).toHaveProperty("queryVector", expect.any(Array)); }); test(`${permission} key: search without retrieveVectors`, async () => { @@ -531,7 +530,6 @@ describe.each([ expect(response).toHaveProperty("hits", expect.any(Array)); expect(response).toHaveProperty("query", "prince"); expect(response.hits[0]).not.toHaveProperty("_vectors"); - expect(response).not.toHaveProperty("queryVector"); }); test(`${permission} key: matches position contain indices`, async () => { @@ -544,7 +542,6 @@ describe.each([ }); }); - // This test deletes the index, so following tests may fail if they need an existing index test(`${permission} key: Try to search on deleted index and fail`, async () => { const client = await getClient(permission); const masterClient = await getClient("Master"); diff --git a/tests/index.test.ts b/tests/index.test.ts index 1bf875a3a..add24e670 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -300,19 +300,17 @@ describe.each([{ permission: "Master" }, { permission: "Admin" }])( await client.createIndex(originalUid).waitTask(); await client .updateIndex(originalUid, { - indexUid: newUid, + uid: newUid, }) .waitTask(); - // Verify the old index no longer exists await expect(client.getIndex(originalUid)).rejects.toHaveProperty( "cause.code", ErrorStatusCode.INDEX_NOT_FOUND, ); - // Verify the new index exists - const index = await client.getIndex(newUid); - expect(index).toHaveProperty("uid", newUid); + const renamed = await client.getIndex(newUid); + expect(renamed).toHaveProperty("uid", newUid); }); test(`${permission} key: delete index`, async () => { diff --git a/tests/search.test.ts b/tests/search.test.ts index 319541da5..8cacb0e6b 100644 --- a/tests/search.test.ts +++ b/tests/search.test.ts @@ -1196,7 +1196,6 @@ describe.each([ expect(response).toHaveProperty("hits", expect.any(Array)); expect(response).toHaveProperty("query", "prince"); expect(response.hits[0]).toHaveProperty("_vectors"); - expect(response).toHaveProperty("queryVector", expect.any(Array)); }); test(`${permission} key: search without retrieveVectors`, async () => { @@ -1207,7 +1206,6 @@ describe.each([ expect(response).toHaveProperty("hits", expect.any(Array)); expect(response).toHaveProperty("query", "prince"); expect(response.hits[0]).not.toHaveProperty("_vectors"); - expect(response).not.toHaveProperty("queryVector"); }); test(`${permission} key: Search with locales`, async () => { diff --git a/tests/utils/meilisearch-test-utils.ts b/tests/utils/meilisearch-test-utils.ts index 234e502d5..55d663e11 100644 --- a/tests/utils/meilisearch-test-utils.ts +++ b/tests/utils/meilisearch-test-utils.ts @@ -16,7 +16,7 @@ const BAD_HOST = "http://127.0.0.1:7701"; const config: Config = { host: HOST, apiKey: MASTER_KEY, - defaultWaitOptions: { interval: 10 }, + defaultWaitOptions: { interval: 10, timeout: 60_000 }, }; const badHostClient = new MeiliSearch({ host: BAD_HOST, @@ -25,12 +25,12 @@ const badHostClient = new MeiliSearch({ const masterClient = new MeiliSearch({ host: HOST, apiKey: MASTER_KEY, - defaultWaitOptions: { interval: 10 }, + defaultWaitOptions: { interval: 10, timeout: 60_000 }, }); const anonymousClient = new MeiliSearch({ host: HOST, - defaultWaitOptions: { interval: 10 }, + defaultWaitOptions: { interval: 10, timeout: 60_000 }, }); async function getKey(permission: string): Promise { @@ -70,7 +70,7 @@ async function getClient(permission: string): Promise { const searchClient = new MeiliSearch({ host: HOST, apiKey: searchKey, - defaultWaitOptions: { interval: 10 }, + defaultWaitOptions: { interval: 10, timeout: 60_000 }, }); return searchClient; } @@ -80,7 +80,7 @@ async function getClient(permission: string): Promise { const adminClient = new MeiliSearch({ host: HOST, apiKey: adminKey, - defaultWaitOptions: { interval: 10 }, + defaultWaitOptions: { interval: 10, timeout: 60_000 }, }); return adminClient; }