Copilot commented on code in PR #205:
URL: https://github.com/apache/solr-mcp/pull/205#discussion_r4027525574


##########
src/main/java/org/apache/solr/mcp/server/indexing/IndexingService.java:
##########
@@ -198,209 +201,124 @@ public IndexingService(SolrClient solrClient, 
IndexingDocumentCreator indexingDo
         *
         * @param collection
         *            the name of the Solr collection to index documents into
-        * @param documents
-        *            the documents to index, one map per document
+        * @param json
+        *            JSON string containing an array of documents to index
         * @return a human-readable summary reporting how many documents were
         *         successfully indexed
         * @throws IOException
         *             if there are critical errors in JSON parsing or Solr
         *             communication
         * @throws SolrServerException
         *             if Solr server encounters errors during indexing
-        * @see IndexingDocumentCreator#createSchemalessDocumentsFromJson(List)
+        * @see 
IndexingDocumentCreator#createSchemalessDocumentsFromJson(String)
         * @see #indexDocuments(String, List)
         */
        @PreAuthorize("isAuthenticated()")
        @McpTool(
                        name = "index-json-documents",
                        annotations = @McpTool.McpAnnotations(idempotentHint = 
true),
-                       description = "Index documents passed as a JSON array 
of objects into Solr collection; one object"
-                                       + " per document, multi-valued fields 
as arrays, nested objects flattened with underscores."
-                                       + " Pass the array itself, not a JSON 
string. Field names are sanitized for Solr"
-                                       + " compatibility (lowercased, special 
characters replaced with underscores); the response"
-                                       + " lists the field names as indexed")
+                       description = "Index documents from json String into 
Solr collection. Field names are"
+                                       + " sanitized for Solr compatibility 
(lowercased, special characters replaced"
+                                       + " with underscores); the response 
lists the field names as indexed")
        public String indexJsonDocuments(@McpToolParam(description = "Solr 
collection to index into") String collection,
-                       @McpToolParam(
-                                       description = "Documents to index: a 
JSON array with one object per document") List<Map<String, Object>> documents)
+                       @McpToolParam(description = "JSON string containing 
documents to index") String json)
                        throws IOException, SolrServerException {
-               List<SolrInputDocument> schemalessDoc = 
indexingDocumentCreator.createSchemalessDocumentsFromJson(documents);
+               List<SolrInputDocument> schemalessDoc = 
indexingDocumentCreator.createSchemalessDocumentsFromJson(json);
                int successCount = indexDocuments(collection, schemalessDoc);
                return "Successfully indexed " + successCount + " of " + 
schemalessDoc.size() + " documents into collection '"
                                + collection + "'" + 
describeIndexedFields(schemalessDoc);
        }
 
        /**
-        * Indexes documents from a CSV string into a specified Solr collection.
-        *
-        * <p>
-        * This method serves as the primary entry point for CSV document 
indexing
-        * operations and is exposed as an MCP tool for AI client interactions. 
It
-        * processes CSV data with headers and indexes them using a schema-less
-        * approach.
-        *
-        * <p>
-        * <strong>Supported CSV Formats:</strong>
-        *
-        * <ul>
-        * <li><strong>Header Row Required</strong>: First row must contain 
column names
-        * <li><strong>Comma Delimited</strong>: Standard CSV format with comma
-        * separators
-        * <li><strong>Mixed Data Types</strong>: Automatic type detection by 
Solr
-        * </ul>
-        *
-        * <p>
-        * <strong>Processing Workflow:</strong>
-        *
-        * <ol>
-        * <li>Parse CSV string to extract headers and data rows
-        * <li>Convert to schema-less SolrInputDocument objects
-        * <li>Execute batch indexing with error handling
-        * <li>Commit changes to make documents searchable
-        * </ol>
-        *
-        * <p>
-        * <strong>MCP Tool Usage:</strong>
-        *
-        * <p>
-        * AI clients can invoke this method with natural language requests 
like "index
-        * this CSV data into my_collection" or "add these CSV records to the 
search
-        * index".
-        *
-        * <p>
-        * <strong>Error Handling:</strong>
-        *
-        * <p>
-        * If indexing fails, the method attempts individual document 
processing to
-        * maximize the number of successfully indexed documents. Detailed error
-        * information is logged for troubleshooting purposes.
+        * Indexes CSV rows into a Solr collection by forwarding the payload, 
as given,
+        * to Solr's own CSV update handler. Solr reads the header row for the 
field
+        * names and parses the rows; the server does not inspect the payload. 
A column
+        * name repeated in the header yields a multi-valued field, and empty 
cells are
+        * skipped. Solr accepts or rejects the payload as a whole.
         *
         * @param collection
         *            the name of the Solr collection to index documents into
         * @param csv
-        *            CSV string containing documents to index (first row must 
be
-        *            headers)
-        * @return a human-readable summary reporting how many documents were
-        *         successfully indexed
+        *            CSV text with a header row
+        * @return a human-readable confirmation that Solr accepted and 
committed the
+        *         payload
         * @throws IOException
-        *             if there are critical errors in CSV parsing or Solr 
communication
+        *             if communication with Solr fails
         * @throws SolrServerException
-        *             if Solr server encounters errors during indexing
-        * @see IndexingDocumentCreator#createSchemalessDocumentsFromCsv(String)
-        * @see #indexDocuments(String, List)
+        *             if Solr rejects the payload
         */
        @PreAuthorize("isAuthenticated()")
        @McpTool(
                        name = "index-csv-documents",
                        annotations = @McpTool.McpAnnotations(idempotentHint = 
true),
-                       description = "Index documents from CSV string into 
Solr collection. Column names are"
-                                       + " sanitized for Solr compatibility 
(lowercased, special characters replaced"
-                                       + " with underscores); the response 
lists the field names as indexed")
+                       description = "Index documents from CSV string into 
Solr collection via Solr's CSV handler. The first row"
+                                       + " is the header and its column names 
are used as the field names, as given; repeat a column name"
+                                       + " to make that field multi-valued; 
empty cells are skipped")
        public String indexCsvDocuments(@McpToolParam(description = "Solr 
collection to index into") String collection,
                        @McpToolParam(description = "CSV string containing 
documents to index") String csv)
                        throws IOException, SolrServerException {
-               List<SolrInputDocument> schemalessDoc = 
indexingDocumentCreator.createSchemalessDocumentsFromCsv(csv);
-               int successCount = indexDocuments(collection, schemalessDoc);
-               return "Successfully indexed " + successCount + " of " + 
schemalessDoc.size() + " documents into collection '"
-                               + collection + "'" + 
describeIndexedFields(schemalessDoc);
+               ContentStreamUpdateRequest request = new 
ContentStreamUpdateRequest("/update");
+               request.setParam("header", "true");
+               request.addContentStream(new 
ContentStreamBase.StringStream(csv, "text/csv; charset=UTF-8"));
+               return forward(collection, request, "CSV payload");
        }
 
        /**
-        * Indexes documents from an XML string into a specified Solr 
collection.
-        *
-        * <p>
-        * This method serves as the primary entry point for XML document 
indexing
-        * operations and is exposed as an MCP tool for AI client interactions. 
It
-        * processes XML data with nested elements and attributes, indexing 
them using a
-        * schema-less approach.
-        *
-        * <p>
-        * <strong>Supported XML Formats:</strong>
-        *
-        * <ul>
-        * <li><strong>Single Document</strong>: Root element treated as one 
document
-        * <li><strong>Multiple Documents</strong>: Child elements with 'doc', 
'item',
-        * or 'record' names treated as separate documents
-        * <li><strong>Nested Elements</strong>: Automatically flattened with 
underscore
-        * notation
-        * <li><strong>Attributes</strong>: Converted to fields with "_attr" 
suffix
-        * <li><strong>Mixed Data Types</strong>: Automatic type detection by 
Solr
-        * </ul>
-        *
-        * <p>
-        * <strong>Processing Workflow:</strong>
-        *
-        * <ol>
-        * <li>Parse XML string to extract elements and attributes
-        * <li>Flatten nested structures using underscore notation
-        * <li>Convert to schema-less SolrInputDocument objects
-        * <li>Execute batch indexing with error handling
-        * <li>Commit changes to make documents searchable
-        * </ol>
-        *
-        * <p>
-        * <strong>MCP Tool Usage:</strong>
-        *
-        * <p>
-        * AI clients can invoke this method with natural language requests 
like "index
-        * this XML data into my_collection" or "add these XML records to the 
search
-        * index".
-        *
-        * <p>
-        * <strong>Error Handling:</strong>
-        *
-        * <p>
-        * If indexing fails, the method attempts individual document 
processing to
-        * maximize the number of successfully indexed documents. Detailed error
-        * information is logged for troubleshooting purposes.
-        *
-        * <p>
-        * <strong>Example XML Processing:</strong>
-        *
-        * <pre>{@code
-        * Input:
-        * <documents>
-        *   <document id="1">
-        *     <title>Sample</title>
-        *     <author>
-        *       <name>John Doe</name>
-        *     </author>
-        *   </document>
-        * </documents>
-        *
-        * Result: {id_attr:"1", title:"Sample", author_name:"John Doe"}
-        * }</pre>
+        * Indexes documents supplied in Solr's update XML format
+        * ({@code <add><doc><field name="...">...</field></doc></add>}) by 
forwarding
+        * the payload to Solr's update handler. The only server-side step is
+        * {@link SolrUpdateXml}: the same grammar carries {@code <delete>} and
+        * {@code <commit>} commands that an indexing tool must not forward, so 
the root
+        * element must be {@code <add>}. Solr parses the payload and accepts 
or rejects
+        * it as a whole.
         *
         * @param collection
         *            the name of the Solr collection to index documents into
         * @param xml
-        *            XML string containing documents to index
-        * @return a human-readable summary reporting how many documents were
-        *         successfully indexed
-        * @throws ParserConfigurationException
-        *             if XML parser configuration fails
-        * @throws SAXException
-        *             if XML parsing fails due to malformed content
+        *            a Solr {@code <add>} block
+        * @return a human-readable confirmation that Solr accepted and 
committed the
+        *         payload
         * @throws IOException
-        *             if I/O errors occur during parsing or Solr communication
+        *             if communication with Solr fails
         * @throws SolrServerException
-        *             if Solr server encounters errors during indexing
-        * @see IndexingDocumentCreator#createSchemalessDocumentsFromXml(String)
-        * @see #indexDocuments(String, List)
+        *             if Solr rejects the payload
         */
        @PreAuthorize("isAuthenticated()")
        @McpTool(
                        name = "index-xml-documents",
                        annotations = @McpTool.McpAnnotations(idempotentHint = 
true),
-                       description = "Index documents from XML string into 
Solr collection. Element names are"
-                                       + " sanitized for Solr compatibility 
(lowercased, special characters replaced"
-                                       + " with underscores); the response 
lists the field names as indexed")
+                       description = "Index documents from Solr update XML 
into Solr collection: <add><doc><field name=\"id\">1</field>"
+                                       + "<field 
name=\"genres\">a</field><field name=\"genres\">b</field></doc></add>; repeat 
<field> for"
+                                       + " multi-valued fields. Only <add> 
blocks are accepted; delete and commit commands are rejected."
+                                       + " Field names are used as given")
        public String indexXmlDocuments(@McpToolParam(description = "Solr 
collection to index into") String collection,
-                       @McpToolParam(description = "XML string containing 
documents to index") String xml)
-                       throws ParserConfigurationException, SAXException, 
IOException, SolrServerException {
-               List<SolrInputDocument> schemalessDoc = 
indexingDocumentCreator.createSchemalessDocumentsFromXml(xml);
-               int successCount = indexDocuments(collection, schemalessDoc);
-               return "Successfully indexed " + successCount + " of " + 
schemalessDoc.size() + " documents into collection '"
-                               + collection + "'" + 
describeIndexedFields(schemalessDoc);
+                       @McpToolParam(description = "Solr update XML: an <add> 
block of <doc> elements") String xml)
+                       throws IOException, SolrServerException {
+               SolrUpdateXml.requireAddBlock(xml);
+               ContentStreamUpdateRequest request = new 
ContentStreamUpdateRequest("/update");
+               request.addContentStream(new 
ContentStreamBase.StringStream(xml, ClientUtils.TEXT_XML));
+               return forward(collection, request, "XML <add> block");
+       }
+
+       /**
+        * Sends a payload to a Solr update handler and reports Solr's answer. 
The
+        * commit rides along on the same request rather than following as a 
second
+        * round trip, so the status and query time reported here cover the 
commit this
+        * message claims. Solr's update response carries no document count, so 
none is
+        * claimed.
+        *
+        * <p>
+        * The commit is a soft one: {@code waitSearcher} keeps the documents 
searchable
+        * the moment the tool returns, while the segment fsync is left to 
Solr's
+        * {@code autoCommit}, which the {@code _default} configset enables at 
15 s, so
+        * many small calls do not each force one.
+        */
+       private String forward(String collection, ContentStreamUpdateRequest 
request, String payload)
+                       throws IOException, SolrServerException {
+               request.setAction(AbstractUpdateRequest.ACTION.COMMIT, false, 
true, true);
+               UpdateResponse response = request.process(solrClient, 
collection);
+               return "Solr accepted the " + payload + " for collection '" + 
collection + "' and committed it (status "
+                               + response.getStatus() + ", " + 
response.getQTime() + " ms)";
        }
 

Review Comment:
   This method unconditionally reports “Solr accepted … and committed it” even 
if the update response indicates failure (non-zero status). To keep the message 
accurate and avoid false positives, check `response.getStatus()` and throw an 
exception (or return an error message) when the status is non-zero.



##########
src/test/java/org/apache/solr/mcp/server/indexing/IndexingServiceTest.java:
##########
@@ -296,19 +214,73 @@ void 
indexJsonDocuments_WhenSolrClientThrowsException_ShouldPropagateException()
        }
 
        @Test
-       void 
indexCsvDocuments_WhenSolrClientThrowsIOException_ShouldPropagateException() 
throws Exception {
-               String csv = "id,title\n1,Test";
-               List<SolrInputDocument> mockDocs = createMockDocuments(1);
-               
when(indexingDocumentCreator.createSchemalessDocumentsFromCsv(csv)).thenReturn(mockDocs);
-               when(solrClient.add(eq("test_collection"), 
any(List.class))).thenThrow(new IOException("Network error"));
-               when(solrClient.add(eq("test_collection"), 
any(SolrInputDocument.class)))
-                               .thenThrow(new IOException("Network error"));
-               when(solrClient.commit("test_collection", false, true, 
true)).thenReturn(null);
+       void indexCsvDocuments_ForwardsPayloadAsGivenToSolrCsvHandler() throws 
Exception {
+               when(solrClient.request(any(ContentStreamUpdateRequest.class), 
eq("test_collection")))
+                               .thenReturn(new NamedList<>());
+               String csv = "id,Show Title,genres,genres\n1,A,x,y\n2,B,z,\n";
+
+               String result = 
indexingService.indexCsvDocuments("test_collection", csv);
+
+               ArgumentCaptor<ContentStreamUpdateRequest> captor = 
ArgumentCaptor.forClass(ContentStreamUpdateRequest.class);
+               verify(solrClient).request(captor.capture(), 
eq("test_collection"));
+               assertEquals("/update", captor.getValue().getPath());
+               assertEquals("true", 
captor.getValue().getParams().get("header"));
+               assertNull(captor.getValue().getParams().get("fieldnames"), 
"column names are Solr's to read, as given");
+               var stream = 
captor.getValue().getContentStreams().iterator().next();
+               assertTrue(stream.getContentType().startsWith("text/csv"));
+               assertEquals(csv, new String(stream.getStream().readAllBytes(), 
StandardCharsets.UTF_8));
+               assertEquals("true", 
captor.getValue().getParams().get("commit"),
+                               "the commit rides on the update request, not a 
second round trip");
+               assertEquals("true", 
captor.getValue().getParams().get("softCommit"),
+                               "soft commit: searchable on return, the segment 
fsync left to Solr's autoCommit");
+               verify(solrClient, never()).commit(anyString());
+               assertTrue(result.contains("Solr accepted the CSV payload"), 
result);
+               assertTrue(result.contains("'test_collection'"), result);
+               assertFalse(result.contains(" of "), "no document count is 
claimed: Solr does not report one: " + result);
+       }
 
-               indexingService.indexCsvDocuments("test_collection", csv);
+       @Test
+       void indexXmlDocuments_ForwardsAddBlockToSolr() throws Exception {
+               when(solrClient.request(any(ContentStreamUpdateRequest.class), 
eq("test_collection")))
+                               .thenReturn(new NamedList<>());
+               String xml = "<add><doc><field name=\"id\">1</field><field 
name=\"title\">T</field></doc></add>";
+
+               String result = 
indexingService.indexXmlDocuments("test_collection", xml);
+
+               ArgumentCaptor<ContentStreamUpdateRequest> captor = 
ArgumentCaptor.forClass(ContentStreamUpdateRequest.class);
+               verify(solrClient).request(captor.capture(), 
eq("test_collection"));
+               assertTrue(
+                               
captor.getValue().getContentStreams().iterator().next().getContentType().startsWith("application/xml"));
+               assertEquals("true", 
captor.getValue().getParams().get("commit"),
+                               "the commit rides on the update request, not a 
second round trip");
+               assertEquals("true", 
captor.getValue().getParams().get("softCommit"),
+                               "soft commit: searchable on return, the segment 
fsync left to Solr's autoCommit");
+               verify(solrClient, never()).commit(anyString());
+               assertTrue(result.contains("Solr accepted the XML <add> 
block"), result);
+               assertTrue(result.contains("'test_collection'"), result);
+       }
 

Review Comment:
   The XML payload in this test is not well-formed (`<field name=\"title\">` is 
never closed). This can let the test pass while exercising an impossible 
success case (since Solr would reject malformed XML). Update the test string to 
valid Solr update XML so the forwarding path is tested realistically.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to