Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .code-samples.meilisearch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,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,
Expand Down
19 changes: 15 additions & 4 deletions src/indexes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,24 +298,35 @@ export class Index<T extends RecordAny = RecordAny> {
* 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<D extends RecordAny = T>(
params?: DocumentsQuery<D>,
): Promise<ResourceResults<D[]>> {
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<ResourceResults<D[]>>({
path: `${relativeBaseURL}/fetch`,
body: params,
body: normalizedParams,
})
: // Else use `GET /documents` method
await this.httpRequest.get<ResourceResults<D[]>>({
path: relativeBaseURL,
params,
params: normalizedParams,
});
}

Expand Down
8 changes: 8 additions & 0 deletions src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ export type ResultsWrapper<T> = {

export type IndexOptions = {
primaryKey?: string;
uid?: string;
};

export type IndexObject = {
Expand Down Expand Up @@ -408,6 +409,7 @@ export type SearchResponse<
facetDistribution?: FacetDistribution;
facetStats?: FacetStats;
facetsByIndex?: FacetsByIndex;
queryVector?: number[];
} & (undefined extends S
? Partial<FinitePagination & InfinitePagination>
: true extends IsFinitePagination<NonNullable<S>>
Expand Down Expand Up @@ -508,6 +510,12 @@ export type DocumentsQuery<T = RecordAny> = 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<T = RecordAny> = {
Expand Down
26 changes: 25 additions & 1 deletion tests/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,30 @@ 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();

expect(resolvedTask.type).toEqual("indexSwap");
expect(resolvedTask.details?.swaps).toEqual(swaps);
});
});

describe("Test on base routes", () => {
Expand Down Expand Up @@ -603,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));
});
});
Expand Down
83 changes: 83 additions & 0 deletions tests/documents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,89 @@ 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<Book>({
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();

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<Book>({
sort: ["title:asc", "id:desc"],
});

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 () => {
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<Book>({
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();
Expand Down
1 change: 0 additions & 1 deletion tests/get_search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,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");
Expand Down
21 changes: 21 additions & 0 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,27 @@ 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, {
uid: newUid,
})
.waitTask();

await expect(client.getIndex(originalUid)).rejects.toHaveProperty(
"cause.code",
ErrorStatusCode.INDEX_NOT_FOUND,
);

const renamed = await client.getIndex(newUid);
expect(renamed).toHaveProperty("uid", newUid);
});

test(`${permission} key: delete index`, async () => {
const client = await getClient(permission);
await client.createIndex(indexNoPk.uid).waitTask();
Expand Down
10 changes: 5 additions & 5 deletions tests/utils/meilisearch-test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<string> {
Expand Down Expand Up @@ -70,7 +70,7 @@ async function getClient(permission: string): Promise<MeiliSearch> {
const searchClient = new MeiliSearch({
host: HOST,
apiKey: searchKey,
defaultWaitOptions: { interval: 10 },
defaultWaitOptions: { interval: 10, timeout: 60_000 },
});
return searchClient;
}
Expand All @@ -80,7 +80,7 @@ async function getClient(permission: string): Promise<MeiliSearch> {
const adminClient = new MeiliSearch({
host: HOST,
apiKey: adminKey,
defaultWaitOptions: { interval: 10 },
defaultWaitOptions: { interval: 10, timeout: 60_000 },
});
return adminClient;
}
Expand Down