Partial update a document

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 the doc merge and the ops list on the fresh base — safe because the rejected attempt wrote nothing. Set 0 to fail fast: a conflict then answers 409 version_conflict_exception for 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 _id field 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 the doc merge. 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": true fields — 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

OperatorFieldsEffectFast lane
setfield, valueSet the field to value, creating intermediate objects for dotted paths.Yes — top-level inplace field, numeric/boolean value
unsetfieldRemove the field. Missing fields are a no-op.Yes — top-level inplace field
set_if_absentfield, valueSet the field to value only when it is currently absent.No
renamefrom, toMove the value from one (dotted) path to another. Fails when from does not exist.No
current_datefieldSet the field to the current Unix time in milliseconds.No

Numeric

OperatorFieldsEffectFast lane
incrfield, valueAdd a numeric value (may be negative). A missing field starts from 0.Yes — integer field, integer value
decrfield, valueSubtract a numeric value (may be negative). A missing field starts from 0.No
mulfield, valueMultiply by a numeric value. A missing field stays 0.Yes — integer field, integer value
minfield, valueKeep the smaller of the current value and value.Yes — value in the field’s family
maxfield, valueKeep the larger of the current value and value.Yes — value in the field’s family
togglefieldFlip 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.

OperatorFieldsEffect
appendfield, valueAppend to the array (an array value appends its elements).
add_to_setfield, valueAppend unless an equal element already exists.
insertfield, index, valueInsert the element at index, clamped to the array length.
popfieldRemove the last element.
shiftfieldRemove the first element.
clearfieldSet the field to the empty array [].
slicefield, valueKeep the first value elements (value >= 0), or the last `
removefield, valueRemove every element equal to value (an array value removes any of its elements).
replacefield, valueReplace 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.

OperatorFieldsEffect
str_appendfield, valueAppend value to the end of the string.
str_prependfield, valuePrepend value to the beginning of the string.
str_replacefield, value, replacementReplace every occurrence of the value substring with replacement.
str_trimfieldTrim leading and trailing whitespace.
  • doc_as_upsert
    (Optional, boolean, default: false) When true and the document does not exist, the partial body is indexed as a brand-new document instead of returning 404. When false, 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: no doc body and no doc_as_upsert;
  • every field is a top-level field (dotted paths disqualify) declared with "inplace": true in 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 value matches 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;
  • incr and mul additionally require an integer-typed field and an integer value — float fields and float deltas fall back, and so does a negative delta on an unsigned field;
  • toggle additionally 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": true and updated through an eligible ops-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.
Calendar September 29, 2026
Edit Edit this page