Skip to content

feat(indexing): index-json-documents takes a typed documents array - #202

Merged
epugh merged 6 commits into
apache:mainfrom
adityamparikh:feat/json-documents-array
Sep 16, 2026
Merged

epugh merged 6 commits into
apache:mainfrom
adityamparikh:feat/json-documents-array

Conversation

@adityamparikh

@adityamparikh adityamparikh commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

index-json-documents took its documents as a JSON string, so the model had to escape every quote and newline of the payload inside a JSON string argument. On the 61-show sample that escaping is about 12% of the output tokens the model generates for the call, and one mis-escaped quote fails the whole call after the payload has been produced.

The parameter is now List<Map<String, Object>> documents, advertised in the tool schema as an array of objects. The model emits native JSON, the MCP client parses it once, and on the SDK 2.0 line (#23) the shape is validated before dispatch.

before:  {"collection":"shows","json":"[{\"id\":\"netflix-001\",\"title\":\"Stranger Things\",...}]"}
after:   {"collection":"shows","documents":[{"id":"netflix-001","title":"Stranger Things",...}]}

Server-side indexing time is unchanged — 61 documents take 0.1 s either way — so the whole saving is in what the model has to emit.

What changed

  • JsonDocumentCreator.create(List<Map<String,Object>>) converts the list with one valueToTree call and hands it to the same flatten(JsonNode) / toDocument(JsonNode) walk create(String) uses, so both entry points flatten identically by construction (nested objects to a_b, arrays to multi-valued fields, nulls skipped). JsonDocumentCreatorTest pins that they produce the same documents.
  • IndexingService.indexJsonDocuments takes the typed list; the tool description tells the model to pass the array itself, not a string. Empty/null input is rejected by the creator, which already owns that policy for every format.
  • IndexTool in the index-data prompt now carries the canonical format name separately from the payload parameter name — the two were the same string until the JSON parameter was renamed to documents, and the prompt's opening line needs the format ("markdown" even when the caller passes md), not the parameter.
  • Tests keep their JSON text blocks and parse them through a new TestDocuments.json(...) helper; the MCP-level tests send the array.
  • README tool table.

Input size limits removed

Behavior change worth a reviewer's attention: the 10 MB input cap is gone from all four formats, not just JSON.

The typed documents array has no string form to measure, so the JSON tool path was already uncapped; adding a second limit to match would have meant maintaining the cap in two shapes. All four formats carried the same copy-pasted 10 * 1024 * 1024 constant and the same getBytes() length check, which materialised a second full copy of the payload purely to measure it, immediately before the parser read the string again — in two different layers (JSON/CSV/markdown in the creators, XML in the orchestrator) with two different message formats.

Payload size is now bounded by the heap and by whatever limit the transport in front of the server imposes. Blank/null validation is unchanged on every path and still covered. The one test that exercised a limit — testCreateSchemalessDocumentsFromXmlWithLargeDocument, which built a 12 MB document to assert the XML check fired — is removed with it.

Not changed

CSV, XML and Markdown stay strings: those formats have few quotes and no repeated keys, so a string is already their cheapest wire form.

Independent of #203 and #205; the three merge in any order.

Tests

./gradlew build (Java 25) and ./gradlew nativeTest -Pnative (GraalVM CE 25.0.2) are green; the generic List<Map> parameter deserialises natively without extra hints.

🤖 Generated with Claude Code

adityamparikh and others added 2 commits September 13, 2026 22:50
The tool took its documents as a JSON string, so the model had to escape
every quote and newline of the payload inside a JSON string argument. On a
61-document sample that escaping is about 12% of the output tokens, and a
single mis-escaped quote fails the whole call after the payload has been
generated. The parameter is now List<Map<String, Object>>, advertised as an
array of objects, so the model emits native JSON and the SDK parses it once.

JsonDocumentCreator gains a create(List<Map>) entry point that runs the same
flattening as the string path; JsonDocumentCreatorTest pins that both produce
identical documents. The index-data prompt tells the model to pass the array
itself, and its opening line now names the format rather than the parameter.
Tests keep their JSON text blocks and parse them through TestDocuments.json.

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>
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>
adityamparikh and others added 4 commits September 14, 2026 15:04
… points

Cleanup pass over the typed-documents change. No behaviour change beyond
the markdown prompt fix noted below.

JsonDocumentCreator: create(List) was inserted between the create(String)
javadoc and the method it documents, so javac bound only the trailing
block -- create(String) lost its docs and create(List) inherited a
@PARAM json it does not have. Moving it below reunites them. Both entry
points now share flatten(JsonNode)/toDocument(JsonNode) instead of
maintaining parallel loops, so objectsAndStringProduceTheSameDocuments
guards a shared path rather than being the only thing keeping two
hand-written loops in step. create(List) converts the whole list with one
valueToTree call instead of one per document.

Null moves down to the creator, which already owns the "nothing to index"
policy, so it reports null the same way it reports empty. That drops the
one-off IllegalArgumentException from indexJsonDocuments, which none of
the three sibling index tools had.

IndexTool gains the canonical format. The prompt's first %s was
indexTool.paramName(), which worked only because paramName happened to
equal the format keyword; renaming the JSON parameter to `documents`
broke that, and re-normalising the raw argument lost the md -> markdown
canonicalisation, so format=md rendered "You are indexing md data".

Also: createSchemalessDocuments -> createSchemalessDocumentsFromJson, so
the orchestrator keeps its ...From<Format> family; the stale @see on
indexJsonDocuments; TestDocuments catches JsonProcessingException, which
is what readValue declares and what survives Jackson 3, and hoists its
TypeReference; and CollectionServiceIntegrationTest passes the documents
it already holds instead of serialising and reparsing them, retiring a
now-unused autowired ObjectMapper.

Signed-off-by: Aditya Parikh <aditya.m.parikh@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JZoVjWoHU315uEngMQdhWE
Signed-off-by: Aditya Parikh <aditya.m.parikh@gmail.com>
The typed `documents` array has no string form to measure, so the
`index-json-documents` path was already uncapped. Rather than add a
second limit to match, remove the one on the string entry point so both
JSON paths behave the same way.

Payload size is now bounded by the heap and by whatever limit the
transport in front of the server imposes, not by this creator. CSV and
markdown keep their own caps; this changes JSON only.

No test covered the limit, so nothing else moves.

Signed-off-by: Aditya Parikh <aditya.m.parikh@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JZoVjWoHU315uEngMQdhWE
Signed-off-by: Aditya Parikh <aditya.m.parikh@gmail.com>
Follows the JSON change: all three creators carried the same copy-pasted
10 MB constant and the same getBytes() length check, which materialised a
second full copy of the payload purely to measure it, immediately before
the parser read the string again.

Payload size is now bounded by the heap and by whatever limit the
transport in front of the server imposes, not by the creators. The XML
path keeps its own "XML document too large" check in
IndexingDocumentCreator, which is a separate limit with test coverage.

No test covered either cap, so nothing else moves.

Signed-off-by: Aditya Parikh <aditya.m.parikh@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JZoVjWoHU315uEngMQdhWE
Signed-off-by: Aditya Parikh <aditya.m.parikh@gmail.com>
Completes the removal across all four formats. The XML limit lived in the
orchestrator rather than in XmlDocumentCreator, and was the last of the
copy-pasted 10 MB checks; like the others it allocated a full byte[] copy
of the payload purely to measure it before the parser read the string.

Removes MAX_XML_SIZE_BYTES, the check, the now-unused StandardCharsets
import, and testCreateSchemalessDocumentsFromXmlWithLargeDocument, which
built a 12 MB document to assert the limit fired. The null/blank
validation on the XML path is unchanged and still covered.

Payload size is now bounded by the heap and by whatever limit the
transport in front of the server imposes.

Signed-off-by: Aditya Parikh <aditya.m.parikh@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JZoVjWoHU315uEngMQdhWE
Signed-off-by: Aditya Parikh <aditya.m.parikh@gmail.com>
@epugh
epugh merged commit 589a4b8 into apache:main Sep 16, 2026
1 check passed
adityamparikh added a commit to adityamparikh/solr-mcp that referenced this pull request Sep 16, 2026
…103 #104 apache#205) into sb4

Resolves the conflicts against main and ports what merged cleanly but
would not compile on the Boot 4 / Jackson 3 line:

- gradle/libs.versions.toml: keep the Boot 4 test starters and the
  mcp-sdk pin, add spring-security-test (apache#187), drop commons-csv (apache#205).
- JsonDocumentCreator: take apache#202's flatten()/toDocument() split with
  Jackson 3 imports and JacksonException.
- CollectionServiceIntegrationTest / IndexingServiceIntegrationTest:
  take main's typed-documents and autowired-creator setup.
- TestDocuments, JsonDocumentCreatorTest (new on main): com.fasterxml ->
  tools.jackson, JsonMapper.builder().build(), JacksonException.
- IndexingServiceIntegrationTest: add the assertThrows static import
  that sb4's explicit-import expansion left out for apache#205's new XML test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Aditya Parikh <aditya.m.parikh@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants