Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
40 changes: 40 additions & 0 deletions tests/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
72 changes: 72 additions & 0 deletions tests/documents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<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();

await client.index(indexPk.uid).addDocuments(dataset).waitTask();

const documents = await client.index(indexPk.uid).getDocuments<Book>({
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<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
2 changes: 2 additions & 0 deletions tests/get_search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand Down
23 changes: 23 additions & 0 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions tests/search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand Down