升级到Spring Data Elasticsearch 5后如何正确执行Elasticsearch查询?
升级背景
将Maven项目从Spring Boot 2.7升级到3.1.5,同步完成以下升级:
- Spring Data Elasticsearch 至5.1版本
- Elasticsearch 从7.17.2升级到8.7版本
核心问题1:新旧Query API不兼容
升级后,原org.elasticsearch.index.query.QueryBuilders所在依赖被移除,默认使用新的co.elastic.clients.elasticsearch._types.query_dsl.QueryBuilders构建查询,但新的Query类型与旧版本依赖的ElasticsearchOperations不兼容。
尝试切换到支持新Query类的co.elastic.clients.elasticsearch.ElasticsearchClient,自动注入后执行搜索时抛出**"Connection is closed"**错误,完整堆栈信息如下:
Unsatisfied dependency expressed through constructor parameter 4: Error creating bean with name 'myESRepository' defined in org.MyProject....MyESRepository defined in @EnableElasticsearchRepositories declared on IndexingConfiguration: Failed to instantiate [org.springframework.data.elasticsearch.repository.support.SimpleElasticsearchRepository]: Constructor threw exception at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:801) at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:240) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1352) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1189) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:560) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:520) at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:325) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:323) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:973) at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:950) at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:616) at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:738) at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:440) at org.springframework.boot.SpringApplication.run(SpringApplication.java:316) at org.springframework.boot.test.context.SpringBootContextLoader.lambda$loadContext$3(SpringBootContextLoader.java:137) at org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:58) at org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:46) at org.springframework.boot.SpringApplication.withHook(SpringApplication.java:1406) at org.springframework.boot.test.context.SpringBootContextLoader$ContextLoaderHook.run(SpringBootContextLoader.java:545) at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:137) at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:108) at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:187) at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:119) ... 73 more Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'myESRepository' defined in org.blah.MyESRepository defined in @EnableElasticsearchRepositories declared on IndexingConfiguration: Failed to instantiate [org.springframework.data.elasticsearch.repository.support.SimpleElasticsearchRepository]: Constructor threw exception at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1770) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:598) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:520) at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:325) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:323) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254) at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1417) at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1337) at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:910) at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:788) ... 97 more Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.data.elasticsearch.repository.support.SimpleElasticsearchRepository]: Constructor threw exception at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:224) at org.springframework.data.repository.core.support.RepositoryFactorySupport.lambda$instantiateClass$5(RepositoryFactorySupport.java:571) at java.base/java.util.Optional.map(Optional.java:260) at org.springframework.data.repository.core.support.RepositoryFactorySupport.instantiateClass(RepositoryFactorySupport.java:571) at org.springframework.data.repository.core.support.RepositoryFactorySupport.getTargetRepositoryViaReflection(RepositoryFactorySupport.java:536) at org.springframework.data.elasticsearch.repository.support.ElasticsearchRepositoryFactory.getTargetRepository(ElasticsearchRepositoryFactory.java:79) at org.springframework.data.repository.core.support.RepositoryFactorySupport.getRepository(RepositoryFactorySupport.java:317) at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.lambda$afterPropertiesSet$5(RepositoryFactoryBeanSupport.java:279) at org.springframework.data.util.Lazy.getNullable(Lazy.java:245) at org.springframework.data.util.Lazy.get(Lazy.java:114) at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.afterPropertiesSet(RepositoryFactoryBeanSupport.java:285) at org.springframework.data.elasticsearch.repository.support.ElasticsearchRepositoryFactoryBean.afterPropertiesSet(ElasticsearchRepositoryFactoryBean.java:69) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1817) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1766) ... 108 more Caused by: org.springframework.dao.DataAccessResourceFailureException: Connection is closed at org.springframework.data.elasticsearch.client.elc.ElasticsearchExceptionTranslator.translateExceptionIfPossible(ElasticsearchExceptionTranslator.java:107) at org.springframework.data.elasticsearch.client.elc.ElasticsearchExceptionTranslator.translateException(ElasticsearchExceptionTranslator.java:63) at org.springframework.data.elasticsearch.client.elc.ChildTemplate.execute(ChildTemplate.java:73) at org.springframework.data.elasticsearch.client.elc.IndicesTemplate.doExists(IndicesTemplate.java:177) at org.springframework.data.elasticsearch.client.elc.IndicesTemplate.exists(IndicesTemplate.java:169) at org.springframework.data.elasticsearch.repository.support.SimpleElasticsearchRepository.<init>(SimpleElasticsearchRepository.java:83) at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:77) at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499) at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480) at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:211) ... 121 more Caused by: java.lang.RuntimeException: Connection is closed at org.springframework.data.elasticsearch.client.elc.ElasticsearchExceptionTranslator.translateException(ElasticsearchExceptionTranslator.java:62) ... 131 more Caused by: org.apache.http.ConnectionClosedException: Connection is closed at org.elasticsearch.client.RestClient.extractAndWrapCause(RestClient.java:920) at org.elasticsearch.client.RestClient.performRequest(RestClient.java:300) at org.elasticsearch.client.RestClient.performRequest(RestClient.java:288) at co.elastic.clients.transport.rest_client.RestClientTransport.performRequest(RestClientTransport.java:153) at co.elastic.clients.elasticsearch.indices.ElasticsearchIndicesClient.exists(ElasticsearchIndicesClient.java:620) at org.springframework.data.elasticsearch.client.elc.IndicesTemplate.lambda$doExists$2(IndicesTemplate.java:177) at org.springframework.data.elasticsearch.client.elc.ChildTemplate.execute(ChildTemplate.java:71) ... 130 more Caused by: org.apache.http.ConnectionClosedException: Connection is closed at org.apache.http.nio.protocol.HttpAsyncRequestExecutor.endOfInput(HttpAsyncRequestExecutor.java:356) at org.apache.http.impl.nio.DefaultNHttpClientConnection.consumeInput(DefaultNHttpClientConnection.java:261) at org.apache.http.impl.nio.client.InternalIODispatch.onInputReady(InternalIODispatch.java:87) at org.apache.http.impl.nio.client.InternalIODispatch.onInputReady(InternalIODispatch.java:40) at org.apache.http.impl.nio.reactor.AbstractIODispatch.inputReady(AbstractIODispatch.java:114) at org.apache.http.impl.nio.reactor.BaseIOReactor.readable(BaseIOReactor.java:162) at org.apache.http.impl.nio.reactor.AbstractIOReactor.processEvent(AbstractIOReactor.java:337) at org.apache.http.impl.nio.reactor.AbstractIOReactor.processEvents(AbstractIOReactor.java:315) at org.apache.http.impl.nio.reactor.AbstractIOReactor.execute(AbstractIOReactor.java:276) at org.apache.http.impl.nio.reactor.BaseIOReactor.execute(BaseIOReactor.java:104) at org.apache.http.impl.nio.reactor.AbstractMultiworkerIOReactor$Worker.run(AbstractMultiworkerIOReactor.java:591) at java.base/java.lang.Thread.run(Thread.java:833)
测试环境配置
旧版本中通过@Autowired注入ElasticsearchOperations,集成测试共享Elasticsearch Testcontainer,实现以下接口:
@SpringBootTest @Testcontainers public interface ElasticsearchIntegrationTest { final DockerImageName ES_IMAGE = DockerImageName.parse("docker.elastic.co/elasticsearch/elasticsearch:8.7.0"); static ElasticsearchContainer elasticsearchContainer = new ElasticsearchContainer(ES_IMAGE); @DynamicPropertySource static void elasticsearchProperties(final DynamicPropertyRegistry dynamicPropertyRegistry) { final String username = "elastic"; final String password = "password"; elasticsearchContainer.withPassword(password); elasticsearchContainer.start(); dynamicPropertyRegistry.add("spring.elasticsearch.username", () -> username); dynamicPropertyRegistry.add("spring.elasticsearch.password", () -> password); dynamicPropertyRegistry.add("spring.elasticsearch.uris", () -> List.of(elasticsearchContainer.getHttpHostAddress())); } }
核心问题2:旧API路径走不通
手动添加被移除的Elasticsearch依赖后,仍可使用旧Query类型,但NativeQuery已被弃用:
<dependency> <groupId>org.elasticsearch</groupId> <artifactId>elasticsearch</artifactId> <version>8.11.3</version> </dependency>
旧测试代码依赖NativeSearchQueryBuilder,但该类已在Spring Data 5中被移除:
class MyIndexIntegrationTest implements ElasticsearchIntegrationTest { @Autowired private ElasticsearchOperations elasticsearchOperations; @Test void testSearchMyIndex() { final String matchThis = "hello"; final Query field1Query = new NativeSearchQueryBuilder() // Removed in Spring Data 5 .withQuery(QueryBuilders.matchQuery("field1", matchThis)) .build(); final List<MyIndex> actualIndexed = elasticsearchOperations .search(field1Query, MyIndex.class) .stream() .map(SearchHit::getContent) .toList(); // assertions etc } }
当前困境
- 使用新API的
QueryBuilders构建的Query无法兼容ElasticsearchOperations - 使用旧API则依赖已弃用的
NativeQuery,不符合升级趋势 - 切换到
ElasticsearchClient后出现连接关闭错误
解决方案建议
方案1:使用Spring Data Elasticsearch 5.x的新Query API兼容ElasticsearchOperations
Spring Data Elasticsearch 5.x提供了基于Elasticsearch Client的ElasticsearchOperations实现,可直接使用新QueryBuilders构建查询,通过Query静态方法包装:
import co.elastic.clients.elasticsearch._types.query_dsl.QueryBuilders; import org.springframework.data.elasticsearch.client.elc.ElasticsearchTemplate; import org.springframework.data.elasticsearch.client.elc.Query; // 注入ElasticsearchTemplate(ElasticsearchOperations的实现类) @Autowired private ElasticsearchTemplate elasticsearchTemplate; @Test void testSearchWithNewAPI() { String matchThis = "hello"; // 使用新QueryBuilders构建查询,并用Query.of包装 Query query = Query.of(q -> q.match(m -> m.field("field1").query(matchThis))); List<MyIndex> actualIndexed = elasticsearchTemplate.search(query, MyIndex.class) .stream() .map(SearchHit::getContent) .toList(); // 断言逻辑 }
方案2:修复ElasticsearchClient的连接关闭问题
连接关闭错误通常源于Testcontainer配置或客户端初始化问题:
- 确保容器完全就绪:在Testcontainer启动后添加健康检查等待,避免Spring上下文初始化时容器未就绪:
@DynamicPropertySource static void elasticsearchProperties(final DynamicPropertyRegistry dynamicPropertyRegistry) { final String username = "elastic"; final String password = "password"; elasticsearchContainer.withPassword(password); elasticsearchContainer.start(); // 等待Elasticsearch集群健康状态正常 elasticsearchContainer.waitingFor(Wait.forHttp("/_cluster/health") .withBasicAuth(username, password) .forStatusCode(200)); dynamicPropertyRegistry.add("spring.elasticsearch.username", () -> username); dynamicPropertyRegistry.add("spring.elasticsearch.password", () -> password); dynamicPropertyRegistry.add("spring.elasticsearch.uris", () -> List.of(elasticsearchContainer.getHttpHostAddress())); }
- 依赖Spring自动配置:不要手动创建
ElasticsearchClientBean,使用Spring Boot自动配置的实例,避免连接池冲突。
方案3:临时兼容旧API(不推荐)
若需短期过渡,可使用新包路径下的NativeQuery,它是旧NativeQuery的替代类,支持旧版QueryBuilder:
import org.springframework.data.elasticsearch.client.elc.NativeQuery; import org.elasticsearch.index.query.QueryBuilders; @Test void testSearchWithLegacyAPI() { String matchThis = "hello"; NativeQuery query = NativeQuery.builder() .withQuery(QueryBuilders.matchQuery("field1", matchThis)) .build(); List<MyIndex> actualIndexed = elasticsearchOperations.search(query, MyIndex.class) .stream() .map(SearchHit::getContent) .toList(); }
此方案仅作过渡使用,长期建议完全迁移到新API。
内容的提问来源于stack exchange,提问作者NoID

