Skip to content

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

Open
adityamparikh wants to merge 6 commits into
apache:mainfrom
adityamparikh:feat/json-documents-array
Open

adityamparikh wants to merge 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",...}]}

What changed

  • JsonDocumentCreator.create(List<Map<String,Object>>): same flattening as the string path (nested objects to a_b, arrays to multi-valued fields, nulls skipped). JsonDocumentCreatorTest pins that both entry points produce identical documents.
  • IndexingService.indexJsonDocuments takes the typed list; description tells the model to pass the array itself, not a string. The index-data prompt says the same for the JSON path and its opening line names the format rather than 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.

Cleanup pass (00e8c7c)

  • One flattening path. create(List) and create(String) 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 rather than one per document.
  • Javadoc rebinding. create(List) had been 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.
  • Null moved to the creator, which already owns the "nothing to index" policy, dropping a one-off IllegalArgumentException from indexJsonDocuments that none of the three sibling index tools had.
  • IndexTool gained 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 mdmarkdown canonicalisation, so format=md rendered "You are indexing md data".
  • createSchemalessDocumentscreateSchemalessDocumentsFromJson, keeping the orchestrator's ...From<Format> family; stale @see; TestDocuments catches JsonProcessingException (what readValue declares, and what survives Jackson 3) and hoists its TypeReference; CollectionServiceIntegrationTest passes the documents it already holds instead of serialising and reparsing them, retiring a now-unused autowired ObjectMapper.

Input size limits removed (0c0a2f2, 327ae50, 792ecd5)

The typed documents array has no string form to measure, so the JSON tool path was already uncapped. Rather than add a second limit to match, the existing 10 MB caps were removed across all four formats.

All four 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. Only one test touched a limit — testCreateSchemalessDocumentsFromXmlWithLargeDocument, which built a 12 MB document to assert the XML check fired — and it 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. Part of a set with feat/markdown-multi-document and fix/xml-record-fields; the three are independent and merge in any order.

Verification

  • ./gradlew build on Java 25: green — 406 tests, 399 passed, 0 failed, 7 skipped (the pre-existing @Disabled OTLP/LGTM class). The count is one lower than before the cap removal, which is the deleted XML size test.
  • ./gradlew nativeTest -Pnative on GraalVM CE 25.0.2: 259 successful, 0 failed (the generic List<Map> parameter deserialises natively without extra hints). Measured before the cleanup commits; the cleanup adds no new types or reflection.
  • Measured against a running server: 61 documents index in 0.1 s server-side either way; the saving is entirely in what the model has to emit.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JZoVjWoHU315uEngMQdhWE

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>
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.

1 participant