Indexing & updates

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.

Every write to Solr — one document, a batch, or a delete — goes through the same /update request handler family and comes out the other side as a set of Lucene index operations sitting in the transaction log, invisible to search until a commit opens a new searcher over them. This page covers the update endpoints and document formats (JSON, XML, CSV, CBOR), the two ways documents actually get there in practice — the bin/solr post Post Tool and SolrJ’s SolrInputDocument — the update request processor (URP) chain every document passes through on the way in, rich-document extraction via Solr Cell/Apache Tika, hard vs. soft commits and the transaction log itself, RealTime Get, and where the (now out-of-core) Data Import Handler and external ETL/CDC pipelines fit into keeping Solr in sync with a system of record. Modifying a document that is already indexed — atomic/partial updates and optimistic concurrency with version — is Partial updates & concurrency; the commit and NRT model itself is introduced in Core concepts & architecture.

The /update request handler family

A single UpdateRequestHandler, registered at /update, accepts XML, JSON, CSV, or javabin and picks the right content-stream loader from the request’s Content-Type header. Two extra paths exist purely as a convenience so you don’t have to set that header by hand:

Path Content-Type it assumes

/update

Whatever Content-Type says (XML, JSON, CSV, or javabin).

/update/json

application/json — Solr-style JSON without setting the header.

/update/json/docs

application/json, but treats the body as arbitrary ("custom") JSON, mapped to fields with split/f params instead of Solr’s own document shape.

/update/csv

text/csv.

Every request can carry update commands, not just documents: add, delete (by id or by query), commit, optimize, and rollback. The JSON/XML/CSV shapes below are three ways to say the same thing; see Indexing with Update Handlers for the complete command reference.

JSON updates

The default and most idiomatic format. A bare JSON array is shorthand for add-ing each element; an object with add/delete/commit keys is the explicit command form.

[
  { "id": "1", "title": "The Left Hand of Darkness", "author": "Ursula K. Le Guin", "year_i": 1969 },
  { "id": "2", "title": "The Dispossessed", "author": "Ursula K. Le Guin", "year_i": 1974 }
]
curl "http://localhost:8983/solr/books/update" \
  -H 'Content-Type: application/json' \
  -d '[
        { "id": "1", "title": "The Left Hand of Darkness", "author": "Ursula K. Le Guin", "year_i": 1969 },
        { "id": "2", "title": "The Dispossessed", "author": "Ursula K. Le Guin", "year_i": 1974 }
      ]'

# Explicit command form: add with an overwrite/commitWithin option, plus a delete by id and by query.
curl "http://localhost:8983/solr/books/update" \
  -H 'Content-Type: application/json' \
  -d '{
        "add": { "doc": { "id": "3", "title": "A Wizard of Earthsea" }, "commitWithin": 5000 },
        "delete": { "id": "2" },
        "delete": { "query": "year_i:[* TO 1900]" }
      }'
# https://solr.apache.org/guide/solr/latest/indexing-guide/indexing-with-update-handlers.html

/update/json/docs accepts JSON that was never shaped for Solr at all — an arbitrary object or an array of them from some upstream system — and maps it to fields at request time with split (which part of the payload is one document) and f (source-path-to-field-name rules), so no reshaping step has to run before the document reaches Solr:

# Arbitrary/custom JSON: each element of the top-level array is one document,
# and every field name is used as-is (f=$FQN:/**).
curl "http://localhost:8983/solr/books/update/json/docs?split=/&f=id:/isbn&f=title:/name" \
  -H 'Content-Type: application/json' \
  -d '[ { "isbn": "978-0-575-07871-6", "name": "The Left Hand of Darkness" } ]'
# https://solr.apache.org/guide/solr/latest/indexing-guide/indexing-with-update-handlers.html

XML updates

The original format; still fully supported and occasionally the more convenient one for a system already producing XML. <field> names must match the schema exactly.

