Skip to content

Commit 4664ed4

Browse files
Merge #1474
1474: Add method to add/update documents from a string r=bidoubiwa a=bidoubiwa Partial: #1061 Introduces two methods on Index(): - `addDocumentsFromString` - `updateDocumentsFromString` These allow the user to add documents using any of the following format: `csv`, `json` or `jsonld`. They require 2 parameters and 1 optional parameter: - the documents in a string - the content type, either one of the following options: - `'text/csv'` or `ContentType.CSV` in typescript - `'application/x-ndjson'` or `ContentType.NDJSON` in typescript - 'application/json' or `ContentType.JSON` in typescript - The document addition options. for example the primary key The content of the string must match its content-type. Only when using the `CSV` format it is possible to use `csvDelimiter` Usage: ```ts client.index('myIndex').addDocumentsFromString(documents: string, contentType: ContentType, queryParams: RawDocumentAdditionOptions): Promise<EnqueuedTask> ``` ```ts client.index('myIndex').updateDocumentsFromString(documents: string, contentType: ContentType, queryParams: RawDocumentAdditionOptions): Promise<EnqueuedTask> ``` ### Example: https://github.com/meilisearch/meilisearch-js/blob/d948a7551108b18def7e1e69c05997c47b9ac2ae/tests/raw_document.test.ts#L164-L174 Co-authored-by: Charlotte Vermandel <[email protected]>
2 parents 4f60551 + 3e7c1a8 commit 4664ed4

File tree

5 files changed

+333
-3
lines changed

5 files changed

