From e9ea19dd11d27ebc1e66590ab291e4ba20b4fced Mon Sep 17 00:00:00 2001 From: Aditya Parikh Date: Sun, 13 Sep 2026 22:53:08 -0400 Subject: [PATCH 1/3] feat(indexing): index many markdown documents in one call MarkdownDocumentCreator treated the whole input as one document, so a client with sixty documents needed sixty tool calls, each a full model round trip. A new document now starts at every YAML front matter block; text before the first block is its own document, a thematic break followed by prose does not split, and input with at most one block is parsed exactly as before, with the same content-derived id. The tool description says so, and the parameter description no longer calls the input a single document. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01CiUHyyXLTo9ATdgg8eRFZJ Signed-off-by: Aditya Parikh --- README.md | 2 +- .../mcp/server/indexing/IndexingService.java | 8 +- .../MarkdownDocumentCreator.java | 97 +++++++++++- .../MarkdownDocumentCreatorTest.java | 142 ++++++++++++++++++ 4 files changed, 238 insertions(+), 11 deletions(-) create mode 100644 src/test/java/org/apache/solr/mcp/server/indexing/documentcreator/MarkdownDocumentCreatorTest.java diff --git a/README.md b/README.md index 0d0c1f7c..ebf65271 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ Using a different client, or want STDIO/HTTP/Docker options? See the per-client | `index-json-documents` | Index documents from a JSON string into a collection | | `index-csv-documents` | Index documents from a CSV string into a collection | | `index-xml-documents` | Index documents from an XML string into a collection | -| `index-markdown-documents` | Index a markdown document into a collection, extracting front matter, title, headings, and body text | +| `index-markdown-documents` | Index markdown documents into a collection (one per YAML front matter block), extracting front matter, title, headings, and body text | | `create-collection` | Create a collection (configSet, numShards, replicationFactor optional — default `_default`, `1`, `1`) | | `list-collections` | List all available Solr collections | | `get-collection-stats` | Get statistics and metrics for a collection | 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 13504967..b247dd4c 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 @@ -400,7 +400,8 @@ public String indexXmlDocuments(@McpToolParam(description = "Solr collection to } /** - * Indexes a document from a markdown string into a specified Solr collection. + * Indexes one or more documents from a markdown string into a specified Solr + * collection; each YAML front matter block starts a new document. * *

* This method serves as the primary entry point for markdown document indexing @@ -461,12 +462,13 @@ public String indexXmlDocuments(@McpToolParam(description = "Solr collection to @McpTool( name = "index-markdown-documents", annotations = @McpTool.McpAnnotations(idempotentHint = true), - description = "Index a document from markdown String into Solr collection, extracting front matter, title, headings, and body text. " + description = "Index one or more documents from a markdown String into Solr collection, extracting front matter, title, headings, and body text. " + + "A new document starts at each YAML front matter block (--- ... ---), so many documents can be sent in one call. " + "Do NOT use for JSON/CSV/XML input; use index-json-documents, index-csv-documents, or index-xml-documents instead. " + "Only convert source content to markdown when there is no dedicated tool for the source format, and supply a stable 'id' in the YAML front matter when doing so.") public String indexMarkdownDocuments(@McpToolParam(description = "Solr collection to index into") String collection, @McpToolParam( - description = "Markdown string to index, optionally starting with YAML front matter") String markdown) + description = "Markdown to index; each YAML front matter block starts a new document") String markdown) throws IOException, SolrServerException { List schemalessDoc = indexingDocumentCreator.createSchemalessDocumentsFromMarkdown(markdown); int successCount = indexDocuments(collection, schemalessDoc); diff --git a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/MarkdownDocumentCreator.java b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/MarkdownDocumentCreator.java index db560202..4fda7080 100644 --- a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/MarkdownDocumentCreator.java +++ b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/MarkdownDocumentCreator.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.HexFormat; import java.util.List; +import java.util.regex.Pattern; import org.apache.solr.common.SolrInputDocument; import org.commonmark.Extension; import org.commonmark.ext.front.matter.YamlFrontMatterExtension; @@ -92,6 +93,13 @@ public class MarkdownDocumentCreator implements SolrDocumentCreator { private static final int MAX_INPUT_SIZE_BYTES = 10 * 1024 * 1024; + /** + * A line that may appear inside a YAML front matter block: {@code key: value}, + * a {@code - item} list entry, an indented continuation, or blank. + */ + private static final Pattern FRONT_MATTER_LINE = Pattern + .compile("^(?:[A-Za-z0-9_.\\-]+:.*|\\s+-\\s.*|\\s{2,}\\S.*|\\s*)$"); + /** Solr field holding the document's unique key. */ public static final String FIELD_ID = "id"; @@ -118,15 +126,17 @@ public MarkdownDocumentCreator() { * Creates a SolrInputDocument from a markdown string. * *

- * The whole input is treated as a single document: front matter entries map to - * fields, the title is resolved from front matter or the first level-1 heading, - * all heading texts are collected into a multi-valued {@code headings} field, - * and the plain text body is stored in {@code content}. + * Each YAML front matter block starts a new document, so one string may carry + * many; text before the first block is its own document. Within a document, + * front matter entries map to fields, the title is resolved from front matter + * or the first level-1 heading, all heading texts are collected into a + * multi-valued {@code headings} field, and the plain text body is stored in + * {@code content}. * * @param markdown * markdown string, optionally starting with YAML front matter - * @return a single-element list containing the created document, or an empty - * list if the input is blank + * @return one document per front matter block (a single document when there is + * at most one), or an empty list if the input is blank * @throws DocumentProcessingException * if the input exceeds the size limit or parsing fails */ @@ -141,6 +151,79 @@ public List create(String markdown) throws DocumentProcessing return List.of(); } + List documents = new ArrayList<>(); + for (String chunk : splitDocuments(markdown)) { + if (!chunk.trim().isEmpty()) { + documents.add(parseOne(chunk)); + } + } + return documents; + } + + /** + * Splits one Markdown string into documents. A new document starts at every + * YAML front matter block (a {@code ---} line, one or more {@code key: value} + * or list-item lines, a closing {@code ---} line). Text before the first block + * is its own document. A lone {@code ---} followed by prose is a thematic break + * and does not split, and input with a single front matter block, or none, + * comes back unchanged. + */ + static List splitDocuments(String markdown) { + String[] lines = markdown.split("\n", -1); + List starts = new ArrayList<>(); + for (int i = 0; i < lines.length; i++) { + if (!isDelimiter(lines[i])) { + continue; + } + int j = i + 1; + boolean entry = false; + boolean closed = false; + while (j < lines.length) { + if (isDelimiter(lines[j])) { + closed = true; + break; + } + if (!FRONT_MATTER_LINE.matcher(lines[j]).matches()) { + break; + } + entry |= !lines[j].isBlank(); + j++; + } + if (closed && entry) { + starts.add(i); + i = j; + } + } + if (starts.size() <= 1 && (starts.isEmpty() || isBlankBefore(lines, starts.getFirst()))) { + return List.of(markdown); + } + List chunks = new ArrayList<>(); + int from = isBlankBefore(lines, starts.getFirst()) ? starts.getFirst() : 0; + for (int k = 0; k < starts.size(); k++) { + int start = starts.get(k); + if (start > from) { + chunks.add(String.join("\n", java.util.Arrays.copyOfRange(lines, from, start))); + } + from = start; + } + chunks.add(String.join("\n", java.util.Arrays.copyOfRange(lines, from, lines.length))); + return chunks; + } + + private static boolean isDelimiter(String line) { + return line.strip().equals("---"); + } + + private static boolean isBlankBefore(String[] lines, int index) { + for (int i = 0; i < index; i++) { + if (!lines[i].isBlank()) { + return false; + } + } + return true; + } + + private SolrInputDocument parseOne(String markdown) { Node document; try { document = parser.parse(markdown); @@ -174,7 +257,7 @@ public List create(String markdown) throws DocumentProcessing doc.addField(FIELD_CONTENT, content); } - return List.of(doc); + return doc; } /** diff --git a/src/test/java/org/apache/solr/mcp/server/indexing/documentcreator/MarkdownDocumentCreatorTest.java b/src/test/java/org/apache/solr/mcp/server/indexing/documentcreator/MarkdownDocumentCreatorTest.java new file mode 100644 index 00000000..9a22e7e4 --- /dev/null +++ b/src/test/java/org/apache/solr/mcp/server/indexing/documentcreator/MarkdownDocumentCreatorTest.java @@ -0,0 +1,142 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.mcp.server.indexing.documentcreator; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.apache.solr.common.SolrInputDocument; +import org.junit.jupiter.api.Test; + +/** + * One Markdown string may carry many documents: a new document starts at every + * YAML front-matter block. That lets a client send a whole dataset in one tool + * call instead of one call per document, while a single file with one front + * matter block behaves exactly as before. + */ +class MarkdownDocumentCreatorTest { + + private final MarkdownDocumentCreator creator = new MarkdownDocumentCreator(); + + @Test + void eachFrontMatterBlockStartsANewDocument() { + String markdown = """ + --- + id: show-1 + title: Stranger Things + genres: + - Sci-Fi + - Horror + --- + + # Stranger Things + + Kids in Indiana. + + --- + id: show-2 + title: Dark + --- + + # Dark + + A German town. + + --- + id: show-3 + title: Severance + --- + Office workers. + """; + + List documents = creator.create(markdown); + + assertThat(documents).hasSize(3); + assertThat(documents).extracting(d -> d.getFieldValue("id")).containsExactly("show-1", "show-2", "show-3"); + assertThat(documents).extracting(d -> d.getFieldValue("title")).containsExactly("Stranger Things", "Dark", + "Severance"); + assertThat(documents.getFirst().getFieldValues("genres")).containsExactly("Sci-Fi", "Horror"); + assertThat(documents.getFirst().getFieldValue("content")).asString().contains("Kids in Indiana") + .doesNotContain("German"); + assertThat(documents.get(2).getFieldValue("content")).asString().contains("Office workers"); + } + + @Test + void aThematicBreakInsideTheBodyDoesNotSplit() { + String markdown = """ + --- + id: one + --- + # Title + + First part. + + --- + + Second part, after a horizontal rule, still the same document. + """; + + List documents = creator.create(markdown); + + assertThat(documents).hasSize(1); + assertThat(documents.getFirst().getFieldValue("content")).asString().contains("First part") + .contains("Second part"); + } + + @Test + void generatedIdsAreDerivedPerDocument() { + String markdown = """ + --- + title: A + --- + Body A. + + --- + title: B + --- + Body B. + """; + + List documents = creator.create(markdown); + + assertThat(documents).hasSize(2); + assertThat(documents.get(0).getFieldValue("id")).isNotEqualTo(documents.get(1).getFieldValue("id")); + // The same single document on its own gets the same id as inside the batch + SolrInputDocument alone = creator.create("---\ntitle: A\n---\nBody A.\n").getFirst(); + assertThat(alone.getFieldValue("id")).isEqualTo(documents.get(0).getFieldValue("id")); + } + + @Test + void textBeforeTheFirstFrontMatterIsItsOwnDocument() { + String markdown = """ + # Preface + + Untitled notes. + + --- + id: two + --- + Second. + """; + + List documents = creator.create(markdown); + + assertThat(documents).hasSize(2); + assertThat(documents.get(0).getFieldValue("title")).isEqualTo("Preface"); + assertThat(documents.get(1).getFieldValue("id")).isEqualTo("two"); + } +} From 8db569222bd5cf608cdbc82326b5b55c9d72a35c Mon Sep 17 00:00:00 2001 From: Aditya Parikh Date: Sun, 13 Sep 2026 23:41:41 -0400 Subject: [PATCH 2/3] feat(indexing): index-markdown-documents takes a documents array Replaces the front-matter splitter with a signature change. There is no standard multi-document Markdown format, so the splitter was a convention of our own with a heuristic that could misfire; a typed array makes the document boundary part of the tool schema instead. The client sends one string per document, each goes through the unchanged single-document creator, and the model emits a JSON array natively, which it does reliably. MarkdownDocumentCreator is back to its main version; the splitter tests go with it, and IndexingServiceTest covers the per-element path. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01CiUHyyXLTo9ATdgg8eRFZJ Signed-off-by: Aditya Parikh --- README.md | 2 +- .../mcp/server/indexing/IndexingService.java | 28 ++-- .../MarkdownDocumentCreator.java | 97 +----------- .../server/McpClientIntegrationTestBase.java | 2 +- .../server/indexing/IndexingServiceTest.java | 15 ++ .../MarkdownDocumentCreatorTest.java | 142 ------------------ 6 files changed, 41 insertions(+), 245 deletions(-) delete mode 100644 src/test/java/org/apache/solr/mcp/server/indexing/documentcreator/MarkdownDocumentCreatorTest.java diff --git a/README.md b/README.md index ebf65271..1fafa419 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ Using a different client, or want STDIO/HTTP/Docker options? See the per-client | `index-json-documents` | Index documents from a JSON string into a collection | | `index-csv-documents` | Index documents from a CSV string into a collection | | `index-xml-documents` | Index documents from an XML string into a collection | -| `index-markdown-documents` | Index markdown documents into a collection (one per YAML front matter block), extracting front matter, title, headings, and body text | +| `index-markdown-documents` | Index markdown documents into a collection (one array element per document), extracting front matter, title, headings, and body text | | `create-collection` | Create a collection (configSet, numShards, replicationFactor optional — default `_default`, `1`, `1`) | | `list-collections` | List all available Solr collections | | `get-collection-stats` | Get statistics and metrics for a collection | 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 b247dd4c..9aa3c5ef 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 @@ -18,6 +18,7 @@ import io.micrometer.observation.annotation.Observed; import java.io.IOException; +import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.TreeSet; @@ -400,8 +401,8 @@ public String indexXmlDocuments(@McpToolParam(description = "Solr collection to } /** - * Indexes one or more documents from a markdown string into a specified Solr - * collection; each YAML front matter block starts a new document. + * Indexes markdown documents into a specified Solr collection, one array + * element per document. * *

* This method serves as the primary entry point for markdown document indexing @@ -448,9 +449,8 @@ public String indexXmlDocuments(@McpToolParam(description = "Solr collection to * * @param collection * the name of the Solr collection to index documents into - * @param markdown - * markdown string to index, optionally starting with YAML front - * matter + * @param documents + * the markdown documents, one string per document matter * @throws IOException * if there are critical errors in Solr communication * @throws SolrServerException @@ -462,15 +462,21 @@ public String indexXmlDocuments(@McpToolParam(description = "Solr collection to @McpTool( name = "index-markdown-documents", annotations = @McpTool.McpAnnotations(idempotentHint = true), - description = "Index one or more documents from a markdown String into Solr collection, extracting front matter, title, headings, and body text. " - + "A new document starts at each YAML front matter block (--- ... ---), so many documents can be sent in one call. " + description = "Index markdown documents into Solr collection, one array element per document, extracting front matter, title, headings, and body text from each. " + + "Pass many documents in one call rather than one call per document. " + "Do NOT use for JSON/CSV/XML input; use index-json-documents, index-csv-documents, or index-xml-documents instead. " + "Only convert source content to markdown when there is no dedicated tool for the source format, and supply a stable 'id' in the YAML front matter when doing so.") public String indexMarkdownDocuments(@McpToolParam(description = "Solr collection to index into") String collection, @McpToolParam( - description = "Markdown to index; each YAML front matter block starts a new document") String markdown) + description = "Markdown documents to index, one string per document, each optionally starting with YAML front matter") List documents) throws IOException, SolrServerException { - List schemalessDoc = indexingDocumentCreator.createSchemalessDocumentsFromMarkdown(markdown); + if (documents == null) { + throw new IllegalArgumentException("documents cannot be null"); + } + List schemalessDoc = new ArrayList<>(); + for (String markdown : documents) { + schemalessDoc.addAll(indexingDocumentCreator.createSchemalessDocumentsFromMarkdown(markdown)); + } int successCount = indexDocuments(collection, schemalessDoc); return "Successfully indexed " + successCount + " of " + schemalessDoc.size() + " documents into collection '" + collection + "'"; @@ -622,7 +628,7 @@ private static IndexTool resolveIndexTool(String format) { case "json" -> new IndexTool("index-json-documents", "json"); case "csv" -> new IndexTool("index-csv-documents", "csv"); case "xml" -> new IndexTool("index-xml-documents", "xml"); - case "markdown", "md" -> new IndexTool("index-markdown-documents", "markdown"); + case "markdown", "md" -> new IndexTool("index-markdown-documents", "documents"); default -> throw new IllegalArgumentException("format must be one of json/csv/xml/markdown, got: " + format); }; @@ -693,7 +699,7 @@ public String indexDataPrompt( Next step suggestion: once data is indexed, the `search-collection` prompt drives searching it. - """.formatted(indexTool.paramName(), collection, collection, sampleSection, indexTool.name(), + """.formatted(format.trim().toLowerCase(), collection, collection, sampleSection, indexTool.name(), collection, indexTool.paramName(), collection); } } diff --git a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/MarkdownDocumentCreator.java b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/MarkdownDocumentCreator.java index 4fda7080..db560202 100644 --- a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/MarkdownDocumentCreator.java +++ b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/MarkdownDocumentCreator.java @@ -22,7 +22,6 @@ import java.util.ArrayList; import java.util.HexFormat; import java.util.List; -import java.util.regex.Pattern; import org.apache.solr.common.SolrInputDocument; import org.commonmark.Extension; import org.commonmark.ext.front.matter.YamlFrontMatterExtension; @@ -93,13 +92,6 @@ public class MarkdownDocumentCreator implements SolrDocumentCreator { private static final int MAX_INPUT_SIZE_BYTES = 10 * 1024 * 1024; - /** - * A line that may appear inside a YAML front matter block: {@code key: value}, - * a {@code - item} list entry, an indented continuation, or blank. - */ - private static final Pattern FRONT_MATTER_LINE = Pattern - .compile("^(?:[A-Za-z0-9_.\\-]+:.*|\\s+-\\s.*|\\s{2,}\\S.*|\\s*)$"); - /** Solr field holding the document's unique key. */ public static final String FIELD_ID = "id"; @@ -126,17 +118,15 @@ public MarkdownDocumentCreator() { * Creates a SolrInputDocument from a markdown string. * *

- * Each YAML front matter block starts a new document, so one string may carry - * many; text before the first block is its own document. Within a document, - * front matter entries map to fields, the title is resolved from front matter - * or the first level-1 heading, all heading texts are collected into a - * multi-valued {@code headings} field, and the plain text body is stored in - * {@code content}. + * The whole input is treated as a single document: front matter entries map to + * fields, the title is resolved from front matter or the first level-1 heading, + * all heading texts are collected into a multi-valued {@code headings} field, + * and the plain text body is stored in {@code content}. * * @param markdown * markdown string, optionally starting with YAML front matter - * @return one document per front matter block (a single document when there is - * at most one), or an empty list if the input is blank + * @return a single-element list containing the created document, or an empty + * list if the input is blank * @throws DocumentProcessingException * if the input exceeds the size limit or parsing fails */ @@ -151,79 +141,6 @@ public List create(String markdown) throws DocumentProcessing return List.of(); } - List documents = new ArrayList<>(); - for (String chunk : splitDocuments(markdown)) { - if (!chunk.trim().isEmpty()) { - documents.add(parseOne(chunk)); - } - } - return documents; - } - - /** - * Splits one Markdown string into documents. A new document starts at every - * YAML front matter block (a {@code ---} line, one or more {@code key: value} - * or list-item lines, a closing {@code ---} line). Text before the first block - * is its own document. A lone {@code ---} followed by prose is a thematic break - * and does not split, and input with a single front matter block, or none, - * comes back unchanged. - */ - static List splitDocuments(String markdown) { - String[] lines = markdown.split("\n", -1); - List starts = new ArrayList<>(); - for (int i = 0; i < lines.length; i++) { - if (!isDelimiter(lines[i])) { - continue; - } - int j = i + 1; - boolean entry = false; - boolean closed = false; - while (j < lines.length) { - if (isDelimiter(lines[j])) { - closed = true; - break; - } - if (!FRONT_MATTER_LINE.matcher(lines[j]).matches()) { - break; - } - entry |= !lines[j].isBlank(); - j++; - } - if (closed && entry) { - starts.add(i); - i = j; - } - } - if (starts.size() <= 1 && (starts.isEmpty() || isBlankBefore(lines, starts.getFirst()))) { - return List.of(markdown); - } - List chunks = new ArrayList<>(); - int from = isBlankBefore(lines, starts.getFirst()) ? starts.getFirst() : 0; - for (int k = 0; k < starts.size(); k++) { - int start = starts.get(k); - if (start > from) { - chunks.add(String.join("\n", java.util.Arrays.copyOfRange(lines, from, start))); - } - from = start; - } - chunks.add(String.join("\n", java.util.Arrays.copyOfRange(lines, from, lines.length))); - return chunks; - } - - private static boolean isDelimiter(String line) { - return line.strip().equals("---"); - } - - private static boolean isBlankBefore(String[] lines, int index) { - for (int i = 0; i < index; i++) { - if (!lines[i].isBlank()) { - return false; - } - } - return true; - } - - private SolrInputDocument parseOne(String markdown) { Node document; try { document = parser.parse(markdown); @@ -257,7 +174,7 @@ private SolrInputDocument parseOne(String markdown) { doc.addField(FIELD_CONTENT, content); } - return doc; + return List.of(doc); } /** diff --git a/src/test/java/org/apache/solr/mcp/server/McpClientIntegrationTestBase.java b/src/test/java/org/apache/solr/mcp/server/McpClientIntegrationTestBase.java index 3b9f2302..25c025d8 100644 --- a/src/test/java/org/apache/solr/mcp/server/McpClientIntegrationTestBase.java +++ b/src/test/java/org/apache/solr/mcp/server/McpClientIntegrationTestBase.java @@ -418,7 +418,7 @@ void indexMarkdownDocumentAndFindItById() throws Exception { """; CallToolResult indexResult = mcpClient.callTool(new CallToolRequest("index-markdown-documents", - Map.of("collection", COLLECTION, "markdown", markdown))); + Map.of("collection", COLLECTION, "documents", List.of(markdown)))); assertNotNull(indexResult); assertNotError(indexResult); 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 b1491d16..7ae7323e 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 @@ -324,6 +324,21 @@ private List createMockDocuments(int count) { return docs; } + @Test + void indexMarkdownDocuments_IndexesEachElementAsItsOwnDocument() throws Exception { + when(indexingDocumentCreator.createSchemalessDocumentsFromMarkdown("# One")).thenReturn(createMockDocuments(1)); + when(indexingDocumentCreator.createSchemalessDocumentsFromMarkdown("# Two")).thenReturn(createMockDocuments(1)); + when(solrClient.add(eq("test_collection"), any(Collection.class))).thenReturn(null); + when(solrClient.commit("test_collection")).thenReturn(null); + + String result = indexingService.indexMarkdownDocuments("test_collection", List.of("# One", "# Two")); + + assertTrue(result.contains("2 of 2"), result); + verify(indexingDocumentCreator).createSchemalessDocumentsFromMarkdown("# One"); + verify(indexingDocumentCreator).createSchemalessDocumentsFromMarkdown("# Two"); + verify(solrClient, times(1)).add(eq("test_collection"), any(Collection.class)); + } + @Test void indexDataPrompt_jsonPath_referencesIndexJsonDocuments() { String sample = """ diff --git a/src/test/java/org/apache/solr/mcp/server/indexing/documentcreator/MarkdownDocumentCreatorTest.java b/src/test/java/org/apache/solr/mcp/server/indexing/documentcreator/MarkdownDocumentCreatorTest.java deleted file mode 100644 index 9a22e7e4..00000000 --- a/src/test/java/org/apache/solr/mcp/server/indexing/documentcreator/MarkdownDocumentCreatorTest.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.solr.mcp.server.indexing.documentcreator; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.List; -import org.apache.solr.common.SolrInputDocument; -import org.junit.jupiter.api.Test; - -/** - * One Markdown string may carry many documents: a new document starts at every - * YAML front-matter block. That lets a client send a whole dataset in one tool - * call instead of one call per document, while a single file with one front - * matter block behaves exactly as before. - */ -class MarkdownDocumentCreatorTest { - - private final MarkdownDocumentCreator creator = new MarkdownDocumentCreator(); - - @Test - void eachFrontMatterBlockStartsANewDocument() { - String markdown = """ - --- - id: show-1 - title: Stranger Things - genres: - - Sci-Fi - - Horror - --- - - # Stranger Things - - Kids in Indiana. - - --- - id: show-2 - title: Dark - --- - - # Dark - - A German town. - - --- - id: show-3 - title: Severance - --- - Office workers. - """; - - List documents = creator.create(markdown); - - assertThat(documents).hasSize(3); - assertThat(documents).extracting(d -> d.getFieldValue("id")).containsExactly("show-1", "show-2", "show-3"); - assertThat(documents).extracting(d -> d.getFieldValue("title")).containsExactly("Stranger Things", "Dark", - "Severance"); - assertThat(documents.getFirst().getFieldValues("genres")).containsExactly("Sci-Fi", "Horror"); - assertThat(documents.getFirst().getFieldValue("content")).asString().contains("Kids in Indiana") - .doesNotContain("German"); - assertThat(documents.get(2).getFieldValue("content")).asString().contains("Office workers"); - } - - @Test - void aThematicBreakInsideTheBodyDoesNotSplit() { - String markdown = """ - --- - id: one - --- - # Title - - First part. - - --- - - Second part, after a horizontal rule, still the same document. - """; - - List documents = creator.create(markdown); - - assertThat(documents).hasSize(1); - assertThat(documents.getFirst().getFieldValue("content")).asString().contains("First part") - .contains("Second part"); - } - - @Test - void generatedIdsAreDerivedPerDocument() { - String markdown = """ - --- - title: A - --- - Body A. - - --- - title: B - --- - Body B. - """; - - List documents = creator.create(markdown); - - assertThat(documents).hasSize(2); - assertThat(documents.get(0).getFieldValue("id")).isNotEqualTo(documents.get(1).getFieldValue("id")); - // The same single document on its own gets the same id as inside the batch - SolrInputDocument alone = creator.create("---\ntitle: A\n---\nBody A.\n").getFirst(); - assertThat(alone.getFieldValue("id")).isEqualTo(documents.get(0).getFieldValue("id")); - } - - @Test - void textBeforeTheFirstFrontMatterIsItsOwnDocument() { - String markdown = """ - # Preface - - Untitled notes. - - --- - id: two - --- - Second. - """; - - List documents = creator.create(markdown); - - assertThat(documents).hasSize(2); - assertThat(documents.get(0).getFieldValue("title")).isEqualTo("Preface"); - assertThat(documents.get(1).getFieldValue("id")).isEqualTo("two"); - } -} From 8f9cde3fa81fe94805410c74562c460f3a65bded Mon Sep 17 00:00:00 2001 From: Aditya Parikh Date: Mon, 14 Sep 2026 10:26:29 -0400 Subject: [PATCH 3/3] test(indexing): no commit stub in the markdown array test The mock returns null without it, and a strict stub on the one-argument commit would be flagged unnecessary once #196's soft commit lands. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01CiUHyyXLTo9ATdgg8eRFZJ Signed-off-by: Aditya Parikh --- .../org/apache/solr/mcp/server/indexing/IndexingServiceTest.java | 1 - 1 file changed, 1 deletion(-) 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 7ae7323e..3d887213 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 @@ -329,7 +329,6 @@ void indexMarkdownDocuments_IndexesEachElementAsItsOwnDocument() throws Exceptio when(indexingDocumentCreator.createSchemalessDocumentsFromMarkdown("# One")).thenReturn(createMockDocuments(1)); when(indexingDocumentCreator.createSchemalessDocumentsFromMarkdown("# Two")).thenReturn(createMockDocuments(1)); when(solrClient.add(eq("test_collection"), any(Collection.class))).thenReturn(null); - when(solrClient.commit("test_collection")).thenReturn(null); String result = indexingService.indexMarkdownDocuments("test_collection", List.of("# One", "# Two"));