Partial update a document #
Sometimes we may only need to update a portion of the fields of a document.
A partial update fetches the stored document, merges the partial doc body,
applies the ordered ops operators, and replaces the document in place under
its original _id — the new values are visible to search immediately.
Examples #
Update the org.id field of the document with system id 0,0 in the
collection my-collection:
POST /my-collection/_update/0,0
{
"doc": {
"org": {
"id": "infinilabs"
}
}
}
The API returns the following result:
{
"_collection": "default:my-collection",
"_id": "0,0",
"_key": "news_001",
"result": "updated",
"_shards": { "total": 1, "successful": 1, "failed": 0 }
}
For requests that carry ops, the response also reports what was applied:
applied_ops (the number of operators) and ops (a one-liner per applied
operator, in order).
Atomic-style operators are sent as an ordered ops list; each entry addresses
a (dotted) field path and each operator sees the effect of the previous one:
POST /my-collection/_update/0,0
{
"ops": [
{ "op": "incr", "field": "views", "value": 1 },
{ "op": "min", "field": "price", "value": 9.9 },
{ "op": "toggle", "field": "active" },
{ "op": "append", "field": "tags", "value": ["hot", "new"] },
{ "op": "str_append", "field": "title", "value": " (updated)" },
{ "op": "unset", "field": "status" }
]
}
The first failing operator aborts the whole update before any write happens, so the document is never left half-transformed.
The document keeps its _id across partial updates. Fields defined with
"inplace": true in the schema (see
Field parameters)
are applied as O(1) in-place column writes; an update request that mixes
inplace fields with regular fields is applied in one atomic step, with every
field kind seeing its new value. The O(1) fast lane is operator-dependent —
the Fast lane column in the tables below marks exactly which operators
qualify.
Request #
POST /<target>/_update/<doc_id>?retry_on_conflict=<n>
{
"doc": {<fields>},
"ops": [<operator>],
"doc_as_upsert": false
}
Query parameters #
retry_on_conflict
(Optional, integer, default:3) How many times to retry the update when the document is concurrently modified between this request’s fetch and its write (optimistic concurrency control). Each retry re-fetches the document and re-applies thedocmerge and theopslist on the fresh base — safe because the rejected attempt wrote nothing. Set0to fail fast: a conflict then answers409 version_conflict_exceptionfor the client to handle.
Path parameters #
<target>
(Required, string) Name of the collection to target.<doc_id>
(Required, string) The system id of this document, in the<rolling>,<seq>form (e.g.0,1), as returned in the_idfield of index/search responses.
Request body #
doc
(Optional, object) Partial document body, deep-merged into the stored source: nested objects merge recursively, scalars and arrays are replaced.ops
(Optional, array) Ordered list of update operators, applied after thedocmerge. Operators are applied in order; a field may appear in more than one operator and the effects compose sequentially. Supported operators — the Fast lane column marks the operators that can be applied as O(1) in-place column writes on"inplace": truefields — every unmarked operator, and any request shape the lane rejects, silently takes the read-modify-write path instead (see In-place fast lane eligibility):
General
| Operator | Fields | Effect | Fast lane |
|---|---|---|---|
set | field, value | Set the field to value, creating intermediate objects for dotted paths. | Yes — top-level inplace field, numeric/boolean value |
unset | field | Remove the field. Missing fields are a no-op. | Yes — top-level inplace field |
set_if_absent | field, value | Set the field to value only when it is currently absent. | No |
rename | from, to | Move the value from one (dotted) path to another. Fails when from does not exist. | No |
current_date | field | Set the field to the current Unix time in milliseconds. | No |
Numeric
| Operator | Fields | Effect | Fast lane |
|---|---|---|---|
incr | field, value | Add a numeric value (may be negative). A missing field starts from 0. | Yes — integer field, integer value |
decr | field, value | Subtract a numeric value (may be negative). A missing field starts from 0. | No |
mul | field, value | Multiply by a numeric value. A missing field stays 0. | Yes — integer field, integer value |
min | field, value | Keep the smaller of the current value and value. | Yes — value in the field’s family |
max | field, value | Keep the larger of the current value and value. | Yes — value in the field’s family |
toggle | field | Flip a boolean field. A missing field is treated as false. | Yes — boolean field |
Array
Every array operator views the current field the same way: a missing field
starts as an empty array, and a scalar value is promoted to a one-element
array first. None of the array operators are fast-lane eligible — they
always take the read-modify-write path, even on inplace fields.
| Operator | Fields | Effect |
|---|---|---|
append | field, value | Append to the array (an array value appends its elements). |
add_to_set | field, value | Append unless an equal element already exists. |
insert | field, index, value | Insert the element at index, clamped to the array length. |
pop | field | Remove the last element. |
shift | field | Remove the first element. |
clear | field | Set the field to the empty array []. |
slice | field, value | Keep the first value elements (value >= 0), or the last ` |
remove | field, value | Remove every element equal to value (an array value removes any of its elements). |
replace | field, value | Replace the whole array with value (a scalar becomes a one-element array). |
String
String operators require a string-typed field; a missing field starts from the empty string. String operators are never fast-lane eligible — they always take the read-modify-write path.
| Operator | Fields | Effect |
|---|---|---|
str_append | field, value | Append value to the end of the string. |
str_prepend | field, value | Prepend value to the beginning of the string. |
str_replace | field, value, replacement | Replace every occurrence of the value substring with replacement. |
str_trim | field | Trim leading and trailing whitespace. |
doc_as_upsert
(Optional, boolean, default:false) Whentrueand the document does not exist, the partial body is indexed as a brand-new document instead of returning404. Whenfalse, a missing document fails the request.
In-place fast lane eligibility #
Exactly seven operators can take the O(1) in-place fast lane: set, unset,
incr, mul, min, max, and toggle. The whole request takes the fast
lane only when every condition below holds; otherwise it silently falls back
to the read-modify-write path, with the same end result:
- the request is
ops-only: nodocbody and nodoc_as_upsert; - every
fieldis a top-level field (dotted paths disqualify) declared with"inplace": truein the schema and typed numeric or boolean — see Field parameters for how to declare the parameter and for the column semantics (visibility, missing-field defaults, integer overflow wrapping); - at most one operator per field — a repeated field needs sequential composition;
- each
valuematches its field’s family: an integer for integer fields, a non-negative integer for unsigned fields, a number for float fields, a boolean for boolean fields; incrandmuladditionally require an integer-typed field and an integervalue— float fields and float deltas fall back, and so does a negative delta on an unsigned field;toggleadditionally requires a boolean-typed field.
decr, set_if_absent, rename, current_date, and every array and string
operator never take the fast lane. On the read-modify-write path they still
update inplace fields correctly — the merged document’s write-back updates
the columns — they just pay the fetch/replace cost and the optimistic-
concurrency retries.
Concurrency semantics #
The read-modify-write path enforces optimistic concurrency control: the
fetch reads the document together with its version, and the write back is a
conditional replace the engine rejects when a concurrent write — another
partial update, a keyed re-index, or an inplace column update — advanced the
version in between. A rejected attempt wrote nothing, so the request retries
(re-fetch, re-merge, re-apply) up to retry_on_conflict times; an exhausted
conflict answers:
{
"error": {
"type": "version_conflict_exception",
"reason": "document [0,5] version conflict, expected [7], current [9]: ..."
}
}
Two notes:
- Fields declared with
"inplace": trueand updated through an eligibleops-only request (see In-place fast lane eligibility) take the fast lane instead, which applies its operators atomically inside the engine — no fetch/replace race exists there and no retrying is needed. Inplace values become visible the moment the column write lands — search snapshots pin the document set, not the values. - A partial update forwarded to a primary on another node carries the fetched version over the RPC as well — the conditional gate is enforced on the primary’s node either way. (During a mixed-version rolling upgrade, an older primary ignores the version and the update runs ungated there.)
- Fast-lane inplace updates replicate at the value level: the primary ships each operator’s resulting absolute values to the replicas, which apply them directly to their own columns. Delivery is idempotent — a duplicate replay of the same values is a no-op — and a replica that cannot take the fast lane falls back to applying the update through its own engine.