Add or replace documents
- Add or replace documents
Meilisearch provides three document operations: add or replace, add or update, and delete. This guide explains the difference between each operation and when to use them.
Add or replace documents
Use POST /indexes/{index_uid}/documents to add new documents or replace existing ones. If a document with the same primary key already exists, Meilisearch replaces the entire document with the new version.
cURL
JS
Python
PHP
Java
Ruby
Go
C#
Rust
Swift
Dart
curl \
-X POST 'MEILISEARCH_URL/indexes/movies/documents' \
-H 'Authorization: Bearer API_KEY' \
-H 'Content-Type: application/json' \
--data-binary '[
{
"id": 287947,
"title": "Shazam",
"poster": "https://image.tmdb.org/t/p/w1280/xnopI5Xtky18MPhK40cZAGAOVeV.jpg",
"overview": "A boy is given the ability to become an adult superhero in times of need with a single magic word.",
"release_date": "2019-03-23"
}
]'
client.index('movies').addDocuments([{
id: 287947,
title: 'Shazam',
poster: 'https://image.tmdb.org/t/p/w1280/xnopI5Xtky18MPhK40cZAGAOVeV.jpg',
overview: 'A boy is given the ability to become an adult superhero in times of need with a single magic word.',
release_date: '2019-03-23'
}], { skipCreation: true })
client.index('movies').add_documents([{
'id': 287947,
'title': 'Shazam',
'poster': 'https://image.tmdb.org/t/p/w1280/xnopI5Xtky18MPhK40cZAGAOVeV.jpg',
'overview': 'A boy is given the ability to become an adult superhero in times of need with a single magic word.',
'release_date': '2019-03-23'
}], skip_creation=True)
$client->index('movies')->addDocuments([
[
'id' => 287947,
'title' => 'Shazam',
'poster' => 'https://image.tmdb.org/t/p/w1280/xnopI5Xtky18MPhK40cZAGAOVeV.jpg',
'overview' => 'A boy is given the ability to become an adult superhero in times of need with a single magic word.',
'release_date' => '2019-03-23'
]
]);
client.index("movies").addDocuments("[{"
+ "\"id\": 287947,"
+ "\"title\": \"Shazam\","
+ "\"poster\": \"https://image.tmdb.org/t/p/w1280/xnopI5Xtky18MPhK40cZAGAOVeV.jpg\","
+ "\"overview\": \"A boy is given the ability to become an adult superhero in times of need with a single magic word.\","
+ "\"release_date\": \"2019-03-23\""
+ "}]"
);
client.index('movies').add_documents([
{
id: 287947,
title: 'Shazam',
poster: 'https://image.tmdb.org/t/p/w1280/xnopI5Xtky18MPhK40cZAGAOVeV.jpg',
overview: 'A boy is given the ability to become an adult superhero in times of need with a single magic word.',
release_date: '2019-03-23'
}
])
documents := []map[string]interface{}{
{
"id": 287947,
"title": "Shazam",
"poster": "https://image.tmdb.org/t/p/w1280/xnopI5Xtky18MPhK40cZAGAOVeV.jpg",
"overview": "A boy is given the ability to become an adult superhero in times of need with a single magic word.",
"release_date": "2019-03-23",
},
}
options := &meilisearch.DocumentOptions{SkipCreation: false}
client.Index("movies").AddDocuments(documents, options)
var movie = new[]
{
new Movie
{
Id = "287947",
Title = "Shazam",
Poster = "https://image.tmdb.org/t/p/w1280/xnopI5Xtky18MPhK40cZAGAOVeV.jpg",
Overview = "A boy is given the ability to become an adult superhero in times of need with a single magic word.",
ReleaseDate = "2019-03-23"
}
};
await index.AddDocumentsAsync(movie);
let task: TaskInfo = client
.index("movies")
.add_or_replace(&[
Movie {
id: 287947,
title: "Shazam".to_string(),
poster: "https://image.tmdb.org/t/p/w1280/xnopI5Xtky18MPhK40cZAGAOVeV.jpg".to_string(),
overview: "A boy is given the ability to become an adult superhero in times of need with a single magic word.".to_string(),
release_date: "2019-03-23".to_string(),
}
], None)
.await
.unwrap();
let documentJsonString = """
[
{
"reference_number": 287947,
"title": "Shazam",
"poster": "https://image.tmdb.org/t/p/w1280/xnopI5Xtky18MPhK40cZAGAOVeV.jpg",
"overview": "A boy is given the ability to become an adult superhero in times of need with a single magic word.",
"release_date": "2019-03-23"
}
]
"""
let documents: Data = documentJsonString.data(using: .utf8)!
client.index("movies").addDocuments(documents: documents) { (result) in
switch result {
case .success(let task):
print(task)
case .failure(let error):
print(error)
}
}
await client.index('movies').addDocuments([
{
'id': 287947,
'title': 'Shazam',
'poster':
'https://image.tmdb.org/t/p/w1280/xnopI5Xtky18MPhK40cZAGAOVeV.jpg',
'overview':
'A boy is given the ability to become an adult superhero in times of need with a single magic word.',
'release_date': '2019-03-23'
}
]);
This operation is best when you have complete document objects and want to ensure the stored version matches exactly what you send.
When replacing a document, any fields present in the old version but missing from the new version are removed. Always include all fields you want to keep.
Example
Suppose your index contains this document:
{
"id": 287947,
"title": "Shazam",
"overview": "A boy becomes a superhero.",
"genres": ["Action", "Comedy"]
}
If you send a POST request with:
{
"id": 287947,
"title": "Shazam!",
"overview": "A boy is given the ability to become an adult superhero."
}
The stored document becomes:
{
"id": 287947,
"title": "Shazam!",
"overview": "A boy is given the ability to become an adult superhero."
}
The genres field is gone because it was not included in the replacement.
Add or update documents
Use PUT /indexes/{index_uid}/documents to add new documents or partially update existing ones. If a document with the same primary key already exists, Meilisearch merges the new fields into the existing document. Fields not included in the update remain unchanged. Partial updates apply only to top-level fields: updating an object field replaces the entire object, removing any omitted subfields.
cURL
JS
Python
PHP
Java
Ruby
Go
C#
Rust
Swift
Dart
curl \
-X PUT 'MEILISEARCH_URL/indexes/movies/documents' \
-H 'Authorization: Bearer API_KEY' \
-H 'Content-Type: application/json' \
--data-binary '[
{
"id": 287947,
"title": "Shazam ⚡️",
"genres": "comedy"
}
]'
client.index('movies').updateDocuments([{
id: 287947,
title: 'Shazam ⚡️',
genres: 'comedy'
}], { skipCreation: true })
client.index('movies').update_documents([{
'id': 287947,
'title': 'Shazam ⚡️',
'genres': 'comedy'
}], skip_creation=True)
$client->index('movies')->updateDocuments([
[
'id' => 287947,
'title' => 'Shazam ⚡️',
'genres' => 'comedy'
]
]);
client.index("movies").updateDocuments("[{
+ "\"id\": 287947,"
+ "\"title\": \"Shazam ⚡️\","
+ "\"genres\": \"comedy\""
+ "}]"
);
client.index('movies').update_documents([
{
id: 287947,
title: 'Shazam ⚡️',
genres: 'comedy'
}
])
documents := []map[string]interface{}{
{
"id": 287947,
"title": "Shazam ⚡️",
"genres": "comedy",
},
}
options := &meilisearch.DocumentOptions{SkipCreation: true}
client.Index("movies").UpdateDocuments(documents, options)
var movie = new[]
{
new Movie { Id = "287947", Title = "Shazam ⚡️", Genres = "comedy" }
};
await index.UpdateDocumentsAsync(movie);
// Define the type of our documents
#[derive(Serialize, Deserialize)]
struct IncompleteMovie {
id: usize,
title: String,
genres: String
}
let task: TaskInfo = client
.index("movies")
.add_or_update(&[
IncompleteMovie {
id: 287947,
title: "Shazam ⚡️".to_string(),
genres: "comedy".to_string()
}
], None)
.await
.unwrap();
let documentJsonString = """
[
{
"reference_number": 287947,
"title": "Shazam ⚡️",
"genres": "comedy"
}
]
"""
let documents: Data = documentJsonString.data(using: .utf8)!
client.index("movies").updateDocuments(documents: documents) { (result) in
switch result {
case .success(let task):
print(task)
case .failure(let error):
print(error)
}
}
await client.index('movies').updateDocuments([
{
'id': 287947,
'title': 'Shazam ⚡️',
'genres': 'comedy',
}
]);
This operation is ideal when you only need to change specific fields without resending the entire document.
Example
Starting with the same document:
{
"id": 287947,
"title": "Shazam",
"overview": "A boy becomes a superhero.",
"genres": ["Action", "Comedy"]
}
If you send a PUT request with:
{
"id": 287947,
"title": "Shazam ⚡️",
"genres": "comedy"
}
The stored document becomes:
{
"id": 287947,
"title": "Shazam ⚡️",
"overview": "A boy becomes a superhero.",
"genres": "comedy"
}
The overview field is preserved because the update only touched title and genres.
Delete documents
Use DELETE /indexes/{index_uid}/documents/{document_id} to remove a single document by its primary key:
cURL
JS
Python
PHP
Java
Ruby
Go
C#
Rust
Swift
Dart
curl \
-X DELETE 'MEILISEARCH_URL/indexes/movies/documents/25684' \
-H 'Authorization: Bearer API_KEY'
client.index('movies').deleteDocument(25684)
client.index('movies').delete_document(25684)
$client->index('movies')->deleteDocument(25684);
client.index("movies").deleteDocument("25684");
client.index('movies').delete_document(25684)
client.Index("movies").DeleteDocument("25684")
await client.Index("movies").DeleteOneDocumentAsync("25684");
let task: TaskInfo = client
.index("movies")
.delete_document(25684)
.await
.unwrap();
client.index("movies").deleteDocument("25684") { (result) in
switch result {
case .success(let task):
print(task)
case .failure(let error):
print(error)
}
}
await client.index('movies').deleteDocument(25684);
Meilisearch also supports batch deletion and deletion by filter:
- Delete by batch: send a
POST /indexes/{index_uid}/documents/delete-batchrequest with an array of document IDs - Delete by filter: send a
POST /indexes/{index_uid}/documents/deleterequest with a filter expression to remove all matching documents
Choosing the right operation
| Operation | HTTP method | Behavior | Use when |
|---|---|---|---|
| Add or replace | POST |
Replaces entire document | You have complete documents and want exact control |
| Add or update | PUT |
Merges fields into existing document | You only need to change specific fields |
| Delete | DELETE |
Removes document entirely | You need to remove documents from the index |
Batch operations
All three operations support sending multiple documents in a single request. Send an array of documents in the request body:
curl \
-X POST 'MEILISEARCH_URL/indexes/movies/documents' \
-H 'Content-Type: application/json' \
--data-binary '[
{ "id": 1, "title": "Movie One" },
{ "id": 2, "title": "Movie Two" },
{ "id": 3, "title": "Movie Three" }
]'
Batch operations are processed as a single task. Meilisearch handles large batches efficiently, so prefer sending documents in bulk rather than one at a time.
Update without creating new documents
By default, both POST and PUT document operations create new documents if no document with the given primary key exists. To change this behavior, add the skipCreation=true query parameter to your request. When enabled, Meilisearch silently ignores any documents whose primary key does not match an existing document in the index.
curl \
-X POST 'MEILISEARCH_URL/indexes/movies/documents?skipCreation=true' \
-H 'Content-Type: application/json' \
--data-binary '[
{ "id": 1, "title": "Updated Title" },
{ "id": 99999, "title": "This document does not exist" }
]'
In this example, only document 1 is updated. Document 99999 is ignored because it does not already exist in the index.
This is useful when you want to safely update fields for existing documents without accidentally creating incomplete records.
Retrieve multiple documents by ID
Use POST /indexes/{index_uid}/documents/fetch to retrieve specific documents by their primary keys:
curl \
-X POST 'MEILISEARCH_URL/indexes/movies/documents/fetch' \
-H 'Content-Type: application/json' \
--data-binary '{
"ids": ["id1", "id2", "id3"]
}'
Meilisearch returns the matching documents in the results array. Note that documents are not returned in the order you queried them, and non-existent IDs are silently ignored.
Prefer POST /indexes/{index_uid}/documents/fetch over GET /indexes/{index_uid}/documents. The GET variant is discouraged unless you have a specific reason to use it (for example, to take advantage of HTTP caching at the proxy or CDN level). The GET route accepts fewer parameters and only supports string filter expressions, while the POST route accepts the richer JSON body used throughout this guide.
Filter the documents you fetch
You can pass a filter expression to POST /indexes/{index_uid}/documents/fetch to retrieve only documents that match a condition:
cURL
curl \
-X POST 'MEILISEARCH_URL/indexes/movies/documents/fetch' \
-H 'Authorization: Bearer API_KEY' \
-H 'Content-Type: application/json' \
--data-binary '{
"filter": "genres = Action AND rating > 8"
}'
Any attribute you reference in a document filter must first be declared in the index’s filterableAttributes setting. This rule is the same as for search filters and is specific to the documents endpoint when filtering the documents you retrieve or delete.
Supported content types
By default, Meilisearch expects a JSON array in the request body and the Content-Type: application/json header. The documents endpoint also accepts NDJSON (application/x-ndjson) and CSV (text/csv) payloads when you set the matching header.
cURL
curl \
-X POST 'MEILISEARCH_URL/indexes/movies/documents' \
-H 'Authorization: Bearer API_KEY' \
-H 'Content-Type: text/csv' \
--data-binary @movies.csv
cURL
curl \
-X POST 'MEILISEARCH_URL/indexes/movies/documents' \
-H 'Authorization: Bearer API_KEY' \
-H 'Content-Type: application/x-ndjson' \
--data-binary @movies.ndjson
When uploading CSV data, you can override the default comma separator with the csvDelimiter query parameter (for example, ?csvDelimiter=;).
csvDelimiter is only valid when the request uses Content-Type: text/csv. Passing it alongside a JSON or NDJSON payload returns an error.
Next steps
Documents API reference
Full API reference for document operations
Indexing overview
Learn more about how indexing works in Meilisearch
Monitor tasks
Track the status of your document operations
