Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion .github/workflows/build-and-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,40 @@ jobs:
retention-days: 7

# ============================================================================
# Job 2: Publish Docker Images
# Job 2: Solr Version Compatibility Tests
# ============================================================================
# Tests the server against multiple Solr versions using Testcontainers.
# Runs in parallel so each Solr version is an independent job.
# Tested versions: 8.11, 9.4, 9.9, 9.10, 10
# ============================================================================
solr-compatibility:
name: Solr ${{ matrix.solr-version }} Compatibility
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
solr-version:
- "8.11-slim"
- "9.4-slim"
- "9.9-slim"
- "9.10-slim"
- "10-slim"

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Java
uses: ./.github/actions/setup-java

- name: Run tests against Solr ${{ matrix.solr-version }}
env:
SOLR_VERSION: ${{ matrix.solr-version }}
run: ./gradlew test "-Dsolr.test.image=solr:${SOLR_VERSION}"

# ============================================================================
# Job 3: Publish Docker Images
# ============================================================================
# This job builds multi-platform Docker images using Jib and publishes them
# to GitHub Container Registry (GHCR) and Docker Hub.
Expand Down
20 changes: 13 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,18 +81,24 @@ The Solr Docker image used in tests is configurable via the `solr.test.image` sy
./gradlew test -Dsolr.test.image=solr:8.11-slim # Solr 8.11
./gradlew test -Dsolr.test.image=solr:9.4-slim # Solr 9.4
./gradlew test -Dsolr.test.image=solr:9.9-slim # Solr 9.9 (default)
./gradlew test -Dsolr.test.image=solr:9.10-slim # Solr 9.10
./gradlew test -Dsolr.test.image=solr:10-slim # Solr 10
```

**Tested compatible versions:** 8.11, 9.4, 9.9
**Tested compatible versions:** 8.11, 9.4, 9.9, 9.10, 10

### Solr 10 Compatibility Notes
### Solr 10 Compatibility

Solr 10 introduces breaking changes that will require updates to this project:
Solr 10.0.0 is fully supported with the JSON wire format. The `/admin/mbeans` endpoint was
removed in Solr 10; `getCacheMetrics()` and `getHandlerMetrics()` now catch `RuntimeException`
(which covers `RemoteSolrException`) so they degrade gracefully and return `null`. Tests that
check `cacheStats` and `handlerStats` already handle `null` values.

- **MBeans removal:** `SolrInfoMBeanHandler` is removed. `CollectionService.getCollectionStats()` uses `/admin/mbeans` for cache and handler metrics — this will need to migrate to the `/admin/metrics` endpoint or OpenTelemetry.
- **Metrics migration:** Dropwizard metrics replaced by OpenTelemetry. All metric names switch to snake_case. JMX, Prometheus exporter, SLF4J, and Graphite reporters are removed.
- **SolrJ base URL:** SolrClient now only accepts root URLs (e.g., `http://host:8983/solr`). This project already uses root URLs with per-request collection names, so **no change needed** here.
- **SolrJ dependency:** Upgrade `solr-solrj` from 9.x to 10.x in `gradle/libs.versions.toml`. The Jetty BOM alignment (`jetty = "10.0.22"`) will also need updating since Solr 10 uses Jetty 12.x.
Remaining known differences from Solr 9:
- **`/admin/mbeans` removed:** Cache and handler stats from `getCollectionStats()` will always be `null` on Solr 10. A future migration to `/admin/metrics` will restore these metrics.
- **Metrics migration:** Dropwizard metrics replaced by OpenTelemetry. Metric names switch to snake_case in Solr 10.
- **SolrJ base URL:** Already uses root URLs — **no change needed**.
- **SolrJ 10.x dependency:** Not yet on Maven Central (as of 2026-03-06); tests use SolrJ 9.x against a Solr 10 server. Update `solr-solrj` and Jetty BOM when 10.x is released.

## Key Configuration

Expand Down
6 changes: 1 addition & 5 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,7 @@ dependencies {
implementation(libs.spring.boot.starter.actuator)
implementation(libs.spring.boot.starter.aop)
implementation(libs.spring.ai.starter.mcp.server.webmvc)
implementation(libs.solr.solrj) {
exclude(group = "org.apache.httpcomponents")
}
implementation(libs.solr.solrj)
implementation(libs.commons.csv)
// JSpecify for nullability annotations
implementation(libs.jspecify)
Expand All @@ -126,8 +124,6 @@ dependencies {
dependencyManagement {
imports {
mavenBom("org.springframework.ai:spring-ai-bom:${libs.versions.spring.ai.get()}")
// Align Jetty family to 10.x compatible with SolrJ 9.x
mavenBom("org.eclipse.jetty:jetty-bom:${libs.versions.jetty.get()}")
}
}

Expand Down
4 changes: 1 addition & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ spotless = "7.0.2"

# Main dependencies
spring-ai = "1.1.3"
solr = "9.9.0"
solr = "10.0.0"
commons-csv = "1.10.0"
jspecify = "1.0.0"
mcp-server-security = "0.0.4"
Expand All @@ -17,8 +17,6 @@ mcp-server-security = "0.0.4"
errorprone-core = "2.38.0"
nullaway = "0.12.7"

# Jetty BOM version
jetty = "10.0.22"

# Test dependencies
testcontainers = "1.21.3"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@
import java.util.Date;
import java.util.List;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.request.CollectionAdminRequest;
import org.apache.solr.client.solrj.request.GenericSolrRequest;
import org.apache.solr.client.solrj.request.LukeRequest;
import org.apache.solr.client.solrj.request.SolrQuery;
import org.apache.solr.client.solrj.response.CollectionAdminResponse;
import org.apache.solr.client.solrj.response.LukeResponse;
import org.apache.solr.client.solrj.response.QueryResponse;
Expand Down Expand Up @@ -352,7 +352,7 @@ public List<String> listCollections() {
@SuppressWarnings("unchecked")
List<String> collections = (List<String>) response.getResponse().get(COLLECTIONS_KEY);
return collections != null ? collections : new ArrayList<>();
} catch (SolrServerException | IOException e) {
} catch (SolrServerException | IOException _) {
return new ArrayList<>();
}
}
Expand Down Expand Up @@ -595,7 +595,9 @@ public CacheStats getCacheMetrics(String collection) {
}

return stats;
} catch (SolrServerException | IOException _) {
} catch (SolrServerException | IOException | RuntimeException _) {
// RuntimeException covers SolrException subclasses (e.g. RemoteSolrException)
// thrown when the /admin/mbeans endpoint is unavailable (removed in Solr 10).
return null; // Return null instead of empty object
}
}
Expand Down Expand Up @@ -767,7 +769,9 @@ public HandlerStats getHandlerMetrics(String collection) {
}

