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 c1c2a8e fix: correct indexing, collection-name and service defects
(#176)
c1c2a8e is described below
commit c1c2a8ef86ddffc9a040e218e06a2e44c250f5ed
Author: Aditya Parikh <[email protected]>
AuthorDate: Tue Aug 18 17:25:48 2026 -0400
fix: correct indexing, collection-name and service defects (#176)
Verified findings from a CodeRabbit review. Two change observable behaviour
and update the tests that pinned the old behaviour.
Silent data loss:
- JsonDocumentCreator returned an empty list for a bare JSON object, so
indexing a single document indexed nothing and still reported success. It
is now indexed as one document; testNonArrayJsonInput updated accordingly.
- processArrayField filtered out objects but not nested arrays, so a nested
array reached asString() and threw.
Wrong collection resolved:
- CollectionService.extractCollectionName truncated at any "_shard", so a
collection named orders_shard_archive resolved to orders. Anchored to the
real SolrCloud core suffix _shard<N>[_replica...]. Consequently
"collection_shard" (no shard number, not a core name) now survives intact,
and data_shard1_shard2_replica_n1 resolves to data_shard1 rather than data
- correct for a collection whose own name ends in _shard1. The two test
expectations that encoded the old behaviour are updated with comments.
Other correctness:
- SolrConfig detected the Solr path by searching the whole URL string, so a
host named "solr" matched the "/solr/" inside the authority and skipped
normalisation. Match on the URL path only. Also moved the SolrClient
factory Javadoc, which sat above jsonResponseParser(ObjectMapper).
- SearchService passed sort clauses straight to SolrQuery.SortClause, whose
constructor calls ORDER.valueOf(): a missing "order" key threw NPE and an
unknown one threw an opaque IllegalArgumentException. Validate both keys
with messages that say what to send. The search prompt also advertised a
"sortFields" parameter that does not exist; it is "sortClauses".
- SchemaService cast definition.get("name") directly, recording a null name
when absent and throwing a bare ClassCastException otherwise. It also
concatenated an exception message into JSON, so a message containing a
quote or newline produced invalid JSON.
- FieldNameSanitizer used a locale-sensitive toLowerCase(); under a Turkish
default locale 'I' maps to a dotless 'i' and changes the field name.
- CollectionUtils.getFloat's Javadoc promised 0.0f for missing values while
the method returns null.
- Dtos pinned timezone = "UTC" on the @JsonFormat patterns. With the default
ObjectMapper this is a no-op (Jackson defaults to UTC), but setting
spring.jackson.time-zone would otherwise emit local time carrying a
literal 'Z' - verified: the same instant serialises as
1970-01-01T05:30:00.000Z under Asia/Kolkata.
- logback-spring.xml scoped the HTTP CONSOLE appender to "http & !stdio".
Activating both profiles attached a stdout appender while STDIO was
transporting JSON-RPC over that same stream.
Signed-off-by: Aditya Parikh <[email protected]>
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
Co-authored-by: Eric Pugh <[email protected]>
---
.../mcp/server/collection/CollectionService.java | 21 +++++-----
.../mcp/server/collection/CollectionUtils.java | 20 +++------
.../apache/solr/mcp/server/config/SolrConfig.java | 47 ++++++++++++++--------
.../documentcreator/CsvDocumentCreator.java | 3 ++
.../documentcreator/FieldNameSanitizer.java | 13 ++++--
.../documentcreator/JsonDocumentCreator.java | 15 ++++++-
.../solr/mcp/server/schema/SchemaService.java | 32 +++++++++++++--
.../solr/mcp/server/search/SearchService.java | 36 +++++++++++++++--
src/main/resources/logback-spring.xml | 8 +++-
.../server/collection/CollectionServiceTest.java | 23 +++++++++--
.../indexing/IndexingServiceIntegrationTest.java | 7 +++-
11 files changed, 168 insertions(+), 57 deletions(-)
diff --git
a/src/main/java/org/apache/solr/mcp/server/collection/CollectionService.java
b/src/main/java/org/apache/solr/mcp/server/collection/CollectionService.java
index c082ce1..8a259e6 100644
--- a/src/main/java/org/apache/solr/mcp/server/collection/CollectionService.java
+++ b/src/main/java/org/apache/solr/mcp/server/collection/CollectionService.java
@@ -145,6 +145,14 @@ public class CollectionService {
/** Suffix pattern used to identify shard names in SolrCloud
deployments */
private static final String SHARD_SUFFIX = "_shard";
+ /**
+ * Matches a SolrCloud shard/replica suffix at the end of a core name,
e.g.
+ * {@code _shard1} or {@code _shard1_replica_n1}. Anchored so that
collection
+ * names merely containing "_shard" are left intact.
+ */
+ private static final java.util.regex.Pattern SHARD_SUFFIX_PATTERN =
java.util.regex.Pattern
+ .compile("_shard\\d+(_replica.*)?$");
+
/** Request parameter name for specifying response writer type */
private static final String WT_PARAM = "wt";
@@ -948,15 +956,10 @@ public class CollectionService {
return collectionOrShard;
}
- // Check if this looks like a shard name (contains "_shard"
pattern)
- if (collectionOrShard.contains(SHARD_SUFFIX)) {
- // Extract collection name before "_shard"
- int shardIndex =
collectionOrShard.indexOf(SHARD_SUFFIX);
- return collectionOrShard.substring(0, shardIndex);
- }
-
- // If it doesn't look like a shard name, return as-is
- return collectionOrShard;
+ // Strip only a real SolrCloud shard/replica suffix. Matching a
bare
+ // "_shard" anywhere would truncate legitimate collection names
such as
+ // "orders_shard_archive" down to "orders".
+ return
SHARD_SUFFIX_PATTERN.matcher(collectionOrShard).replaceFirst("");
}
/**
diff --git
a/src/main/java/org/apache/solr/mcp/server/collection/CollectionUtils.java
b/src/main/java/org/apache/solr/mcp/server/collection/CollectionUtils.java
index 2df9309..f64e878 100644
--- a/src/main/java/org/apache/solr/mcp/server/collection/CollectionUtils.java
+++ b/src/main/java/org/apache/solr/mcp/server/collection/CollectionUtils.java
@@ -148,12 +148,13 @@ public class CollectionUtils {
* numeric types returned by Solr.
*
* <p>
- * <strong>Default Value Behavior:</strong>
+ * <strong>Missing Value Behavior:</strong>
*
* <p>
- * Returns {@code 0.0f} for missing or null values, which is typically
the
- * desired behavior for metrics like hit ratios, performance averages,
and
- * statistical calculations where missing data should be interpreted as
zero.
+ * Returns {@code null} when the key is absent or its value is null, so
callers
+ * can distinguish "no data reported" from a measured zero. A Solr
endpoint that
+ * is unavailable reports no metric at all, which is not the same as a
hit ratio
+ * of 0.
*
* <p>
* <strong>Common Use Cases:</strong>
@@ -164,20 +165,11 @@ public class CollectionUtils {
* <li>Statistical calculations and performance indicators
* </ul>
*
- * <p>
- * <strong>Note:</strong>
- *
- * <p>
- * This method differs from {@link #getLong(NamedList, String)} by
returning a
- * default value instead of null, which is more appropriate for Float
metrics
- * that represent rates, ratios, or averages.
- *
* @param stats
* the NamedList containing the metric data to extract from
* @param key
* the key to look up in the NamedList
- * @return the Float value if found, or 0.0f if the key doesn't exist
or value
- * is null
+ * @return the Float value if found and convertible, {@code null}
otherwise
* @see Number#floatValue()
*/
public static Float getFloat(NamedList<Object> stats, String key) {
diff --git a/src/main/java/org/apache/solr/mcp/server/config/SolrConfig.java
b/src/main/java/org/apache/solr/mcp/server/config/SolrConfig.java
index ae0eae4..dceb10c 100644
--- a/src/main/java/org/apache/solr/mcp/server/config/SolrConfig.java
+++ b/src/main/java/org/apache/solr/mcp/server/config/SolrConfig.java
@@ -17,6 +17,7 @@
package org.apache.solr.mcp.server.config;
import com.fasterxml.jackson.databind.ObjectMapper;
+import java.net.URI;
import java.util.concurrent.TimeUnit;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.impl.HttpJdkSolrClient;
@@ -167,33 +168,47 @@ public class SolrConfig {
* @param properties
* the injected Solr configuration properties containing
connection
* URL
+ * @param jsonResponseParser
+ * the parser that converts Solr's JSON responses into the
NamedList
+ * tree SolrJ expects
* @return configured SolrClient instance ready for use in application
services
* @see HttpJdkSolrClient.Builder
* @see SolrConfigurationProperties#url()
*/
@Bean
- JsonResponseParser jsonResponseParser(ObjectMapper objectMapper) {
- return new JsonResponseParser(objectMapper);
+ SolrClient solrClient(SolrConfigurationProperties properties,
JsonResponseParser jsonResponseParser) {
+ return buildSolrClient(properties, jsonResponseParser);
}
+ /**
+ * Response parser used by {@link #solrClient}, requesting {@code
wt=json} and
+ * converting the response into SolrJ's
+ * {@link org.apache.solr.common.util.NamedList} tree.
+ *
+ * @param objectMapper
+ * the application's Jackson mapper, reused so Solr
responses are
+ * parsed with the same configuration as the rest of the app
+ * @return the JSON response parser
+ */
@Bean
- SolrClient solrClient(SolrConfigurationProperties properties,
JsonResponseParser jsonResponseParser) {
- String url = properties.url();
+ JsonResponseParser jsonResponseParser(ObjectMapper objectMapper) {
+ return new JsonResponseParser(objectMapper);
+ }
- // Ensure URL is properly formatted for Solr
- // The URL should end with /solr/ for proper path construction
- if (!url.endsWith("/")) {
- url = url + "/";
+ private static SolrClient buildSolrClient(SolrConfigurationProperties
properties,
+ JsonResponseParser jsonResponseParser) {
+ // Normalise against the URL's *path* only. Testing the whole
URL string
+ // would see the "/solr/" inside an authority such as
http://solr/ and
+ // wrongly conclude the path was already present.
+ URI parsed = URI.create(properties.url());
+ String path = parsed.getPath() == null ? "" : parsed.getPath();
+ if (!path.endsWith("/")) {
+ path = path + "/";
}
-
- // If URL doesn't contain /solr/ path, add it
- if (!url.endsWith("/" + SOLR_PATH) && !url.contains("/" +
SOLR_PATH)) {
- if (url.endsWith("/")) {
- url = url + SOLR_PATH;
- } else {
- url = url + "/" + SOLR_PATH;
- }
+ if (!path.contains("/" + SOLR_PATH)) {
+ path = path + SOLR_PATH;
}
+ String url = parsed.resolve(path).toString();
// JSON wire format for responses; XML wire format for update
requests.
// The default JavaBin request writer uses a binary codec that
requires
diff --git
a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/CsvDocumentCreator.java
b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/CsvDocumentCreator.java
index bf56679..6c0a664 100644
---
a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/CsvDocumentCreator.java
+++
b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/CsvDocumentCreator.java
@@ -109,6 +109,9 @@ public class CsvDocumentCreator implements
SolrDocumentCreator {
* @see FieldNameSanitizer#sanitizeFieldName(String)
*/
public List<SolrInputDocument> create(String csv) throws
DocumentProcessingException {
+ if (csv.isBlank()) {
+ throw new DocumentProcessingException("CSV input cannot
be empty");
+ }
if (csv.getBytes(StandardCharsets.UTF_8).length >
MAX_INPUT_SIZE_BYTES) {
throw new DocumentProcessingException(
"Input too large: exceeds maximum size
of " + MAX_INPUT_SIZE_BYTES + " bytes");
diff --git
a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/FieldNameSanitizer.java
b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/FieldNameSanitizer.java
index fb175a5..7b63671 100644
---
a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/FieldNameSanitizer.java
+++
b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/FieldNameSanitizer.java
@@ -16,6 +16,7 @@
*/
package org.apache.solr.mcp.server.indexing.documentcreator;
+import java.util.Locale;
import java.util.regex.Pattern;
/**
@@ -88,15 +89,21 @@ public final class FieldNameSanitizer {
* @param fieldName
* the original field name to sanitize
* @return sanitized field name compatible with Solr requirements, or
"field" if
- * input is null/empty
+ * the input is empty or sanitizes away to nothing
* @see <a href=
*
"https://solr.apache.org/guide/solr/latest/indexing-guide/fields.html">Solr
* Field Guide</a>
*/
public static String sanitizeFieldName(String fieldName) {
- // Convert to lowercase and replace invalid characters with
underscores
- String sanitized =
INVALID_CHARACTERS_PATTERN.matcher(fieldName.toLowerCase()).replaceAll("_");
+ // No null guard: this package is @NullMarked and NullAway
enforces the
+ // non-null contract at compile time. An empty input falls
through to the
+ // isEmpty() check below and yields the default name.
+
+ // Convert to lowercase and replace invalid characters with
underscores.
+ // Locale.ROOT keeps this deterministic - the default locale
would map
+ // 'I' to a dotless 'ı' under a Turkish locale and produce a
different field.
+ String sanitized =
INVALID_CHARACTERS_PATTERN.matcher(fieldName.toLowerCase(Locale.ROOT)).replaceAll("_");
// Remove leading/trailing underscores and collapse multiple
underscores
sanitized =
LEADING_TRAILING_UNDERSCORES_PATTERN.matcher(sanitized).replaceAll("");
diff --git
a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/JsonDocumentCreator.java
b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/JsonDocumentCreator.java
index 457a3cd..605e520 100644
---
a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/JsonDocumentCreator.java
+++
b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/JsonDocumentCreator.java
@@ -116,6 +116,9 @@ public class JsonDocumentCreator implements
SolrDocumentCreator {
* @see FieldNameSanitizer#sanitizeFieldName(String)
*/
public List<SolrInputDocument> create(String json) throws
DocumentProcessingException {
+ if (json.isBlank()) {
+ throw new DocumentProcessingException("JSON input
cannot be empty");
+ }
if (json.getBytes(StandardCharsets.UTF_8).length >
MAX_INPUT_SIZE_BYTES) {
throw new DocumentProcessingException(
"Input too large: exceeds maximum size
of " + MAX_INPUT_SIZE_BYTES + " bytes");
@@ -134,6 +137,14 @@ public class JsonDocumentCreator implements
SolrDocumentCreator {
addAllFieldsFlat(doc, item, "");
documents.add(doc);
}
+ } else if (rootNode.isObject()) {
+ // A single document. Previously fell through
and returned an empty
+ // list, so indexing one object silently
indexed nothing.
+ SolrInputDocument doc = new SolrInputDocument();
+ addAllFieldsFlat(doc, rootNode, "");
+ documents.add(doc);
+ } else {
+ throw new DocumentProcessingException("JSON
input must be an object or an array of objects");
}
} catch (IOException e) {
throw new DocumentProcessingException("Failed to parse
JSON document", e);
@@ -219,7 +230,9 @@ public class JsonDocumentCreator implements
SolrDocumentCreator {
private void processArrayField(SolrInputDocument doc, JsonNode
arrayValue, String fieldName) {
List<Object> values = new ArrayList<>();
for (JsonNode item : arrayValue) {
- if (!item.isObject()) {
+ // Skip objects and nested arrays alike: neither has a
scalar
+ // representation, and asString() on a container node
throws.
+ if (!item.isObject() && !item.isArray()) {
values.add(convertJsonValue(item));
}
}
diff --git a/src/main/java/org/apache/solr/mcp/server/schema/SchemaService.java
b/src/main/java/org/apache/solr/mcp/server/schema/SchemaService.java
index 07253b6..3f3bb96 100644
--- a/src/main/java/org/apache/solr/mcp/server/schema/SchemaService.java
+++ b/src/main/java/org/apache/solr/mcp/server/schema/SchemaService.java
@@ -185,7 +185,10 @@ public class SchemaService {
try {
return toJson(objectMapper, getSchema(collection));
} catch (Exception e) {
- return "{\"error\": \"" + e.getMessage() + "\"}";
+ // Serialise via Jackson rather than concatenating: an
exception message
+ // containing a quote, backslash or newline would
otherwise emit invalid
+ // JSON to the MCP client.
+ return toJson(objectMapper, Map.of("error",
String.valueOf(e.getMessage())));
}
}
@@ -316,7 +319,7 @@ public class SchemaService {
List<String> names = new ArrayList<>(fields.size());
List<SchemaRequest.Update> updates = new
ArrayList<>(fields.size());
for (Map<String, Object> field : fields) {
- names.add((String) field.get("name"));
+ names.add(requireName(field, "field"));
updates.add(new SchemaRequest.AddField(field));
}
@@ -366,7 +369,7 @@ public class SchemaService {
List<String> names = new ArrayList<>(fieldTypes.size());
List<SchemaRequest.Update> updates = new
ArrayList<>(fieldTypes.size());
for (Map<String, Object> fieldType : fieldTypes) {
- names.add((String) fieldType.get("name"));
+ names.add(requireName(fieldType, "field type"));
updates.add(new
SchemaRequest.AddFieldType(toFieldTypeDefinition(fieldType)));
}
@@ -374,6 +377,29 @@ public class SchemaService {
return new SchemaUpdateResult(collection, names);
}
+ /**
+ * Extracts and validates the {@code name} entry of a schema definition.
+ *
+ * <p>
+ * Casting {@code get("name")} directly would record a {@code null}
name for a
+ * definition that omits it, or throw a bare {@link ClassCastException}
if it is
+ * not a string - neither tells the caller which entry was malformed.
+ *
+ * @param definition
+ * the caller-supplied field or field-type definition
+ * @param kind
+ * human-readable noun used in the error message
+ * @return the validated name
+ */
+ private static String requireName(Map<String, Object> definition,
String kind) {
+ Object name = definition.get("name");
+ if (!(name instanceof String s) || s.isBlank()) {
+ throw new IllegalArgumentException(
+ "Each " + kind + " definition requires
a non-empty string 'name'; got: " + name);
+ }
+ return s;
+ }
+
/**
* Builds a {@link FieldTypeDefinition} from a flat input map matching
the Solr
* Schema API add-field-type JSON shape. SolrJ's {@code
FieldTypeDefinition}
diff --git a/src/main/java/org/apache/solr/mcp/server/search/SearchService.java
b/src/main/java/org/apache/solr/mcp/server/search/SearchService.java
index 1235362..0619fec 100644
--- a/src/main/java/org/apache/solr/mcp/server/search/SearchService.java
+++ b/src/main/java/org/apache/solr/mcp/server/search/SearchService.java
@@ -21,6 +21,7 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrServerException;
@@ -293,9 +294,7 @@ public class SearchService {
// sorting
if (!CollectionUtils.isEmpty(sortClauses)) {
- solrQuery.setSorts(sortClauses.stream()
- .map(sortClause -> new
SolrQuery.SortClause(sortClause.get(SORT_ITEM), sortClause.get(SORT_ORDER)))
- .toList());
+
solrQuery.setSorts(sortClauses.stream().map(SearchService::toSortClause).toList());
}
// pagination
@@ -321,6 +320,35 @@ public class SearchService {
return new SearchResponse(documents.getNumFound(),
documents.getStart(), documents.getMaxScore(), docs, facets);
}
+ /**
+ * Builds a {@link SolrQuery.SortClause} from one caller-supplied map.
+ *
+ * <p>
+ * Both keys are validated up front: {@code SortClause}'s constructor
calls
+ * {@code ORDER.valueOf(order)}, which throws {@link
NullPointerException} on a
+ * missing order and an opaque {@link IllegalArgumentException} on an
+ * unrecognised one. Callers are LLMs, so the message needs to say what
to send.
+ */
+ private static SolrQuery.SortClause toSortClause(Map<String, String>
sortClause) {
+ String field = sortClause.get(SORT_ITEM);
+ String order = sortClause.get(SORT_ORDER);
+ if (field == null || field.isBlank()) {
+ throw new IllegalArgumentException("Each sort clause
requires a non-empty '" + SORT_ITEM + "' key");
+ }
+ if (order == null || order.isBlank()) {
+ throw new IllegalArgumentException(
+ "Sort clause for '" + field + "'
requires a '" + SORT_ORDER + "' key of 'asc' or 'desc'");
+ }
+ SolrQuery.ORDER parsed;
+ try {
+ parsed =
SolrQuery.ORDER.valueOf(order.toLowerCase(Locale.ROOT));
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException(
+ "Unsupported sort order '" + order + "'
for '" + field + "'; expected 'asc' or 'desc'", e);
+ }
+ return new SolrQuery.SortClause(field, parsed);
+ }
+
/**
* MCP prompt that guides the client through translating a
natural-language
* question into a Solr query: inspect the schema, build the query, run
the
@@ -376,7 +404,7 @@ public class SearchService {
3. Run the search.
- Call `search` with `collection=%s` and the
chosen `query` plus optional
- `filterQueries`, `facetFields`,
`sortFields`, `start`, `rows`. Set `rows=10` for a
+ `filterQueries`, `facetFields`,
`sortClauses`, `start`, `rows`. Set `rows=10` for a
focused look or `rows=0` if you only need
counts / facets.
4. Interpret and refine.
diff --git a/src/main/resources/logback-spring.xml
b/src/main/resources/logback-spring.xml
index e85a13b..0045aac 100644
--- a/src/main/resources/logback-spring.xml
+++ b/src/main/resources/logback-spring.xml
@@ -35,7 +35,13 @@
under the STDIO profile. This avoids logback "not referenced" warnings
and keeps STDIO stdout completely clean.
-->
- <springProfile name="http">
+ <!--
+ "http & !stdio": if both profiles are activated at once
(PROFILES=stdio,http)
+ the CONSOLE appender would write diagnostics to stdout, corrupting the
MCP
+ JSON-RPC stream that STDIO transports over it. Keeping stdio dominant
here
+ means the combination degrades safely instead of silently breaking.
+ -->
+ <springProfile name="http & !stdio">
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN:-%d{yyyy-MM-dd HH:mm:ss.SSS}
%5p --- [%15.15t] %-40.40logger{39} : %m%n}
diff --git
a/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceTest.java
b/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceTest.java
index d482c17..d0772ec 100644
---
a/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceTest.java
+++
b/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceTest.java
@@ -152,11 +152,24 @@ class CollectionServiceTest {
void extractCollectionName_EdgeCases_ShouldHandleCorrectly() {
// Test various edge cases
assertEquals("a",
collectionService.extractCollectionName("a_shard1"));
- assertEquals("collection",
collectionService.extractCollectionName("collection_shard"));
+ // "collection_shard" has no shard number, so it is not a
SolrCloud core
+ // name - it is a collection that merely ends in "_shard" and
must survive
+ // unchanged. Stripping it would also corrupt names like
+ // "orders_shard_archive".
+ assertEquals("collection_shard",
collectionService.extractCollectionName("collection_shard"));
assertEquals("test_name",
collectionService.extractCollectionName("test_name"));
assertEquals("",
collectionService.extractCollectionName("_shard1"));
}
+ @Test
+ void extractCollectionName_WithShardWordInsideName_ShouldNotTruncate() {
+ // Regression: a bare "contains(_shard)" check truncated this
to "orders".
+ assertEquals("orders_shard_archive",
collectionService.extractCollectionName("orders_shard_archive"));
+ // ...while a genuine core name for that same collection still
resolves.
+ assertEquals("orders_shard_archive",
+
collectionService.extractCollectionName("orders_shard_archive_shard2_replica_n1"));
+ }
+
@Test
void
extractCollectionName_WithShardInMiddleOfName_ShouldExtractCorrectly() {
// Given - "shard" appears in collection name but not as suffix
pattern
@@ -170,15 +183,17 @@ class CollectionServiceTest {
}
@Test
- void
extractCollectionName_WithMultipleOccurrencesOfShard_ShouldUseFirst() {
- // Given
+ void
extractCollectionName_WithMultipleOccurrencesOfShard_ShouldStripOnlyTheCoreSuffix()
{
+ // Given - a collection whose own name ends in "_shard1".
SolrCloud names
+ // its cores "<collection>_shard<N>_replica_<type><M>", so this
core belongs
+ // to the collection "data_shard1", not "data".
String name = "data_shard1_shard2_replica_n1";
// When
String result = collectionService.extractCollectionName(name);
// Then
- assertEquals("data", result, "Should use first occurrence of
'_shard'");
+ assertEquals("data_shard1", result, "Should strip only the
trailing shard/replica suffix");
}
// Health check tests
diff --git
a/src/test/java/org/apache/solr/mcp/server/indexing/IndexingServiceIntegrationTest.java
b/src/test/java/org/apache/solr/mcp/server/indexing/IndexingServiceIntegrationTest.java
index daa9561..e4f7516 100644
---
a/src/test/java/org/apache/solr/mcp/server/indexing/IndexingServiceIntegrationTest.java
+++
b/src/test/java/org/apache/solr/mcp/server/indexing/IndexingServiceIntegrationTest.java
@@ -697,9 +697,12 @@ class IndexingServiceIntegrationTest {
// Create documents
List<SolrInputDocument> documents =
indexingDocumentCreator.createSchemalessDocumentsFromJson(json);
- // Verify no documents were created since input is not an array
+ // A bare object is indexed as a single document. This
previously returned
+ // an empty list, so indexing one object silently indexed
nothing and still
+ // reported success.
assertNotNull(documents);
- assertEquals(0, documents.size());
+ assertEquals(1, documents.size());
+ assertEquals("single_object_001",
documents.getFirst().getFieldValue("id"));
}
@Test