+333
-3
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,12 @@ client.multiSearch(queries?: MultiSearchParams, config?: Partial<Request>): Prom
420420
client.index('myIndex').addDocuments(documents: Document<T>[]): Promise<EnqueuedTask>
421421
```
422422

423+
#### [Add or replace multiple documents in string format](https://docs.meilisearch.com/reference/api/documents.html#add-or-update-documents)
424+
425+
```ts
426+
client.index('myIndex').addDocumentsFromString(documents: string, contentType: ContentType, queryParams: RawDocumentAdditionOptions): Promise<EnqueuedTask>
427+
```
428+
423429
#### [Add or replace multiple documents in batches](https://docs.meilisearch.com/reference/api/documents.html#add-or-replace-documents)
424430

425431
```ts
@@ -432,6 +438,12 @@ client.index('myIndex').addDocumentsInBatches(documents: Document<T>[], batchSiz
432438
client.index('myIndex').updateDocuments(documents: Array<Document<Partial<T>>>): Promise<EnqueuedTask>
433439
```
434440

441+
#### [Add or update multiple documents in string format](https://docs.meilisearch.com/reference/api/documents.html#add-or-update-documents)
442+
443+
```ts
444+
client.index('myIndex').updateDocumentsFromString(documents: string, contentType: ContentType, queryParams: RawDocumentAdditionOptions): Promise<EnqueuedTask>
445+
```
446+
435447
#### [Add or update multiple documents in batches](https://docs.meilisearch.com/reference/api/documents.html#add-or-update-documents)
436448

437449
```ts

src/http-requests.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ class HttpRequests {
112112
url,
113113
params,
114114
body,
115-
config,
115+
config = {},
116116
}: {
117117
method: string
118118
url: string
@@ -129,14 +129,22 @@ class HttpRequests {
129129
constructURL.search = queryParams.toString()
130130
}
131131

132+
// in case a custom content-type is provided
133+
// do not stringify body
134+
if (!config.headers?.['Content-Type']) {
135+
body = JSON.stringify(body)
136+
}
137+
138+
const headers = { ...this.headers, ...config.headers }
139+
132140
try {
133141
const fetchFn = this.httpClient ? this.httpClient : fetch
134142
const result = fetchFn(constructURL.toString(), {
135143
...config,
136144
...this.requestConfig,
137145
method,
138-
body: JSON.stringify(body),
139-
headers: this.headers,
146+
body,
147+
headers,
140148
})
141149

142150
// When using a custom HTTP client, the response is returned to allow the user to parse/handle it as they see fit

src/indexes.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ import {
3737
PaginationSettings,
3838
Faceting,
3939
ResourceResults,
40+
RawDocumentAdditionOptions,
41+
ContentType,
4042
} from './types'
4143
import { removeUndefinedFromObject } from './utils'
4244
import { HttpRequests } from './http-requests'
@@ -372,6 +374,32 @@ class Index<T extends Record<string, any> = Record<string, any>> {
372374
return new EnqueuedTask(task)
373375
}
374376

377+
/**
378+
* Add or replace multiples documents in a string format to an index. It only
379+
* supports csv, ndjson and json formats.
380+
*
381+
* @param documents - Documents provided in a string to add/replace
382+
* @param contentType - Content type of your document:
383+
* 'text/csv'|'application/x-ndjson'|'application/json'
384+
* @param options - Options on document addition
385+
* @returns Promise containing an EnqueuedTask
386+
*/
387+
async addDocumentsFromString(
388+
documents: string,
389+
contentType: ContentType,
390+
queryParams?: RawDocumentAdditionOptions
391+
): Promise<EnqueuedTask> {
392+
const url = `indexes/${this.uid}/documents`
393+
394+
const task = await this.httpRequest.post(url, documents, queryParams, {
395+
headers: {
396+
'Content-Type': contentType,
397+
},
398+
})
399+
400+
return new EnqueuedTask(task)
401+
}
402+
375403
/**
376404
* Add or replace multiples documents to an index in batches
377405
*
@@ -433,6 +461,32 @@ class Index<T extends Record<string, any> = Record<string, any>> {
433461
return updates
434462
}
435463

464+
/**
465+
* Add or update multiples documents in a string format to an index. It only
466+
* supports csv, ndjson and json formats.
467+
*
468+
* @param documents - Documents provided in a string to add/update
469+
* @param contentType - Content type of your document:
470+
* 'text/csv'|'application/x-ndjson'|'application/json'
471+
* @param queryParams - Options on raw document addition
472+
* @returns Promise containing an EnqueuedTask
473+
*/
474+
async updateDocumentsFromString(
475+
documents: string,
476+
contentType: ContentType,
477+
queryParams?: RawDocumentAdditionOptions
478+
): Promise<EnqueuedTask> {
479+
const url = `indexes/${this.uid}/documents`
480+
481+
const task = await this.httpRequest.put(url, documents, queryParams, {
482+
headers: {
483+
'Content-Type': contentType,
484+
},
485+
})
486+
487+
return new EnqueuedTask(task)
488+
}
489+
436490
/**
437491
* Delete one document
438492
*

src/types/types.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,22 @@ export type DocumentOptions = {
201201
primaryKey?: string
202202
}
203203

204+
export const ContentTypeEnum: Readonly<Record<string, ContentType>> = {
205+
JSON: 'application/json',
206+
CSV: 'text/csv',
207+
NDJSON: 'application/x-ndjson',
208+
}
209+
210+
export type ContentType =
211+
| 'text/csv'
212+
| 'application/x-ndjson'
213+
| 'application/json'
214+
| 'PUT'
215+
216+
export type RawDocumentAdditionOptions = DocumentOptions & {
217+
csvDelimiter?: string
218+
}
219+
204220
export type DocumentsQuery<T = Record<string, any>> = ResourceQuery & {
205221
fields?: Fields<T>
206222
}

tests/raw_document.test.ts

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
import {
2+
clearAllIndexes,
3+
config,
4+
getClient,
5+
} from './utils/meilisearch-test-utils'
6+
import { TaskStatus, ContentTypeEnum } from '../src/types'
7+
8+
beforeEach(async () => {
9+
await clearAllIndexes(config)
10+
})
11+
12+
describe.each([{ permission: 'Master' }, { permission: 'Admin' }])(
13+
'Test on raw documents addition using `addDocumentsFromString`',
14+
({ permission }) => {
15+
test(`${permission} key: Add documents in CSV format`, async () => {
16+
const client = await getClient(permission)
17+
const data = `id,title,comment
18+
123,Pride and Prejudice, A great book
19+
546,Le Petit Prince,a french book`
20+
21+
const { taskUid } = await client
22+
.index('csv_index')
23+
.addDocumentsFromString(data, ContentTypeEnum.CSV)
24+
const task = await client.waitForTask(taskUid)
25+
26+
expect(task.details.receivedDocuments).toBe(2)
27+
expect(task.status).toBe(TaskStatus.TASK_SUCCEEDED)
28+
})
29+
30+
test(`${permission} key: Add documents in CSV format with custom primary key`, async () => {
31+
const client = await getClient(permission)
32+
const data = `name,title,comment
33+
123,Pride and Prejudice, A great book
34+
546,Le Petit Prince,a french book`
35+
36+
const { taskUid } = await client
37+
.index('csv_index')
38+
.addDocumentsFromString(data, ContentTypeEnum.CSV, {
39+
primaryKey: 'name',
40+
})
41+
const task = await client.waitForTask(taskUid)
42+
43+
expect(task.details.receivedDocuments).toBe(2)
44+
expect(task.status).toBe(TaskStatus.TASK_SUCCEEDED)
45+
})
46+
47+
test(`${permission} key: Add documents in CSV format with custom delimiter`, async () => {
48+
const client = await getClient(permission)
49+
const data = `name;title;comment
50+
123;Pride and Prejudice; A great book
51+
546;Le Petit Prince;a french book`
52+
53+
const { taskUid } = await client
54+
.index('csv_index')
55+
.addDocumentsFromString(data, ContentTypeEnum.CSV, {
56+
primaryKey: 'name',
57+
csvDelimiter: ';',
58+
})
59+
const task = await client.waitForTask(taskUid)
60+
61+
expect(task.details.receivedDocuments).toBe(2)
62+
expect(task.status).toBe(TaskStatus.TASK_SUCCEEDED)
63+
})
64+
65+
test(`${permission} key: Add documents in JSON lines format`, async () => {
66+
const client = await getClient(permission)
67+
const data = `{ "id": 123, "title": "Pride and Prejudice", "comment": "A great book" }
68+
{ "id": 456, "title": "Le Petit Prince", "comment": "A french book" }`
69+
70+
const { taskUid } = await client
71+
.index('jsonl_index')
72+
.addDocumentsFromString(data, ContentTypeEnum.NDJSON, {})
73+
const task = await client.waitForTask(taskUid)
74+
75+
expect(task.details.receivedDocuments).toBe(2)
76+
expect(task.status).toBe(TaskStatus.TASK_SUCCEEDED)
77+
})
78+
79+
test(`${permission} key: Add documents in JSON lines with custom primary key`, async () => {
80+
const client = await getClient(permission)
81+
const data = `{ "name": 123, "title": "Pride and Prejudice", "comment": "A great book" }
82+
{ "name": 456, "title": "Le Petit Prince", "comment": "A french book" }`
83+
84+
const { taskUid } = await client
85+
.index('jsonl_index')
86+
.addDocumentsFromString(data, ContentTypeEnum.NDJSON, {
87+
primaryKey: 'name',
88+
})
89+
const task = await client.waitForTask(taskUid)
90+
91+
expect(task.details.receivedDocuments).toBe(2)
92+
expect(task.status).toBe(TaskStatus.TASK_SUCCEEDED)
93+
})
94+
95+
test(`${permission} key: Add documents in JSON format`, async () => {
96+
const client = await getClient(permission)
97+
const data = `[{ "id": 123, "title": "Pride and Prejudice", "comment": "A great book" },
98+
{ "id": 456, "title": "Le Petit Prince", "comment": "A french book" }]`
99+
100+
const { taskUid } = await client
101+
.index('json_index')
102+
.addDocumentsFromString(data, ContentTypeEnum.JSON)
103+
const task = await client.waitForTask(taskUid)
104+
105+
expect(task.details.receivedDocuments).toBe(2)
106+
expect(task.status).toBe(TaskStatus.TASK_SUCCEEDED)
107+
})
108+
109+
test(`${permission} key: Add documents in JSON format with custom primary key`, async () => {
110+
const client = await getClient(permission)
111+
const data = `[{ "name": 123, "title": "Pride and Prejudice", "comment": "A great book" },
112+
{ "name": 456, "title": "Le Petit Prince", "comment": "A french book" }]`
113+
114+
const { taskUid } = await client
115+
.index('json_index')
116+
.addDocumentsFromString(data, ContentTypeEnum.JSON, {
117+
primaryKey: 'name',
118+
})
119+
const task = await client.waitForTask(taskUid)
120+
121+
expect(task.details.receivedDocuments).toBe(2)
122+
expect(task.status).toBe(TaskStatus.TASK_SUCCEEDED)
123+
})
124+
}
125+
)
126+
127+
describe.each([{ permission: 'Master' }, { permission: 'Admin' }])(
128+
'Test on raw documents update using `updateDocumentsFromString`',
129+
({ permission }) => {
130+
test(`${permission} key: Update documents in CSV format`, async () => {
131+
const client = await getClient(permission)
132+
const data = `id,title,comment
133+
123,Pride and Prejudice, A great book
134+
546,Le Petit Prince,a french book`
135+
136+
const { taskUid } = await client
137+
.index('csv_index')
138+
.updateDocumentsFromString(data, ContentTypeEnum.CSV)
139+
const task = await client.waitForTask(taskUid)
140+
141+
expect(task.details.receivedDocuments).toBe(2)
142+
expect(task.status).toBe(TaskStatus.TASK_SUCCEEDED)
143+
})
144+
145+
test(`${permission} key: Update documents in CSV format with custom primary key`, async () => {
146+
const client = await getClient(permission)
147+
const data = `name,title,comment
148+
123,Pride and Prejudice, A great book
149+
546,Le Petit Prince,a french book`
150+
151+
const { taskUid } = await client
152+
.index('csv_index')
153+
.updateDocumentsFromString(data, ContentTypeEnum.CSV, {
154+
primaryKey: 'name',
155+
})
156+
const task = await client.waitForTask(taskUid)
157+
158+
expect(task.details.receivedDocuments).toBe(2)
159+
expect(task.status).toBe(TaskStatus.TASK_SUCCEEDED)
160+
})
161+
162+
test(`${permission} key: Update documents in CSV format with custom delimiter`, async () => {
163+
const client = await getClient(permission)
164+
const data = `name;title;comment
165+
123;Pride and Prejudice; A great book
166+
546;Le Petit Prince;a french book`
167+
168+
const { taskUid } = await client
169+
.index('csv_index')
170+
.updateDocumentsFromString(data, ContentTypeEnum.CSV, {
171+
primaryKey: 'name',
172+
csvDelimiter: ';',
173+
})
174+
const task = await client.waitForTask(taskUid)
175+
176+
expect(task.details.receivedDocuments).toBe(2)
177+
expect(task.status).toBe(TaskStatus.TASK_SUCCEEDED)
178+
})
179+
180+
test(`${permission} key: Update documents in JSON lines format`, async () => {
181+
const client = await getClient(permission)
182+
const data = `{ "id": 123, "title": "Pride and Prejudice", "comment": "A great book" }
183+
{ "id": 456, "title": "Le Petit Prince", "comment": "A french book" }`
184+
185+
const { taskUid } = await client
186+
.index('jsonl_index')
187+
.updateDocumentsFromString(data, ContentTypeEnum.NDJSON)
188+
const task = await client.waitForTask(taskUid)
189+
190+
expect(task.details.receivedDocuments).toBe(2)
191+
expect(task.status).toBe(TaskStatus.TASK_SUCCEEDED)
192+
})
193+
194+
test(`${permission} key: Update documents in JSON lines with custom primary key`, async () => {
195+
const client = await getClient(permission)
196+
const data = `{ "name": 123, "title": "Pride and Prejudice", "comment": "A great book" }
197+
{ "name": 456, "title": "Le Petit Prince", "comment": "A french book" }`
198+
199+
const { taskUid } = await client
200+
.index('jsonl_index')
201+
.updateDocumentsFromString(data, ContentTypeEnum.NDJSON, {
202+
primaryKey: 'name',
203+
})
204+
const task = await client.waitForTask(taskUid)
205+
206+
expect(task.details.receivedDocuments).toBe(2)
207+
expect(task.status).toBe(TaskStatus.TASK_SUCCEEDED)
208+
})
209+
210+
test(`${permission} key: Update documents in JSON format`, async () => {
211+
const client = await getClient(permission)
212+
const data = `[{ "id": 123, "title": "Pride and Prejudice", "comment": "A great book" },
213+
{ "id": 456, "title": "Le Petit Prince", "comment": "A french book" }]`
214+
215+
const { taskUid } = await client
216+
.index('json_index')
217+
.updateDocumentsFromString(data, ContentTypeEnum.JSON, {})
218+
const task = await client.waitForTask(taskUid)
219+
220+
expect(task.details.receivedDocuments).toBe(2)
221+
expect(task.status).toBe(TaskStatus.TASK_SUCCEEDED)
222+
})
223+
224+
test(`${permission} key: Update documents in JSON format with custom primary key`, async () => {
225+
const client = await getClient(permission)
226+
const data = `[{ "name": 123, "title": "Pride and Prejudice", "comment": "A great book" },
227+
{ "name": 456, "title": "Le Petit Prince", "comment": "A french book" }]`
228+
229+
const { taskUid } = await client
230+
.index('json_index')
231+
.updateDocumentsFromString(data, ContentTypeEnum.JSON, {
232+
primaryKey: 'name',
233+
})
234+
const task = await client.waitForTask(taskUid)
235+
236+
expect(task.details.receivedDocuments).toBe(2)
237+
expect(task.status).toBe(TaskStatus.TASK_SUCCEEDED)
238+
})
239+
}
240+
)

0 commit comments

Comments
 (0)