return stats;
} catch (SolrServerException | IOException _) {
} catch (SolrServerException | IOException | RuntimeException _) {
// RuntimeException covers SolrException subclasses (e.g. RemoteSolrException)
// thrown when the /admin/mbeans endpoint is unavailable (removed in Solr 10).
return null; // Return null instead of empty object
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.apache.solr.client.solrj.ResponseParser;
import org.apache.solr.client.solrj.response.ResponseParser;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrException;
Expand Down Expand Up @@ -80,8 +80,8 @@ public String getWriterType() {
}

@Override
public String getContentType() {
return MediaType.APPLICATION_JSON_VALUE;
public Collection<String> getContentTypes() {
return List.of(MediaType.APPLICATION_JSON_VALUE);
}

@Override
Expand All @@ -93,15 +93,6 @@ public NamedList<Object> processResponse(InputStream body, String encoding) {
}
}

@Override
public NamedList<Object> processResponse(Reader reader) {
try {
return toNamedList(mapper.readTree(reader));
} catch (IOException e) {
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Failed to parse Solr JSON response", e);
}
}

private SimpleOrderedMap<Object> toNamedList(JsonNode objectNode) {
SimpleOrderedMap<Object> result = new SimpleOrderedMap<>();
objectNode.fields().forEachRemaining(entry -> result.add(entry.getKey(), convertValue(entry.getValue())));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.concurrent.TimeUnit;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.impl.Http2SolrClient;
import org.apache.solr.client.solrj.impl.HttpJdkSolrClient;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand Down Expand Up @@ -94,7 +94,7 @@
* </ul>
*
* @see SolrConfigurationProperties
* @see Http2SolrClient
* @see HttpJdkSolrClient
* @see org.springframework.boot.context.properties.EnableConfigurationProperties
*/
@Configuration
Expand Down Expand Up @@ -162,7 +162,7 @@ public class SolrConfig {
* the injected Solr configuration properties containing connection
* URL
* @return configured SolrClient instance ready for use in application services
* @see Http2SolrClient.Builder
* @see HttpJdkSolrClient.Builder
* @see SolrConfigurationProperties#url()
*/
@Bean
Expand Down Expand Up @@ -190,7 +190,7 @@ SolrClient solrClient(SolrConfigurationProperties properties, JsonResponseParser
}

// Use with explicit base URL; JSON wire format replaces the JavaBin default
return new Http2SolrClient.Builder(url).withConnectionTimeout(CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS)
return new HttpJdkSolrClient.Builder(url).withConnectionTimeout(CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS)
.withIdleTimeout(SOCKET_TIMEOUT_MS, TimeUnit.MILLISECONDS).withResponseParser(jsonResponseParser)
.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
import java.util.List;
import java.util.Map;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.request.SolrQuery;
import org.apache.solr.client.solrj.response.FacetField;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocument;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import static org.junit.jupiter.api.Assertions.*;

import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.impl.Http2SolrClient;
import org.apache.solr.client.solrj.impl.HttpJdkSolrClient;
import org.apache.solr.mcp.server.TestcontainersConfiguration;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
Expand Down Expand Up @@ -49,9 +49,9 @@ void testSolrClientConfiguration() {

// Verify that the SolrClient is using the correct URL
// Note: SolrConfig normalizes the URL to have trailing slash, but
// Http2SolrClient removes
// HttpJdkSolrClient removes
// it
var httpSolrClient = assertInstanceOf(Http2SolrClient.class, solrClient);
var httpSolrClient = assertInstanceOf(HttpJdkSolrClient.class, solrClient);
String expectedUrl = "http://" + solrContainer.getHost() + ":" + solrContainer.getMappedPort(8983) + "/solr";
assertEquals(expectedUrl, httpSolrClient.getBaseURL());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.impl.Http2SolrClient;
import org.apache.solr.client.solrj.impl.HttpJdkSolrClient;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -45,7 +45,7 @@ void testUrlNormalization(String inputUrl, String expectedUrl) throws Exception
try (SolrClient client = solrConfig.solrClient(testProperties, new JsonResponseParser(objectMapper))) {
assertNotNull(client);

var httpClient = assertInstanceOf(Http2SolrClient.class, client);
var httpClient = assertInstanceOf(HttpJdkSolrClient.class, client);
assertEquals(expectedUrl, httpClient.getBaseURL());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
import java.util.List;
import java.util.Map;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.request.SolrQuery;
import org.apache.solr.client.solrj.response.FacetField;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocument;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@
import java.util.Map;
import java.util.OptionalDouble;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.request.CollectionAdminRequest;
import org.apache.solr.client.solrj.request.SolrQuery;
import org.apache.solr.client.solrj.response.FacetField;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocument;
Expand Down