diff --git a/extensions/hibernate-search-orm-elasticsearch/deployment/src/test/java/io/quarkus/hibernate/search/orm/elasticsearch/test/search/shard_failure/MyEntity1.java b/extensions/hibernate-search-orm-elasticsearch/deployment/src/test/java/io/quarkus/hibernate/search/orm/elasticsearch/test/search/shard_failure/MyEntity1.java new file mode 100644 index 00000000000000..e3ecbac58118eb --- /dev/null +++ b/extensions/hibernate-search-orm-elasticsearch/deployment/src/test/java/io/quarkus/hibernate/search/orm/elasticsearch/test/search/shard_failure/MyEntity1.java @@ -0,0 +1,44 @@ +package io.quarkus.hibernate.search.orm.elasticsearch.test.search.shard_failure; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed; +import org.hibernate.search.mapper.pojo.mapping.definition.annotation.KeywordField; + +@Entity +@Indexed +public class MyEntity1 { + + @Id + @GeneratedValue + private Long id; + + @KeywordField + private String text; + + public MyEntity1() { + } + + public MyEntity1(String text) { + this.text = text; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + +} diff --git a/extensions/hibernate-search-orm-elasticsearch/deployment/src/test/java/io/quarkus/hibernate/search/orm/elasticsearch/test/search/shard_failure/MyEntity2.java b/extensions/hibernate-search-orm-elasticsearch/deployment/src/test/java/io/quarkus/hibernate/search/orm/elasticsearch/test/search/shard_failure/MyEntity2.java new file mode 100644 index 00000000000000..b2550b8e475eba --- /dev/null +++ b/extensions/hibernate-search-orm-elasticsearch/deployment/src/test/java/io/quarkus/hibernate/search/orm/elasticsearch/test/search/shard_failure/MyEntity2.java @@ -0,0 +1,44 @@ +package io.quarkus.hibernate.search.orm.elasticsearch.test.search.shard_failure; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed; +import org.hibernate.search.mapper.pojo.mapping.definition.annotation.KeywordField; + +@Entity +@Indexed +public class MyEntity2 { + + @Id + @GeneratedValue + private Long id; + + @KeywordField + private String text; + + public MyEntity2() { + } + + public MyEntity2(String text) { + this.text = text; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + +} diff --git a/extensions/hibernate-search-orm-elasticsearch/deployment/src/test/java/io/quarkus/hibernate/search/orm/elasticsearch/test/search/shard_failure/ShardFailureIgnoreDefaultTest.java b/extensions/hibernate-search-orm-elasticsearch/deployment/src/test/java/io/quarkus/hibernate/search/orm/elasticsearch/test/search/shard_failure/ShardFailureIgnoreDefaultTest.java new file mode 100644 index 00000000000000..ad3623a7ebae5c --- /dev/null +++ b/extensions/hibernate-search-orm-elasticsearch/deployment/src/test/java/io/quarkus/hibernate/search/orm/elasticsearch/test/search/shard_failure/ShardFailureIgnoreDefaultTest.java @@ -0,0 +1,50 @@ +package io.quarkus.hibernate.search.orm.elasticsearch.test.search.shard_failure; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import jakarta.inject.Inject; + +import org.hibernate.search.mapper.orm.session.SearchSession; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.hibernate.search.orm.elasticsearch.test.util.TransactionUtils; +import io.quarkus.narayana.jta.QuarkusTransaction; +import io.quarkus.test.QuarkusUnitTest; + +public class ShardFailureIgnoreDefaultTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClass(TransactionUtils.class) + .addClass(MyEntity1.class) + .addClass(MyEntity2.class) + .addAsResource("hsearch-4915/index2.json")) + .withConfigurationResource("application.properties") + // Override the type of the keyword field to integer, to create an error in one shard only. + .overrideConfigKey( + "quarkus.hibernate-search-orm.elasticsearch.indexes.\"MyEntity2\".schema-management.mapping-file", + "hsearch-4915/index2.json"); + + @Inject + SearchSession session; + + @Test + public void testShardFailureIgnored() { + QuarkusTransaction.joiningExisting().run(() -> { + session.toEntityManager().persist(new MyEntity1("42")); + session.toEntityManager().persist(new MyEntity2("42")); + }); + QuarkusTransaction.joiningExisting().run(() -> { + assertThat(session.search(List.of(MyEntity1.class, MyEntity2.class)) + .where(f -> f.wildcard().field("text").matching("4*")) + .fetchHits(20)) + // MyEntity2 fails because "text" is an integer field there + // We expect that index (shard) to be ignored + .hasSize(1); + }); + } +} diff --git a/extensions/hibernate-search-orm-elasticsearch/deployment/src/test/java/io/quarkus/hibernate/search/orm/elasticsearch/test/search/shard_failure/ShardFailureIgnoreFalseTest.java b/extensions/hibernate-search-orm-elasticsearch/deployment/src/test/java/io/quarkus/hibernate/search/orm/elasticsearch/test/search/shard_failure/ShardFailureIgnoreFalseTest.java new file mode 100644 index 00000000000000..dca03bec0ad4e0 --- /dev/null +++ b/extensions/hibernate-search-orm-elasticsearch/deployment/src/test/java/io/quarkus/hibernate/search/orm/elasticsearch/test/search/shard_failure/ShardFailureIgnoreFalseTest.java @@ -0,0 +1,53 @@ +package io.quarkus.hibernate.search.orm.elasticsearch.test.search.shard_failure; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; + +import jakarta.inject.Inject; + +import org.hibernate.search.mapper.orm.session.SearchSession; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.hibernate.search.orm.elasticsearch.test.util.TransactionUtils; +import io.quarkus.narayana.jta.QuarkusTransaction; +import io.quarkus.test.QuarkusUnitTest; + +public class ShardFailureIgnoreFalseTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClass(TransactionUtils.class) + .addClass(MyEntity1.class) + .addClass(MyEntity2.class) + .addAsResource("hsearch-4915/index2.json")) + .withConfigurationResource("application.properties") + // Request that shard failures cause an exception instead of being ignored + .overrideConfigKey("quarkus.hibernate-search-orm.elasticsearch.query.shard-failure.ignore", "false") + // Override the type of the keyword field to integer, to create an error in one shard only. + .overrideConfigKey( + "quarkus.hibernate-search-orm.elasticsearch.indexes.\"MyEntity2\".schema-management.mapping-file", + "hsearch-4915/index2.json"); + + @Inject + SearchSession session; + + @Test + public void testShardFailureIgnored() { + QuarkusTransaction.joiningExisting().run(() -> { + session.toEntityManager().persist(new MyEntity1("42")); + session.toEntityManager().persist(new MyEntity2("42")); + }); + QuarkusTransaction.joiningExisting().run(() -> { + assertThatThrownBy(() -> session.search(List.of(MyEntity1.class, MyEntity2.class)) + .where(f -> f.wildcard().field("text").matching("4*")) + .fetchHits(20)) + // MyEntity2 fails because "text" is an integer field there + // We expect an exception + .hasMessageContaining("Elasticsearch request failed", + "\"type\": \"query_shard_exception\""); + }); + } +} diff --git a/extensions/hibernate-search-orm-elasticsearch/deployment/src/test/resources/hsearch-4915/index2.json b/extensions/hibernate-search-orm-elasticsearch/deployment/src/test/resources/hsearch-4915/index2.json new file mode 100644 index 00000000000000..561654100718e6 --- /dev/null +++ b/extensions/hibernate-search-orm-elasticsearch/deployment/src/test/resources/hsearch-4915/index2.json @@ -0,0 +1,7 @@ +{ + "properties": { + "text": { + "type": "integer" + } + } +} \ No newline at end of file diff --git a/extensions/hibernate-search-orm-elasticsearch/runtime/src/main/java/io/quarkus/hibernate/search/orm/elasticsearch/runtime/HibernateSearchElasticsearchRecorder.java b/extensions/hibernate-search-orm-elasticsearch/runtime/src/main/java/io/quarkus/hibernate/search/orm/elasticsearch/runtime/HibernateSearchElasticsearchRecorder.java index 7f72c8a5a8b822..586a3942338d44 100644 --- a/extensions/hibernate-search-orm-elasticsearch/runtime/src/main/java/io/quarkus/hibernate/search/orm/elasticsearch/runtime/HibernateSearchElasticsearchRecorder.java +++ b/extensions/hibernate-search-orm-elasticsearch/runtime/src/main/java/io/quarkus/hibernate/search/orm/elasticsearch/runtime/HibernateSearchElasticsearchRecorder.java @@ -456,6 +456,8 @@ private void contributeBackendRuntimeProperties(BiConsumer prope elasticsearchBackendConfig.threadPool().size()); addBackendConfig(propertyCollector, backendName, ElasticsearchBackendSettings.VERSION_CHECK_ENABLED, elasticsearchBackendConfig.versionCheck()); + addBackendConfig(propertyCollector, backendName, ElasticsearchBackendSettings.QUERY_SHARD_FAILURE_IGNORE, + elasticsearchBackendConfig.query().shardFailure().ignore()); addBackendConfig(propertyCollector, backendName, ElasticsearchBackendSettings.DISCOVERY_ENABLED, elasticsearchBackendConfig.discovery().enabled()); diff --git a/extensions/hibernate-search-orm-elasticsearch/runtime/src/main/java/io/quarkus/hibernate/search/orm/elasticsearch/runtime/HibernateSearchElasticsearchRuntimeConfigPersistenceUnit.java b/extensions/hibernate-search-orm-elasticsearch/runtime/src/main/java/io/quarkus/hibernate/search/orm/elasticsearch/runtime/HibernateSearchElasticsearchRuntimeConfigPersistenceUnit.java index 8a9fa181fb631c..7eef01aa32e978 100644 --- a/extensions/hibernate-search-orm-elasticsearch/runtime/src/main/java/io/quarkus/hibernate/search/orm/elasticsearch/runtime/HibernateSearchElasticsearchRuntimeConfigPersistenceUnit.java +++ b/extensions/hibernate-search-orm-elasticsearch/runtime/src/main/java/io/quarkus/hibernate/search/orm/elasticsearch/runtime/HibernateSearchElasticsearchRuntimeConfigPersistenceUnit.java @@ -148,6 +148,11 @@ interface ElasticsearchBackendRuntimeConfig { */ ThreadPoolConfig threadPool(); + /** + * Configuration for search queries to this backend. + */ + ElasticsearchQueryConfig query(); + /** * Whether Hibernate Search should check the version of the Elasticsearch cluster on startup. * @@ -505,6 +510,26 @@ interface ThreadPoolConfig { OptionalInt size(); } + @ConfigGroup + interface ElasticsearchQueryConfig { + /** + * Configuration for the behavior on shard failure. + */ + ElasticsearchQueryShardFailureConfig shardFailure(); + } + + @ConfigGroup + interface ElasticsearchQueryShardFailureConfig { + /** + * Whether partial shard failures are ignored (`true`) + * or lead to Hibernate Search throwing an exception (`false`). + *

+ * Will default to `false` in Hibernate Search 7. + */ + @WithDefault("true") + boolean ignore(); + } + // We can't set actual default values in this section, // otherwise "quarkus.hibernate-search-orm.elasticsearch.index-defaults" will be ignored. @ConfigGroup