<add commitWithin="5000">
  <doc>
    <field name="id">4</field>
    <field name="title">The Word for World Is Forest</field>
    <field name="author">Ursula K. Le Guin</field>
    <field name="genre_ss">science fiction</field>
    <field name="genre_ss">anthropology</field>
  </doc>
</add>
curl "http://localhost:8983/solr/books/update" \
  -H 'Content-Type: application/xml' \
  --data-binary @book.xml
# https://solr.apache.org/guide/solr/latest/indexing-guide/indexing-with-update-handlers.html

CSV updates

/update/csv treats the header row as field names, one document per data row. Multivalued fields need an explicit separator, and per-column overrides (f.<field>.split, f.<field>.separator, literal values for a column not present in the file) live in query parameters.

curl "http://localhost:8983/solr/books/update/csv?commit=true&f.genre_ss.split=true&f.genre_ss.separator=%7C" \
  -H 'Content-Type: text/csv' \
  --data-binary $'id,title,genre_ss\n5,Rocannon'"'"'s World,science fiction|anthropology\n'
# https://solr.apache.org/guide/solr/latest/indexing-guide/indexing-with-update-handlers.html

CBOR

Solr also accepts CBOR (Content-Type: application/cbor) as a compact binary alternative to JSON on the same /update handler — same document shape as Solr-style JSON, just encoded as CBOR on the wire, which is worth it for large batches over a bandwidth-constrained link. javabin remains Solr’s own internal binary format and is what SolrJ uses by default regardless of which format your own client code sees.

The Post Tool (bin/solr post)

bin/solr post wraps the same /update handler for command-line and scripted loads — files, directories, a whole tree of mixed formats, standard input, raw content, or even a shallow website crawl — and is explicitly the tool for getting-started and ad hoc loading, not a production indexing pipeline (it has no retry/backpressure handling of its own).

# A directory of JSON/XML/CSV files, guessing content type from the extension.
bin/solr post -c books example/exampledocs/

