Partial updates & concurrency

This section documents the current Solr line (10.0; 9.10.x the maintained 9.x branch), written and verified against the Apache Solr Reference Guide. No specific patch version is pinned. Some capabilities (the Solr Operator on Kubernetes, the package-manager ecosystem, Learning To Rank model training, and expert plugin development) are linked, not documented in depth.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Indexing & updates covers sending whole documents. This page covers changing a document you already indexed without resending every field, making that read-modify-write race-safe with version, modeling a parent/child document relationship in one block, dropping documents that are byte-for-byte (or field-for-field) duplicates of one already indexed, and the strategies for rebuilding an index once its schema or analysis changes.

Atomic (field-level) updates

An atomic update sends only the fields that changed, wrapped in a modifier, instead of the whole document. Solr reads the stored document, applies the modifiers, and re-indexes the result — the untouched fields must therefore be stored or hold docValues (a copyField destination is the one exception: it must be neither, since it is always recomputed from its source).

Modifier Effect

set

Replace the field’s value(s) with the given value(s); null or an empty list removes the field entirely.

add

Append value(s) to a multivalued field.

add-distinct

Append value(s) to a multivalued field, skipping any already present.

remove

Remove the given value(s) from a multivalued field.

removeregex

Remove every value in a multivalued field that matches a regular expression.

inc

Increment (or, with a negative operand, decrement) a numeric field by the given amount.

curl "http://localhost:8983/solr/books/update?commit=true" \
  -H 'Content-Type: application/json' \
  -d '[{
        "id": "1",
        "in_stock_i": {"inc": -1},
        "genre_ss": {"add-distinct": "classic"},
        "blurb_t": {"set": null}
      }]'
# https://solr.apache.org/guide/solr/latest/indexing-guide/partial-document-updates.html
// SolrJ: the same atomic update, built as a partial SolrInputDocument.
SolrInputDocument doc = new SolrInputDocument();
doc.addField("id", "1");
doc.setField("in_stock_i", Map.of("inc", -1));
doc.setField("genre_ss", Map.of("add-distinct", "classic"));
try (SolrClient client = new Http2SolrClient.Builder("http://localhost:8983/solr").build()) {
    client.add("books", doc);
    client.commit("books");
}

In-place updates: the fast path

Solr takes a shortcut — an in-place update — when every modified field is a non-indexed, non-stored, single-valued numeric docValues field updated with set or inc only: it patches the docValues entry directly instead of re-indexing the whole document (which would otherwise re-run analysis on every other field and write a brand-new Lucene document with a new internal id). in_stock_i above only qualifies for this path if it is defined that way in the schema; touching genre_ss or blurb_t in the same request forces the slower full-document path for the whole update. See Schema & fields for how a field’s indexed/stored/docValues flags are declared, and Partial Document Updates for the exact eligibility rules.

Optimistic concurrency control with version

Every indexed document carries a version field that Solr bumps on every write. Read it back with your document, then send it on the write: the shard applies the write only if the stored version still matches, otherwise it rejects the request with HTTP 409 — the same read-modify-retry contract as Elasticsearch’s if_seq_no/if_primary_term and Couchbase’s CAS token, applied to a version number instead.

version sent Required condition

(omitted)

No constraint — overwrite unconditionally.

0

No constraint, but explicit — equivalent to omitting it.

A number > 1

The document must exist and its stored version must equal this number.

1

The document must exist; any stored version is accepted.

A negative number

The document must not exist — a create-only insert.

# Read the current version alongside the document.
curl --get "http://localhost:8983/solr/books/select" \
  --data-urlencode 'q=id:1' --data-urlencode 'fl=id,in_stock_i,_version_'
# response includes: "_version_": 1727625600123456

# Apply only if nobody else has written id=1 since that version.
curl "http://localhost:8983/solr/books/update?commit=true" \
  -H 'Content-Type: application/json' \
  -d '[{"id": "1", "in_stock_i": 40, "_version_": 1727625600123456}]'
# Lost the race -> HTTP 409, error.msg contains "version conflict" -> re-read and retry.

# Create-only insert: fails with 409 if id=2 already exists.
curl "http://localhost:8983/solr/books/update?commit=true" \
  -H 'Content-Type: application/json' \
  -d '[{"id": "2", "title": "A Wizard of Earthsea", "_version_": -1}]'
# https://solr.apache.org/guide/solr/latest/indexing-guide/partial-document-updates.html
{
  "responseHeader": { "status": 409 },
  "error": {
    "msg": "version conflict for 1 expected=1727625600123456 actual=1727625601987654",
    "code": 409
  }
}

The client owns the retry loop: re-read the document (and its fresh version), re-apply the change, and re-send — bounded by a retry budget, the same shape as the mermaid flow on Elasticsearch’s optimistic-concurrency page. An atomic inc modifier sidesteps the loop entirely for a plain counter, since the increment is applied server-side against whatever value is currently stored, with no client-supplied version needed.

Nested (child) documents and block indexing

A nested (or child) document models a parent/child relationship — an order and its line items, a blog post and its comments — as one indexing unit, so a query can filter on child fields and return (or aggregate) the matching parents without a query-time join. Solr calls indexing a parent together with all of its children in the same request block indexing; the whole block is written, replaced, and deleted as a unit.

