perf(indexing): soft-commit instead of hard-commit, keeping documents searchable - #196
adityamparikh wants to merge 5 commits into
Conversation
The mock returns null without it, and a strict stub on the one-argument commit would be flagged unnecessary once apache#196's soft commit lands. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiUHyyXLTo9ATdgg8eRFZJ Signed-off-by: Aditya Parikh <aditya.m.parikh@gmail.com>
Solr already parses CSV and its own update XML. The server's CSV and XML document creators re-implemented that, and the XML one did it with a convention of its own: a generic <shows><show> mapping that prefixed every field with the record element (show_title) and dropped the id, so XML was the one format whose documents did not match the same data in JSON, CSV or Markdown. Both creators are removed, along with the orchestrator's CSV/XML paths and the commons-csv dependency. The two tools now forward the payload, as given, to Solr's /update handler: - index-csv-documents sends the CSV with header=true. Solr reads the header for the field names, so column names are used as given. Repeated column names are multi-valued fields and empty cells are skipped. - index-xml-documents takes Solr update XML, <add><doc><field name="...">, the format every Solr user already has. Solr's update XML grammar is a command language: <delete>, <commit>, <optimize> and <rollback> go to the same endpoint as <add>, so a tool that forwarded blindly would let an indexing call delete a collection. SolrUpdateXml.requireAddBlock reads the payload with a hardened StAX parser (DTD off, external entities off) only as far as the root element and rejects a DOCTYPE or any root but <add>; Solr parses the rest. Solr's update response carries a status and a QTime but no document count, so the two tools report that Solr accepted and committed the payload instead of an invented "indexed N of N", and the index-data prompt points at the health check for the count. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MJSxr89SRAa7BTC8Jg27Rj Signed-off-by: Aditya Parikh <aditya.m.parikh@gmail.com>
…egration tests Port shows.csv, shows.xml, and ShowsSampleDataIntegrationTest from PR apache#201. The integration test verifies that JSON, CSV, and XML datasets index the exact same 61 documents into Solr. Signed-off-by: Aditya Parikh <aditya.m.parikh@gmail.com> Co-authored-by: Junie <junie@jetbrains.com>
…nd trip Follow-up cleanup on the CSV/XML pass-through, no behaviour change beyond the commit folding below. Reuse: - The XML content type was spelled out as a literal; SolrJ ships it as ClientUtils.TEXT_XML with exactly that value. (The CSV side keeps its literal: ContentStreamBase.TEXT_CSV is private.) - SolrUpdateXml configured an XMLInputFactory by hand. Spring's StaxUtils.createDefensiveInputFactory() sets the same two properties and additionally installs a no-op XMLResolver, so it is strictly more hardened. It is now a static field: XMLInputFactory.newFactory() runs a ServiceLoader scan of the whole classpath on every call, and the factory is never reconfigured after construction, which is the sharing contract StAX requires. - IndexingServiceIntegrationTest is a @SpringBootTest that overwrote its own @Autowired beans with hand-built ones, under a comment claiming it is not a Spring Boot test. IndexingService is @observed, so the test was exercising an unproxied object rather than the one the application runs. Efficiency: - forward() posted the payload and then posted a separate commit. The commit now rides on the same request via setAction(ACTION.COMMIT), which removes a round trip per CSV/XML call and makes the status and QTime the message reports actually cover the commit it claims. The two mock tests that pinned the second call now assert commit=true on the request. Simplification: - describeIndexedFields/describeFieldNames was split when three tools reported field names; only the JSON tool does now, so it is one method again. The indexDocuments javadoc that had drifted onto a constant is reattached. - SolrUpdateXml's ClosingReader record existed only to make one XMLStreamReader try-with-resources-able; the source is an in-memory StringReader and XMLStreamReader.close() does not close it. - Removed the emptied "Apache Commons" heading left by dropping commons-csv. Docs that still described the deleted parsers: IndexingDocumentCreator's class javadoc, the test tree in dev-docs/ARCHITECTURE.md, and the FAQ's claim that CSV and XML get field sanitization and 10 MB guards. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014XuwC2kdJ6Q1dDDZMHDCee Signed-off-by: Aditya Parikh <aditya.m.parikh@gmail.com>
The sample data was made readable from the native test binary with a raw
hosted option in the Gradle args list:
-H:IncludeResources=shows\.(csv|xml)$
That is the wrong seam and the wrong scope. dev-docs/graalvm-native-image.md
says to add a targeted registration through a hints registrar and prefer it
over other mechanisms "so the rule is explicit and reviewed", so the next
person adding a fixture does not find the precedent where the docs point.
And the rule being encoded is not the name of one dataset.
Replaced with a TestRuntimeHintsRegistrar wired through
src/test/resources/META-INF/spring/aot.factories, which spring-test invokes
for every test class. It lives in test sources deliberately: SolrNativeHints
is the production equivalent and must not ship test fixture names in the
application image.
The patterns name the three fixtures rather than matching by extension.
registerPattern compiles * to .*, which crosses /, so a *.xml here would
embed all 124 XML resources present on this project's 210-jar test
classpath, declaring every dependency's XML to be a test fixture.
Registering shows.json alongside its siblings is not redundant: Spring AOT
registers .*\.json globally, so the JSON fixture was covered by the
framework while the CSV and XML ones were not. Covering all three in one
place makes the dataset intentional rather than two-thirds accidental.
Verified with ./gradlew nativeTest -Pnative: 244 passed, 0 failed, 142
skipped (skips unchanged from baseline), with
ShowsSampleDataIntegrationTest executing natively. A negative control with
the registrar unregistered fails that test alone, on
"NullPointerException: missing test resource /shows.csv", confirming the
registration is load-bearing and not a no-op replacing an unnecessary flag.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014XuwC2kdJ6Q1dDDZMHDCee
Signed-off-by: Aditya Parikh <aditya.m.parikh@gmail.com>
95fca5b to
4b0815f
Compare
|
Rebased onto #205 and pushed. This PR now depends on #205 and should merge after it. The first four commits shown are #205's; GitHub cannot retarget the base to a branch on the fork, so they appear here until #205 lands. The rebase was necessary rather than cosmetic. #205 introduces Three fixes on top: Soft commit for CSV and XML ( The size threshold was wrong, and unreachable ( Input format vs conversion target ( Build green: 392 tests, 0 failures, 7 skipped (the pre-existing |
… searchable Every indexing tool ended its work with a hard commit: indexDocuments called solrClient.commit(collection), and the CSV/XML path added by apache#205 issued setAction(COMMIT, waitFlush=true, waitSearcher=true). A hard commit fsyncs the segments, so each tool call waited on the storage device. That is not what Solr's own defaults do. The _default configset ships autoCommit at maxTime 15000 with openSearcher=false, and autoSoftCommit at 3000: a background hard commit purely to truncate the transaction log, and soft commits for visibility. Forcing a synchronous fsync per tool call fought that design. Both paths now commit with waitFlush=false, waitSearcher=true, softCommit=true. waitSearcher keeps the guarantee that matters to a tool caller: the documents are searchable the moment the call returns. Verified 30/30 with zero delay across the JSON and CSV paths. Durability is unchanged. The transaction log is written on the add, before any commit, so documents survive a crash regardless of commit mode; a hard commit governs how much tlog must be replayed on recovery, not whether data is lost. That housekeeping stays with autoCommit. Measured against Solr, 20 interleaved reps, same endpoint and document, only the commit parameter varying: no commit 4.05 ms median soft commit 8.61 ms median, p90 10.65 hard commit 18.94 ms median, p90 41.32 2.2x faster and far tighter -- the hard commit's p90 is four times its median, which is fsync variance. Over the MCP tools, 61 single-document CSV calls go from 1853 ms to 585 ms. Operators running a custom configset with autoCommit disabled should enable it, or the transaction log grows until something else commits. Signed-off-by: Aditya Parikh <aditya.m.parikh@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4b0815f to
44ea747
Compare
|
Reduced to one commit. All payload-splitting and format guidance is reverted — the instructions, the tool descriptions and the Why the guidance went away. It did not work, and the MCP maintainers consider the shape of it an anti-pattern. Tested live: Sonnet 5 in Claude Desktop, an 86 KB markdown paste. With the guidance in place the model did batch correctly — 15 calls of 10 documents, no truncation — and still took over 15 minutes, because splitting does not reduce total output tokens and each extra call re-reads the whole conversation. Meanwhile discussion #1197 records the consensus as keep the data plane out of JSON-RPC arguments entirely, naming "passing entire file content through LLM context" as an anti-pattern, and the File Uploads WG charter names "prose instructions asking for base64 strings or local paths" as the problem it exists to remove. Tuning prose was treating a symptom the spec intends to delete; the real answer is the handle-based path (SEP-2356, SEP-2631), which is bigger than this PR. Why the commit change stays. Both paths hard-committed — Measured, 20 interleaved reps, same endpoint and document, only the commit parameter varying:
2.2x faster, and far tighter — the hard commit's p90 is four times its median, which is fsync variance. Over the MCP tools, 61 single-document CSV calls go from 1853 ms to 585 ms. Searchability is preserved, which was the condition: Still depends on #205 and should merge after it. Build green: 390 tests, 0 failures. |
Summary
Two small changes to inline indexing.
spring.ai.mcp.server.instructions) now tell the model: send data in the format it already has; when converting (for example from a paste), emit CSV and never convert to XML; pass JSON as thedocumentsarray, not a string; split inputs larger than a few hundred KB across several calls, not further than needed because each call commits. These are directives rather than a cost comparison, because a live run showed the model ignoring the comparison and emitting escaped JSON. Instructions are sent once per session, so this is the one home for it; tool descriptions are unchanged.indexDocumentsnow ends with a soft commit (waitFlush=false, waitSearcher=true, softCommit=true) instead of a hard commit. Documents are still searchable when the tool returns; the segment fsync is left to Solr'sautoCommit, which the_defaultconfigset enables at 15 s, and the transaction log covers durability in between. README notes that a configset withautoCommitdisabled should enable it.Tests
IndexingServiceTestverifies the soft-commit overload is used and the hard-commit overload is not.McpServerInstructionsTestpins the splitting clause and the convert-to-CSV directive inapplication.properties.Verification
./gradlew buildon Java 25: green (406 tests, 0 failures, 7 skipped at the previous head; unchanged test set plus one assertion) (the OTLP suite, skipped onmainuntil #198). Independent of #197: the only shared file isIndexingServiceTest, and neither branch stubscommitthere.🤖 Generated with Claude Code
https://claude.ai/code/session_01CiUHyyXLTo9ATdgg8eRFZJ