# One or more specific files.
bin/solr post -c books data/*.json

# Rich documents (PDF, Word, ...) routed through Solr Cell/Tika -- see below.
bin/solr post -c books -params "literal.source=upload" docs/*.pdf

# Raw content piped in, with an explicit type.
cat book.json | bin/solr post -c books -type application/json -out -
# https://solr.apache.org/guide/solr/latest/indexing-guide/post-tool.html

Getting Started’s first round-trip uses plain curl for the single-document case and the Post Tool for bulk sample data — that split holds across this section.

Adding documents from SolrJ

SolrJ builds a SolrInputDocument field-by-field and sends it with SolrClient.add, which is the programmatic equivalent of a JSON/XML add command — no HTTP body to construct by hand, and errors come back as a typed SolrServerException/SolrException instead of a status code to parse.

SolrInputDocument doc = new SolrInputDocument();
doc.addField("id", "6");
doc.addField("title", "The Telling");
doc.addField("author", "Ursula K. Le Guin");
doc.addField("genre_ss", "science fiction");   // repeat addField for a multivalued field

try (SolrClient client = new Http2SolrClient.Builder("http://localhost:8983/solr").build()) {
    client.add("books", doc, 5000);   // commitWithin=5000ms; omit to rely on autoCommit/autoSoftCommit
    // Batch: client.add("books", listOfSolrInputDocuments);
}

Batching many documents into one add call amortizes the network round trip the same way the _bulk API does for Elasticsearch; a List<SolrInputDocument> overload of add takes the whole batch in one request.

Update request processor (URP) chains

Every update request — however it arrived, over any of the endpoints above — runs through an ordered update request processor (URP) chain before it is applied: a pipeline of small, composable steps such as logging, distributing the write to the right shard/replica, deduplicating, adding a field, or rejecting a malformed document. The default chain (LogUpdateProcessorFactoryDistributedUpdateProcessorFactoryRunUpdateProcessorFactory) is what actually performs the add; a custom chain, named in solrconfig.xml, inserts extra processors before that point.

<!-- solrconfig.xml: a chain that stamps an ingest timestamp and drops a scratch field. -->
<updateRequestProcessorChain name="add-timestamp-and-clean">
  <processor class="solr.TimestampUpdateProcessorFactory">
    <str name="fieldName">indexed_at</str>
  </processor>
  <processor class="solr.RemoveBlankFieldUpdateProcessorFactory"/>
  <processor class="solr.RunUpdateProcessorFactory"/>
</updateRequestProcessorChain>
# Route this request through the named chain instead of the default one.
curl "http://localhost:8983/solr/books/update?update.chain=add-timestamp-and-clean&commit=true" \
  -H 'Content-Type: application/json' \
  -d '[{ "id": "7", "title": "Four Ways to Forgiveness" }]'
# https://solr.apache.org/guide/solr/latest/configuration-guide/update-request-processors.html

A chain can also be built on the fly from request parameters instead of being predeclared, which is useful for a one-off backfill that needs a processor no permanent chain has. See Update Request Processors for the full factory catalogue — field mutation, deduplication, language detection, and script-based processors among them.

Rich documents: Solr Cell and Apache Tika

Everything above assumes the caller already has field-shaped data. For binary formats — PDF, Word, Excel, and the rest — the ExtractingRequestHandler ("Solr Cell") sends the file to an Apache Tika server, which extracts text and metadata and hands back XHTML that Solr Cell maps onto fields, same as any other add.

curl "http://localhost:8983/solr/books/update/extract?literal.id=doc-1&commit=true" \
  -F "myfile=@report.pdf"
# literal.id supplies the uniqueKey since a PDF has no natural "id" field.
# https://solr.apache.org/guide/solr/latest/indexing-guide/indexing-with-tika.html

fmap.<source>=<field> renames an extracted field, uprefix=ignored_ catches everything not explicitly mapped instead of erroring on an unknown field, and capture pulls specific embedded elements out separately. See Indexing with Solr Cell and Apache Tika for the parameter reference, running the Tika server, and encrypted-document handling. Solr Cell is meant for moderate document volumes at index time, not as a general-purpose document-conversion service for the rest of your application.

Data Import Handler: now a separate community package

The Data Import Handler (DIH) — the old built-in tool for pulling documents straight from a JDBC database, a flat file, or an XML/HTTP source on a schedule — was removed from Solr’s core distribution (Solr 9.0) and is not bundled with Solr anymore. It survives only as a separately-installed, community-maintained package: install it explicitly (through Solr’s package manager or by dropping its jar on the classpath) before referencing a DIH handler in solrconfig.xml, and treat it as an optional add-on with its own release cadence and support, not a guaranteed-present core feature. Do not assume a fresh Solr install can run a DIH data-config.xml out of the box.

Keeping Solr in sync with a system of record: external ETL/CDC

With DIH no longer built in, the common pattern for keeping Solr’s index current with a database (or another system of record) is an external pipeline: a scheduled batch job, a change-data-capture (CDC) stream (e.g. Debezium reading a database’s replication log), or a message-queue consumer that translates each insert/update/delete into a call against the endpoints on this page — SolrInputDocument`s over SolrJ, or JSON/CSV over `/update — rather than a Solr-embedded pull job. This mirrors how the same problem is solved for Elasticsearch’s own ingest layer: business logic and source-system access live in a general-purpose tool outside the search engine, and Solr’s own job stays limited to receiving well-formed documents and answering queries fast. Choose commitWithin (a single add) or a scheduled autoSoftCommit (a steady stream) so the sync job does not have to manage commits itself — see the next section.

Commits: hard vs. soft

A document is durable-in-the-log the moment /update accepts it, but invisible to search until a commit opens a new (or reopened) searcher. Solr has two kinds, and they answer different questions: "is this safe on disk" and "can a query see it yet."

Hard commit Soft commit

Triggered by

commit=true, client.commit(), or the periodic autoCommit

softCommit=true, client.commit(true, true), or the periodic autoSoftCommit

Durability

fsyncs segment files and truncates the transaction log — the durability boundary

No fsync — durability still depends on the transaction log, not this operation

Visibility

Makes the write searchable if openSearcher is not disabled

Always opens a new searcher — this is the mechanism behind near-real-time (NRT) search

Cost

Relatively expensive; too frequent hurts throughput and cache reuse

Cheap; still not free — every one opens a new searcher and can invalidate caches

# Hard commit on this write: fsync + truncate the tlog + open a new searcher.
curl "http://localhost:8983/solr/books/update?commit=true" \
  -H 'Content-Type: application/json' \
  -d '[{ "id": "8", "title": "Planet of Exile" }]'

# Soft commit only: visible to search now, but not yet fsynced.
curl "http://localhost:8983/solr/books/update?softCommit=true" \
  -H 'Content-Type: application/json' \
  -d '[{ "id": "9", "title": "City of Illusions" }]'

autoCommit, autoSoftCommit, and commitWithin

Explicit commit=true on every write does not scale — it serializes writers behind one expensive operation. The usual setup instead configures both automatic intervals in solrconfig.xml and lets individual writes skip the parameter entirely:

<updateHandler class="solr.DirectUpdateHandler2">
  <!-- Hard commit at most every 60s (or 100k docs), without opening a searcher itself. -->
  <autoCommit>
    <maxTime>60000</maxTime>
    <maxDocs>100000</maxDocs>
    <openSearcher>false</openSearcher>
  </autoCommit>

  <!-- Soft commit every second: the actual NRT visibility knob. -->
  <autoSoftCommit>
    <maxTime>1000</maxTime>
  </autoSoftCommit>
</updateHandler>

openSearcher=false on autoCommit is the standard pairing: the hard commit still fsyncs and truncates the log on schedule, but leaves opening a new searcher (the relatively expensive part) to autoSoftCommit, so durability and visibility are tuned independently. A per-write commitWithin (the JSON/SolrJ examples above both used it) asks Solr to guarantee visibility within that many milliseconds without the caller deciding hard vs. soft itself — Solr picks a soft commit internally unless configured otherwise. In a fresh, unconfigured collection none of autoCommit, autoSoftCommit, or commitWithin is set, so a plain add sits invisible indefinitely until some commit is issued. See Commits and Transaction Logs for the full parameter set, including commit event listeners.

The transaction log

The transaction log (tlog) is a durable, append-only record of every add/delete Solr has accepted since the last hard commit — the write-ahead log that makes a hard commit’s fsync safe to defer and is what a recovering or newly-added replica replays to catch up without a full index copy.

<updateHandler class="solr.DirectUpdateHandler2">
  <updateLog>
    <str name="dir">${solr.ulog.dir:}</str>
  </updateLog>
</updateHandler>

A hard commit truncates the tlog once its contents are safely in fsynced segments; a soft commit does not touch it at all, which is exactly why soft commits alone are not a durability guarantee — an unclean shutdown between hard commits replays whatever the tlog still holds on restart. See Commits and Transaction Logs for tlog sizing (numRecordsToKeep, maxNumLogsToKeep) and replica-recovery behavior.

RealTime Get

GET /solr/<collection>/get?id=<id> answers "what is the current value of this document right now," reading straight from the transaction log (or the in-memory update log) when the document has not reached a searcher yet, and from the index otherwise — so it sees a write a /select query issued the same instant would not. It needs no commit at all.

curl "http://localhost:8983/solr/books/get?id=8"
curl "http://localhost:8983/solr/books/get?ids=8,9"
# https://solr.apache.org/guide/solr/latest/configuration-guide/realtime-get.html

RealTime Get is what lets an application use Solr like a key-value store for "read your own write" checks even while running commits on a relaxed schedule — and it is also load-bearing for SolrCloud itself: disabling the /get handler forces every leader election and replica recovery into a full index copy instead of a partial sync, so leave it enabled. See RealTime Get for the handler configuration and its interaction with atomic updates, covered in depth on Partial updates & concurrency.

Continue with Partial updates & concurrency for atomic field updates and optimistic concurrency on documents already in the index, or Indexing internals & performance for tuning bulk-load throughput.