The schema needs an indexed, non-stored root field — Solr fills it in automatically with the root document’s own id on every document in the block — and an optional nest_path field that lets Solr reconstruct the exact nesting shape (rather than just a flat parent/children list) on re-fetch.

curl "http://localhost:8983/solr/books/schema" -H 'Content-Type: application/json' -d '{
  "add-field": {"name": "_root_", "type": "string", "indexed": true, "stored": false, "docValues": false},
  "add-field": {"name": "_nest_path_", "type": "_nest_path_"}
}'

Index children either as named pseudo-fields (each child keeps a role you can target directly) or as an anonymous childDocuments array (simpler, but the children are indistinguishable by role at query time):

[
  {
    "id": "order-1",
    "customer_s": "Le Guin",
    "items": [
      { "id": "order-1-item-1", "sku_s": "BOOK-042", "qty_i": 2 },
      { "id": "order-1-item-2", "sku_s": "BOOK-099", "qty_i": 1 }
    ]
  }
]
curl "http://localhost:8983/solr/books/update?commit=true" \
  -H 'Content-Type: application/json' -d @order-with-items.json
# https://solr.apache.org/guide/solr/latest/indexing-guide/indexing-nested-documents.html

Querying back across the block uses the Block Join query parsers — \{!child of=…​} to go from a matched parent down to its children, \{!parent which=…​} to go from a matched child back up to its parent — plus a [child] document transformer in fl to fold matching children back into the parent result. Query parsers covers both parsers' syntax in depth. Deleting or updating a parent document affects the whole block; splitting a shard that holds nested documents needs the block kept together, which Indexing Nested Documents covers along with the uniqueness rule every id in a block must still satisfy (every id, parent or child, is unique across the whole collection, not just within its block).

De-duplication: the signature update processor

The signature update request processor (URP) computes a hash — a signature — over one or more fields of each incoming document and either rejects the document as a duplicate or overwrites the prior one that shares the same signature, instead of indexing both. It is wired into an update request processor chain in solrconfig.xml, not passed as a request parameter:

<updateRequestProcessorChain name="dedupe">
  <processor class="solr.processor.SignatureUpdateProcessorFactory">
    <bool name="enabled">true</bool>
    <str name="signatureField">signature_s</str>
    <bool name="overwriteDupes">true</bool>
    <str name="fields">title,author</str>
    <str name="signatureClass">solr.processor.Lookup3Signature</str>
  </processor>
  <processor class="solr.LogUpdateProcessorFactory" />
  <processor class="solr.RunUpdateProcessorFactory" />
</updateRequestProcessorChain>

fields names what gets hashed (omit it to hash the whole document); signatureField is where the resulting hash is stored (declare it in the schema); overwriteDupes (default true) makes a duplicate replace the earlier document rather than fail; signatureClass picks the hash — Lookup3Signature (fast, order-sensitive over the listed fields), MD5Signature (a stronger, order-sensitive hash), or TextProfileSignature (near-duplicate text detection, tolerant of minor differences). Route writes through the chain with update.chain:

curl "http://localhost:8983/solr/books/update?update.chain=dedupe&commit=true" \
  -H 'Content-Type: application/json' \
  -d '[{"id": "3", "title": "The Left Hand of Darkness", "author": "Ursula K. Le Guin"}]'
# A second document with the same title+author overwrites this one instead of adding a duplicate.
# https://solr.apache.org/guide/solr/latest/indexing-guide/de-duplication.html

Reindexing strategies

A schema or solrconfig.xml change (a new field, a retyped analyzer, a different uniqueKey) only takes effect for documents indexed after the change — existing documents keep whatever the old configuration produced until they are reindexed. Solr’s own migration guidance is deliberately simple and leaves the actual document-fetch/resend loop to you or your indexing pipeline:

  • Delete everything and reindex from the source of truth — the simplest and safest option whenever the original data (a database, files, a message log) is still available: delete-by-query :, confirm the core’s data/index directory is empty, then re-run the same indexing job that built the collection the first time.

  • Index into a new collection, then alias — in SolrCloud, create a second collection with the new schema, reindex the same source data into it, then repoint a collection alias from the old collection’s name to the new one — zero downtime, and the old collection stays available as a rollback until you drop it. The REINDEXCOLLECTION collections-API command automates the copy from an existing Solr collection variant of this (rather than a source system) when the only thing that changed is server-side (schema, configset), not the source documents themselves.

# Simplest case: source data still lives outside Solr, so just clear and re-run indexing.
curl "http://localhost:8983/solr/books/update?commit=true" \
  -H 'Content-Type: application/json' -d '{"delete": {"query": "*:*"}}'
# ...then re-run whatever job originally populated "books".

# SolrCloud: copy an existing collection's documents into one built on a new configset.
curl "http://localhost:8983/solr/admin/collections?action=REINDEXCOLLECTION&name=books&target=books_v2&configName=books-v2-config"
# https://solr.apache.org/guide/solr/latest/indexing-guide/reindexing.html

Whichever strategy you use, an alias-based cutover is what lets a reindex run against a live, serving collection instead of requiring a maintenance window — Collections & configsets covers creating and retargeting aliases, and Indexing & updates covers the commit/soft-commit tuning that makes a bulk reindexing job itself fast.