You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ep...@apache.org on 2026/10/06 18:50:35 UTC

(solr-mcp) branch main updated: perf(indexing): one request per JSON/markdown call; retry per document only on a 400 (#233)

This is an automated email from the ASF dual-hosted git repository.

epugh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr-mcp.git


The following commit(s) were added to refs/heads/main by this push:
     new 48953297 perf(indexing): one request per JSON/markdown call; retry per document only on a 400 (#233)
48953297 is described below

commit 48953297a45d7dc5a83f5cad99a91ace8ffa550c
Author: Aditya Parikh <ad...@gmail.com>
AuthorDate: Tue Oct 6 14:50:30 2026 -0400

    perf(indexing): one request per JSON/markdown call; retry per document only on a 400 (#233)
    
    * perf(indexing): cut redundant Solr round trips in indexDocuments
    
    indexDocuments() retried every document individually on ANY batch
    failure (SolrServerException/IOException/RuntimeException), swallowing
    the cause and costing up to N+2 round trips per batch on a missing
    collection, auth failure, 5xx, or Solr being down -- ending as
    "Successfully indexed 0 of N" instead of reporting the real error.
    Per-document fallback now only fires for a Solr 400 (bad documents);
    everything else propagates immediately with the root cause intact.
    
    JSON/markdown indexing also issued a separate solrClient.commit() call
    after the adds. The last batch now carries add + commit on a single
    UpdateRequest (same soft-commit semantics: waitSearcher=true,
    searchable on return), matching how CSV/XML already ride the commit on
    their forwarded request. Markdown (always one document) and JSON up to
    the batch size go from two round trips to one. A batch that falls back
    to per-document retry still commits explicitly, since its embedded
    commit never ran.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <no...@anthropic.com>
    Signed-off-by: Aditya Parikh <ad...@gmail.com>
    
    * refactor(indexing): send JSON and markdown documents in one request
    
    Replace the client-side 1000-document batching with one UpdateRequest
    carrying the documents and the soft commit, as CSV and XML already
    send their whole payload in one request. The documents arrive in a
    single MCP call and are already in memory, so batching bought nothing
    and needed last-batch bookkeeping to place the commit.
    
    A 400 still falls back to per-document adds followed by a commit.
    The per-call field-name memo in JsonDocumentCreator is reverted: it
    saved CPU only, not Solr round trips.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <no...@anthropic.com>
    Signed-off-by: Aditya Parikh <ad...@gmail.com>
    
    ---------
    
    Signed-off-by: Aditya Parikh <ad...@gmail.com>
    Co-authored-by: Claude Opus 5.5 (1M context) <no...@anthropic.com>
---
 .../solr/mcp/server/indexing/IndexingService.java  | 142 ++++++-------------
 .../mcp/server/indexing/IndexingServiceTest.java   | 152 ++++++++++-----------
 2 files changed, 114 insertions(+), 180 deletions(-)

diff --git a/src/main/java/org/apache/solr/mcp/server/indexing/IndexingService.java b/src/main/java/org/apache/solr/mcp/server/indexing/IndexingService.java
index e5ae85f2..2d6fdc20 100644
--- a/src/main/java/org/apache/solr/mcp/server/indexing/IndexingService.java
+++ b/src/main/java/org/apache/solr/mcp/server/indexing/IndexingService.java
@@ -27,8 +27,10 @@ import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.request.AbstractUpdateRequest;
 import org.apache.solr.client.solrj.request.ContentStreamUpdateRequest;
+import org.apache.solr.client.solrj.request.UpdateRequest;
 import org.apache.solr.client.solrj.response.UpdateResponse;
 import org.apache.solr.client.solrj.util.ClientUtils;
+import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrInputDocument;
 import org.apache.solr.common.util.ContentStreamBase;
 import org.apache.solr.mcp.server.indexing.documentcreator.IndexingDocumentCreator;
@@ -50,9 +52,9 @@ import org.springframework.stereotype.Service;
  * <p>
  * This service handles the conversion of JSON, CSV, XML, and markdown documents
  * into Solr-compatible format and manages the indexing process with robust
- * error handling and batch processing capabilities. It employs a schema-less
- * approach where Solr automatically detects field types, eliminating the need
- * for predefined schema configuration.
+ * error handling. It employs a schema-less approach where Solr automatically
+ * detects field types, eliminating the need for predefined schema
+ * configuration.
  *
  * <p>
  * <strong>Core Features:</strong>
@@ -68,10 +70,8 @@ import org.springframework.stereotype.Service;
  * forwarded to Solr's XML update handler
  * <li><strong>Markdown Processing</strong>: Support for markdown documents with
  * front matter, title, and heading extraction
- * <li><strong>Batch Processing</strong>: Efficient bulk indexing with
- * configurable batch sizes
- * <li><strong>Error Resilience</strong>: Individual document fallback when
- * batch operations fail
+ * <li><strong>Error Resilience</strong>: Individual document fallback when Solr
+ * rejects a document
  * <li><strong>Field Names</strong>: Used as given in every format; nested JSON
  * objects are flattened with underscores
  * </ul>
@@ -93,13 +93,12 @@ import org.springframework.stereotype.Service;
  * arrays by converting them to multi-valued fields that Solr natively supports.
  *
  * <p>
- * <strong>Batch Processing Strategy:</strong>
+ * <strong>Error Recovery:</strong>
  *
  * <p>
- * Uses configurable batch sizes (default 1000 documents) for optimal
- * performance. If a batch fails, the service automatically retries by indexing
- * documents individually to identify and skip problematic documents while
- * preserving valid ones.
+ * JSON and markdown documents are sent in one update request. If Solr rejects
+ * it because of a bad document, the service retries the documents individually
+ * to skip the problematic ones while preserving valid ones.
  *
  * <p>
  * <strong>Example Usage:</strong>
@@ -124,8 +123,6 @@ public class IndexingService {
 
 	private static final Logger logger = LoggerFactory.getLogger(IndexingService.class);
 
-	private static final int DEFAULT_BATCH_SIZE = 1000;
-
 	/** SolrJ client for communicating with Solr server */
 	private final SolrClient solrClient;
 
@@ -179,7 +176,7 @@ public class IndexingService {
 	 * <ol>
 	 * <li>Parse JSON string into structured documents
 	 * <li>Convert to schema-less SolrInputDocument objects
-	 * <li>Execute batch indexing with error handling
+	 * <li>Index them in one request, with error handling
 	 * <li>Commit changes to make documents searchable
 	 * </ol>
 	 *
@@ -428,107 +425,52 @@ public class IndexingService {
 	}
 
 	/**
-	 * Indexes a list of SolrInputDocument objects into a Solr collection using
-	 * batch processing.
-	 *
-	 * <p>
-	 * This method implements a robust batch indexing strategy that optimizes
-	 * performance while providing resilience against individual document failures.
-	 * It processes documents in configurable batches and includes fallback
-	 * mechanisms for error recovery.
-	 *
-	 * <p>
-	 * <strong>Batch Processing Strategy:</strong>
-	 *
-	 * <ul>
-	 * <li><strong>Batch Size</strong>: Configurable (default 1000) for optimal
-	 * performance
-	 * <li><strong>Error Recovery</strong>: Individual document retry on batch
-	 * failure
-	 * <li><strong>Success Tracking</strong>: Accurate count of successfully indexed
-	 * documents
-	 * <li><strong>Commit Strategy</strong>: Single soft commit after all batches
-	 * for consistency
-	 * </ul>
-	 *
-	 * <p>
-	 * <strong>Error Handling Workflow:</strong>
-	 *
-	 * <ol>
-	 * <li>Attempt batch indexing for optimal performance
-	 * <li>On batch failure, retry each document individually
-	 * <li>Track successful vs failed document counts
-	 * <li>Continue processing remaining batches despite failures
-	 * <li>Commit all successful changes at the end
-	 * </ol>
-	 *
-	 * <p>
-	 * <strong>Performance Considerations:</strong>
+	 * Indexes documents into a Solr collection in one update request that also
+	 * carries the soft commit, as {@link #forward} does for CSV and XML, so the
+	 * documents are searchable when this returns.
 	 *
 	 * <p>
-	 * Batch processing significantly improves indexing performance compared to
-	 * individual document operations. The fallback to individual processing ensures
-	 * maximum document ingestion even when some documents have issues.
-	 *
-	 * <p>
-	 * <strong>Transaction Behavior:</strong>
-	 *
-	 * <p>
-	 * The method soft-commits after all batches are processed, making indexed
-	 * documents immediately searchable. This ensures atomicity at the operation
-	 * level while maintaining performance through batching.
+	 * If Solr rejects the request with a 400 (a document with an unknown field or a
+	 * value of the wrong type), the documents are retried one at a time so the
+	 * valid ones are still indexed, then committed. Any other failure (unknown
+	 * collection, auth, 5xx, Solr unreachable) would fail every retry the same way,
+	 * so it propagates unchanged.
 	 *
 	 * @param collection
 	 *            the name of the Solr collection to index into
 	 * @param documents
-	 *            list of SolrInputDocument objects to index
+	 *            the documents to index
 	 * @return the number of documents successfully indexed
 	 * @throws SolrServerException
-	 *             if there are critical errors in Solr communication
+	 *             if Solr cannot be reached or the commit fails
 	 * @throws IOException
-	 *             if there are critical errors in commit operations
-	 * @see SolrInputDocument
-	 * @see SolrClient#add(String, java.util.Collection)
-	 * @see SolrClient#commit(String, boolean, boolean, boolean)
+	 *             if there are I/O errors during communication
 	 */
 	public int indexDocuments(String collection, List<SolrInputDocument> documents)
 			throws SolrServerException, IOException {
-		int successCount = 0;
-		final int batchSize = DEFAULT_BATCH_SIZE;
-
-		for (int i = 0; i < documents.size(); i += batchSize) {
-			final int endIndex = Math.min(i + batchSize, documents.size());
-			final List<SolrInputDocument> batch = documents.subList(i, endIndex);
-
-			try {
-				solrClient.add(collection, batch);
-				successCount += batch.size();
-			} catch (SolrServerException | IOException | RuntimeException e) {
-				logger.warn("Batch indexing failed, retrying individually", e);
-				// Try indexing documents individually to identify problematic ones
-				for (SolrInputDocument doc : batch) {
-					try {
-						solrClient.add(collection, doc);
-						successCount++;
-					} catch (SolrServerException | IOException | RuntimeException e2) {
-						logger.debug("Failed to index individual document", e2);
-						// Document failed to index - this is expected behavior for problematic
-						// documents
-						// We continue processing the rest of the batch
-					}
-				}
+		UpdateRequest request = new UpdateRequest();
+		request.add(documents);
+		request.setAction(AbstractUpdateRequest.ACTION.COMMIT, false, true, true);
+		try {
+			request.process(solrClient, collection);
+			return documents.size();
+		} catch (SolrException e) {
+			if (e.code() != SolrException.ErrorCode.BAD_REQUEST.code) {
+				throw e;
 			}
+			logger.warn("Solr rejected the batch, retrying documents individually", e);
 		}
 
-		try {
-			// waitFlush=false, waitSearcher=true, softCommit=true: the documents are
-			// searchable when this method returns, while the hard commit (segment fsync)
-			// is left to Solr's autoCommit, so many small calls do not each force one.
-			solrClient.commit(collection, false, true, true);
-		} catch (SolrServerException | IOException e) {
-			logger.error("Failed to commit after indexing to collection: {}", collection, e);
-			throw e;
+		int successCount = 0;
+		for (SolrInputDocument doc : documents) {
+			try {
+				solrClient.add(collection, doc);
+				successCount++;
+			} catch (SolrException e) {
+				logger.debug("Failed to index individual document", e);
+			}
 		}
+		solrClient.commit(collection, false, true, true);
 		return successCount;
 	}
 
diff --git a/src/test/java/org/apache/solr/mcp/server/indexing/IndexingServiceTest.java b/src/test/java/org/apache/solr/mcp/server/indexing/IndexingServiceTest.java
index cb42cdd8..3a9db4a8 100644
--- a/src/test/java/org/apache/solr/mcp/server/indexing/IndexingServiceTest.java
+++ b/src/test/java/org/apache/solr/mcp/server/indexing/IndexingServiceTest.java
@@ -26,9 +26,11 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.request.ContentStreamUpdateRequest;
+import org.apache.solr.client.solrj.request.UpdateRequest;
 import org.apache.solr.common.SolrInputDocument;
 import org.apache.solr.common.util.NamedList;
 import org.apache.solr.mcp.server.indexing.documentcreator.DocumentProcessingException;
@@ -71,14 +73,19 @@ class IndexingServiceTest {
 		List<Map<String, Object>> json = List.of(Map.of("id", "1", "title", "Test"));
 		List<SolrInputDocument> mockDocs = createMockDocuments(1);
 		when(indexingDocumentCreator.createSchemalessDocumentsFromJson(json)).thenReturn(mockDocs);
-		when(solrClient.add(eq("test_collection"), any(Collection.class))).thenReturn(null);
-		when(solrClient.commit("test_collection", false, true, true)).thenReturn(null);
+		when(solrClient.request(any(UpdateRequest.class), eq("test_collection"))).thenReturn(new NamedList<>());
 
 		indexingService.indexJsonDocuments("test_collection", json);
 
 		verify(indexingDocumentCreator).createSchemalessDocumentsFromJson(json);
-		verify(solrClient).add(eq("test_collection"), any(Collection.class));
-		verify(solrClient).commit("test_collection", false, true, true);
+		// the add and the commit ride on one request
+		ArgumentCaptor<UpdateRequest> captor = ArgumentCaptor.forClass(UpdateRequest.class);
+		verify(solrClient).request(captor.capture(), eq("test_collection"));
+		assertEquals(1, captor.getValue().getDocuments().size());
+		assertEquals("true", captor.getValue().getParams().get("commit"));
+		assertEquals("true", captor.getValue().getParams().get("softCommit"));
+		verify(solrClient, never()).add(anyString(), any(Collection.class));
+		verify(solrClient, never()).commit(anyString(), anyBoolean(), anyBoolean(), anyBoolean());
 	}
 
 	@Test
@@ -97,119 +104,109 @@ class IndexingServiceTest {
 	@Test
 	void indexDocuments_WithSmallBatch_ShouldIndexSuccessfully() throws Exception {
 		List<SolrInputDocument> docs = createMockDocuments(5);
-		when(solrClient.add(eq("test_collection"), any(Collection.class))).thenReturn(null);
-		when(solrClient.commit("test_collection", false, true, true)).thenReturn(null);
+		when(solrClient.request(any(UpdateRequest.class), eq("test_collection"))).thenReturn(new NamedList<>());
 
 		int result = indexingService.indexDocuments("test_collection", docs);
 
 		assertEquals(5, result);
-		verify(solrClient).add(eq("test_collection"), any(Collection.class));
-		verify(solrClient).commit("test_collection", false, true, true);
+		ArgumentCaptor<UpdateRequest> captor = ArgumentCaptor.forClass(UpdateRequest.class);
+		verify(solrClient).request(captor.capture(), eq("test_collection"));
+		assertEquals(5, captor.getValue().getDocuments().size());
+		assertEquals("true", captor.getValue().getParams().get("commit"));
+		verify(solrClient, never()).add(anyString(), any(Collection.class));
+		verify(solrClient, never()).commit(anyString(), anyBoolean(), anyBoolean(), anyBoolean());
 	}
 
 	@Test
-	void indexDocuments_WithLargeBatch_ShouldProcessInBatches() throws Exception {
+	void indexDocuments_WithManyDocuments_SendsOneRequest() throws Exception {
 		List<SolrInputDocument> docs = createMockDocuments(2500);
-		when(solrClient.add(eq("test_collection"), any(Collection.class))).thenReturn(null);
-		when(solrClient.commit(eq("test_collection"), eq(false), eq(true), eq(true))).thenReturn(null);
+		when(solrClient.request(any(UpdateRequest.class), eq("test_collection"))).thenReturn(new NamedList<>());
 
 		int result = indexingService.indexDocuments("test_collection", docs);
 
 		assertEquals(2500, result);
-		verify(solrClient, times(3)).add(eq("test_collection"), any(Collection.class));
-		verify(solrClient).commit("test_collection", false, true, true);
+		ArgumentCaptor<UpdateRequest> captor = ArgumentCaptor.forClass(UpdateRequest.class);
+		verify(solrClient).request(captor.capture(), eq("test_collection"));
+		assertEquals(2500, captor.getValue().getDocuments().size());
+		verify(solrClient, never()).add(anyString(), any(Collection.class));
 	}
-
 	@Test
-	void indexDocuments_WhenBatchFails_ShouldRetryIndividually() throws Exception {
+	void indexDocuments_WhenSolrRejectsTheRequest_ShouldRetryIndividually() throws Exception {
 		List<SolrInputDocument> docs = createMockDocuments(3);
-
-		when(solrClient.add(eq("test_collection"), any(List.class))).thenThrow(new SolrServerException("Batch error"));
-
+		when(solrClient.request(any(UpdateRequest.class), eq("test_collection")))
+				.thenThrow(new RemoteSolrException("http://localhost:8983/solr", 400, "Batch error", null));
 		when(solrClient.add(eq("test_collection"), any(SolrInputDocument.class))).thenReturn(null);
-		when(solrClient.commit("test_collection", false, true, true)).thenReturn(null);
 
 		int result = indexingService.indexDocuments("test_collection", docs);
 
 		assertEquals(3, result);
-		verify(solrClient).add(eq("test_collection"), any(Collection.class));
 		verify(solrClient, times(3)).add(eq("test_collection"), any(SolrInputDocument.class));
+		// the rejected request's commit never ran
 		verify(solrClient).commit("test_collection", false, true, true);
 	}
-
 	@Test
 	void indexDocuments_WhenSomeIndividualDocumentsFail_ShouldIndexSuccessfulOnes() throws Exception {
 		List<SolrInputDocument> docs = createMockDocuments(3);
-
-		when(solrClient.add(eq("test_collection"), any(List.class))).thenThrow(new SolrServerException("Batch error"));
-
-		when(solrClient.add(eq("test_collection"), any(SolrInputDocument.class))).thenReturn(null)
-				.thenThrow(new SolrServerException("Document error")).thenReturn(null);
-
-		when(solrClient.commit("test_collection", false, true, true)).thenReturn(null);
+		RemoteSolrException badRequest = new RemoteSolrException("http://localhost:8983/solr", 400, "Bad doc", null);
+		when(solrClient.request(any(UpdateRequest.class), eq("test_collection"))).thenThrow(badRequest);
+		when(solrClient.add(eq("test_collection"), any(SolrInputDocument.class))).thenReturn(null).thenThrow(badRequest)
+				.thenReturn(null);
 
 		int result = indexingService.indexDocuments("test_collection", docs);
 
 		assertEquals(2, result);
-		verify(solrClient).add(eq("test_collection"), any(Collection.class));
 		verify(solrClient, times(3)).add(eq("test_collection"), any(SolrInputDocument.class));
 		verify(solrClient).commit("test_collection", false, true, true);
 	}
-
-	@Test
-	void indexDocuments_WithEmptyList_ShouldStillCommit() throws Exception {
-		List<SolrInputDocument> emptyDocs = new ArrayList<>();
-		when(solrClient.commit("test_collection", false, true, true)).thenReturn(null);
-
-		int result = indexingService.indexDocuments("test_collection", emptyDocs);
-
-		assertEquals(0, result);
-		verify(solrClient, never()).add(anyString(), any(List.class));
-		verify(solrClient).commit("test_collection", false, true, true);
-	}
-
 	@Test
-	void indexDocuments_WhenCommitFails_ShouldPropagateException() throws Exception {
+	void indexDocuments_WhenTheRequestFails_ShouldPropagateException() throws Exception {
 		List<SolrInputDocument> docs = createMockDocuments(2);
-		when(solrClient.add(eq("test_collection"), any(Collection.class))).thenReturn(null);
-		when(solrClient.commit("test_collection", false, true, true)).thenThrow(new IOException("Commit failed"));
+		when(solrClient.request(any(UpdateRequest.class), eq("test_collection")))
+				.thenThrow(new IOException("Commit failed"));
 
 		assertThrows(IOException.class, () -> {
 			indexingService.indexDocuments("test_collection", docs);
 		});
-		verify(solrClient).add(eq("test_collection"), any(Collection.class));
-		verify(solrClient).commit("test_collection", false, true, true);
+		verify(solrClient).request(any(UpdateRequest.class), eq("test_collection"));
+		// not a Solr 400: no per-document fallback
+		verify(solrClient, never()).add(anyString(), any(SolrInputDocument.class));
+		verify(solrClient, never()).commit(anyString(), anyBoolean(), anyBoolean(), anyBoolean());
 	}
 
 	@Test
 	void indexDocuments_SoftCommitsSoDocumentsAreSearchableWithoutForcingAnFsync() throws Exception {
 		List<SolrInputDocument> docs = createMockDocuments(2);
-		when(solrClient.add(eq("test_collection"), any(Collection.class))).thenReturn(null);
+		when(solrClient.request(any(UpdateRequest.class), eq("test_collection"))).thenReturn(new NamedList<>());
 
 		indexingService.indexDocuments("test_collection", docs);
 
-		// waitFlush=false, waitSearcher=true, softCommit=true. waitSearcher is what
-		// keeps the documents searchable the moment the tool returns; softCommit is
-		// what avoids an fsync per call. Measured against Solr: 8.6 ms versus 18.9 ms
-		// for a hard commit, and a p90 of 10.7 ms versus 41.3 ms.
-		verify(solrClient).commit("test_collection", false, true, true);
+		// waitFlush=false, waitSearcher=true, softCommit=true, on the add request.
+		// waitSearcher is what keeps the documents searchable the moment the tool
+		// returns; softCommit is what avoids an fsync per call. Measured against
+		// Solr: 8.6 ms versus 18.9 ms for a hard commit, and a p90 of 10.7 ms versus
+		// 41.3 ms.
+		ArgumentCaptor<UpdateRequest> captor = ArgumentCaptor.forClass(UpdateRequest.class);
+		verify(solrClient).request(captor.capture(), eq("test_collection"));
+		assertEquals("true", captor.getValue().getParams().get("commit"));
+		assertEquals("true", captor.getValue().getParams().get("softCommit"));
+		assertEquals("true", captor.getValue().getParams().get("waitSearcher"));
+		verify(solrClient, never()).commit(anyString(), anyBoolean(), anyBoolean(), anyBoolean());
 		verify(solrClient, never()).commit(anyString());
 	}
 
 	@Test
-	void indexDocuments_ShouldBatchCorrectly() throws Exception {
-		List<SolrInputDocument> docs = createMockDocuments(1000);
-		when(solrClient.add(eq("test_collection"), any(Collection.class))).thenReturn(null);
-		when(solrClient.commit("test_collection", false, true, true)).thenReturn(null);
+	void indexDocuments_WhenCollectionIsMissing_ShouldPropagateWithoutRetrying() throws Exception {
+		List<SolrInputDocument> docs = createMockDocuments(3);
 
-		int result = indexingService.indexDocuments("test_collection", docs);
+		when(solrClient.request(any(UpdateRequest.class), eq("test_collection")))
+				.thenThrow(new RemoteSolrException("http://localhost:8983/solr", 404, "Collection not found", null));
 
-		assertEquals(1000, result);
+		assertThrows(RemoteSolrException.class, () -> indexingService.indexDocuments("test_collection", docs));
 
-		ArgumentCaptor<Collection<SolrInputDocument>> captor = ArgumentCaptor.forClass(Collection.class);
-		verify(solrClient).add(eq("test_collection"), captor.capture());
-		assertEquals(1000, captor.getValue().size());
-		verify(solrClient).commit("test_collection", false, true, true);
+		verify(solrClient, times(1)).request(any(UpdateRequest.class), eq("test_collection"));
+		verify(solrClient, never()).add(anyString(), any(SolrInputDocument.class));
+		verify(solrClient, never()).add(anyString(), any(Collection.class));
+		verify(solrClient, never()).commit(anyString(), anyBoolean(), anyBoolean(), anyBoolean());
 	}
 
 	@Test
@@ -217,16 +214,15 @@ class IndexingServiceTest {
 		List<Map<String, Object>> json = List.of(Map.of("id", "1"));
 		List<SolrInputDocument> mockDocs = createMockDocuments(1);
 		when(indexingDocumentCreator.createSchemalessDocumentsFromJson(json)).thenReturn(mockDocs);
-		when(solrClient.add(eq("test_collection"), any(List.class)))
-				.thenThrow(new SolrServerException("Solr connection error"));
-		when(solrClient.add(eq("test_collection"), any(SolrInputDocument.class)))
+		// Solr unreachable: propagate rather than retry and report "0 of 1"
+		when(solrClient.request(any(UpdateRequest.class), eq("test_collection")))
 				.thenThrow(new SolrServerException("Solr connection error"));
-		when(solrClient.commit("test_collection", false, true, true)).thenReturn(null);
 
-		indexingService.indexJsonDocuments("test_collection", json);
+		assertThrows(SolrServerException.class, () -> indexingService.indexJsonDocuments("test_collection", json));
 
-		verify(solrClient).add(eq("test_collection"), any(List.class));
-		verify(solrClient).add(eq("test_collection"), any(SolrInputDocument.class));
+		verify(solrClient).request(any(UpdateRequest.class), eq("test_collection"));
+		verify(solrClient, never()).add(anyString(), any(SolrInputDocument.class));
+		verify(solrClient, never()).commit(anyString(), anyBoolean(), anyBoolean(), anyBoolean());
 	}
 
 	@Test
@@ -300,21 +296,17 @@ class IndexingServiceTest {
 	}
 
 	@Test
-	void indexDocuments_WithRuntimeException_ShouldRetryIndividually() throws Exception {
+	void indexDocuments_WithRuntimeException_ShouldPropagateWithoutRetrying() throws Exception {
 		List<SolrInputDocument> docs = createMockDocuments(2);
 
-		when(solrClient.add(eq("test_collection"), any(List.class)))
+		when(solrClient.request(any(UpdateRequest.class), eq("test_collection")))
 				.thenThrow(new RuntimeException("Unexpected error"));
 
-		when(solrClient.add(eq("test_collection"), any(SolrInputDocument.class))).thenReturn(null);
-		when(solrClient.commit("test_collection", false, true, true)).thenReturn(null);
+		assertThrows(RuntimeException.class, () -> indexingService.indexDocuments("test_collection", docs));
 
-		int result = indexingService.indexDocuments("test_collection", docs);
-
-		assertEquals(2, result);
-		verify(solrClient).add(eq("test_collection"), any(Collection.class));
-		verify(solrClient, times(2)).add(eq("test_collection"), any(SolrInputDocument.class));
-		verify(solrClient).commit("test_collection", false, true, true);
+		verify(solrClient).request(any(UpdateRequest.class), eq("test_collection"));
+		verify(solrClient, never()).add(anyString(), any(SolrInputDocument.class));
+		verify(solrClient, never()).commit(anyString(), anyBoolean(), anyBoolean(), anyBoolean());
 	}
 
 	private List<SolrInputDocument> createMockDocuments(int count) {