Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for programmatic mapping and mapping configurers #35026

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
66 changes: 64 additions & 2 deletions docs/src/main/asciidoc/hibernate-search-orm-elasticsearch.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,69 @@ The nice thing with `@IndexedEmbedded` is that it is able to automatically reind
`@IndexedEmbedded` also supports nested documents (using the `storage = NESTED` attribute), but we don't need it here.
You can also specify the fields you want to include in your parent index using the `includePaths` attribute if you don't want them all.

[programmatic-mapping]
=== Programmatic mapping

If, for some reason, adding Hibernate Search annotations to entities is not possible,
mapping can be applied programmatically instead.
Programmatic mapping is configured through the `ProgrammaticMappingConfigurationContext`
that is exposed via a mapping configurer (`HibernateOrmSearchMappingConfigurer`).

[NOTE]
====
A mapping configurer (`HibernateOrmSearchMappingConfigurer`) allows much more than just programmatic mapping capabilities.
It also allows link:{hibernate-search-docs-url}#mapper-orm-mapping-configurer[configuring annotation mapping, bridges, and more].
====

Below is an example of a mapping configurer that applies programmatic mapping:

[source,java]
----
package org.acme.hibernate.search.elasticsearch.config;

import org.hibernate.search.mapper.orm.mapping.HibernateOrmMappingConfigurationContext;
import org.hibernate.search.mapper.orm.mapping.HibernateOrmSearchMappingConfigurer;
import org.hibernate.search.mapper.pojo.mapping.definition.programmatic.TypeMappingStep;

import io.quarkus.hibernate.search.orm.elasticsearch.SearchExtension;

@SearchExtension // <1>
public class CustomMappingConfigurer implements HibernateOrmSearchMappingConfigurer {

@Override
public void configure(HibernateOrmMappingConfigurationContext context) {
TypeMappingStep type = context.programmaticMapping() // <2>
.type(SomeIndexedEntity.class); // <3>
type.indexed() // <4>
.index(SomeIndexedEntity.INDEX_NAME); // <5>
type.property("id").documentId(); // <6>
type.property("text").fullTextField(); // <7>
}
}
----
<1> Annotate the configurer implementation with the `@SearchExtension` qualifier
to tell Quarkus it should be used by Hibernate Search in the default persistence unit.
+
The annotation can also target a specific persistence unit (`@SearchExtension(persistenceUnit = "nameOfYourPU")`).
<2> Access the programmatic mapping context.
<3> Create mapping step for the `SomeIndexedEntity` entity.
<4> Define the `SomeIndexedEntity` entity as indexed.
<5> Provide an index name to be used for the `SomeIndexedEntity` entity.
<6> Define the document id property.
<7> Define a full-text search field for the `text` property.

[TIP]
====
Alternatively, if for some reason you can't or don't want to annotate your mapping configurer with `@SearchExtension`,
you can simply annotate it with `@Dependent @Named("myMappingConfigurer")`
and then reference it from configuration properties:

[source,properties]
----
quarkus.hibernate-search-orm.mapping.configurer=bean:myMappingConfigurer
----
====

== Analyzers and normalizers

=== Introduction
Expand Down Expand Up @@ -438,8 +501,7 @@ package org.acme.hibernate.search.elasticsearch.config;
import org.hibernate.search.backend.elasticsearch.analysis.ElasticsearchAnalysisConfigurationContext;
import org.hibernate.search.backend.elasticsearch.analysis.ElasticsearchAnalysisConfigurer;

import jakarta.enterprise.context.Dependent;
import jakarta.inject.Named;
import io.quarkus.hibernate.search.orm.elasticsearch.SearchExtension;

@SearchExtension // <1>
public class AnalysisConfigurer implements ElasticsearchAnalysisConfigurer {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ public interface HibernateSearchElasticsearchBuildTimeConfigPersistenceUnit {
*/
CoordinationConfig coordination();

/**
* Configuration for mapping.
*/
MappingConfig mapping();

@ConfigGroup
interface ElasticsearchBackendBuildTimeConfig {
/**
Expand Down Expand Up @@ -214,4 +219,28 @@ interface CoordinationConfig {
Optional<String> strategy();
}

@ConfigGroup
interface MappingConfig {
/**
* One or more xref:hibernate-search-orm-elasticsearch.adoc#bean-reference-note-anchor[bean references]
* to the component(s) used to configure Hibernate Search mapping.
*
* The referenced beans must implement `HibernateOrmSearchMappingConfigurer`.
*
* See xref:hibernate-search-orm-elasticsearch.adoc#programmatic-mapping[Programmatic mapping] for an example
* on how mapping configurers can be used to apply programmatic mappings.
*
* [NOTE]
* ====
* Instead of setting this configuration property,
* you can simply annotate your custom `HibernateOrmSearchMappingConfigurer` implementations with `@SearchExtension`
* and leave the configuration property unset: Hibernate Search will use the annotated implementation automatically.
* If this configuration property is set, it takes precedence over any `@SearchExtension` annotation.
* ====
*
* @asciidoclet
*/
Optional<List<String>> configurer();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static io.quarkus.hibernate.search.orm.elasticsearch.runtime.HibernateSearchConfigUtil.addBackendIndexConfig;
import static io.quarkus.hibernate.search.orm.elasticsearch.runtime.HibernateSearchConfigUtil.addConfig;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
Expand All @@ -28,13 +29,15 @@
import org.hibernate.search.backend.elasticsearch.index.layout.IndexLayoutStrategy;
import org.hibernate.search.engine.cfg.BackendSettings;
import org.hibernate.search.engine.cfg.EngineSettings;
import org.hibernate.search.engine.environment.bean.BeanReference;
import org.hibernate.search.engine.reporting.FailureHandler;
import org.hibernate.search.mapper.orm.Search;
import org.hibernate.search.mapper.orm.automaticindexing.session.AutomaticIndexingSynchronizationStrategy;
import org.hibernate.search.mapper.orm.bootstrap.impl.HibernateSearchPreIntegrationService;
import org.hibernate.search.mapper.orm.bootstrap.spi.HibernateOrmIntegrationBooter;
import org.hibernate.search.mapper.orm.cfg.HibernateOrmMapperSettings;
import org.hibernate.search.mapper.orm.coordination.common.spi.CoordinationStrategy;
import org.hibernate.search.mapper.orm.mapping.HibernateOrmSearchMappingConfigurer;
import org.hibernate.search.mapper.orm.mapping.SearchMapping;
import org.hibernate.search.mapper.orm.session.SearchSession;
import org.hibernate.search.mapper.pojo.work.IndexingPlanSynchronizationStrategy;
Expand Down Expand Up @@ -209,7 +212,7 @@ public void contributeBootProperties(BiConsumer<String, Object> propertyCollecto

addConfig(propertyCollector,
HibernateOrmMapperSettings.MAPPING_CONFIGURER,
new QuarkusHibernateOrmSearchMappingConfigurer(rootAnnotationMappedClasses));
collectAllHibernateOrmSearchMappingConfigurers());

addConfig(propertyCollector,
HibernateOrmMapperSettings.COORDINATION_STRATEGY,
Expand Down Expand Up @@ -242,6 +245,24 @@ public void contributeBootProperties(BiConsumer<String, Object> propertyCollecto
}
}

private List<BeanReference<HibernateOrmSearchMappingConfigurer>> collectAllHibernateOrmSearchMappingConfigurers() {
List<BeanReference<HibernateOrmSearchMappingConfigurer>> configurers = new ArrayList<>();
// 1. We add the quarkus-specific configurer:
configurers
.add(BeanReference.ofInstance(new QuarkusHibernateOrmSearchMappingConfigurer(rootAnnotationMappedClasses)));
// 2. Then we check if any configurers were supplied by a user be it through a property or via an extension:
Optional<List<BeanReference<HibernateOrmSearchMappingConfigurer>>> beanReferences = HibernateSearchBeanUtil
.multiExtensionBeanReferencesFor(
buildTimeConfig.mapping().configurer(),
HibernateOrmSearchMappingConfigurer.class,
persistenceUnitName, null, null);
if (beanReferences.isPresent()) {
configurers.addAll(beanReferences.get());
}

return configurers;
}

@Override
public void onMetadataInitialized(Metadata metadata, BootstrapContext bootstrapContext,
BiConsumer<String, Object> propertyCollector) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ private static <T> Optional<List<BeanReference<T>>> multiExtensionBeanReferences
return Optional.of(references);
}

public static <T> InjectableInstance<T> extensionInstanceFor(Class<T> beanType, String persistenceUnitName,
private static <T> InjectableInstance<T> extensionInstanceFor(Class<T> beanType, String persistenceUnitName,
String backendName, String indexName) {
return Arc.container().select(beanType,
new SearchExtension.Literal(persistenceUnitName, backendName == null ? "" : backendName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public void initData() {
entityManager.persist(new Analysis3TestingEntity("irrelevant"));
entityManager.persist(new Analysis4TestingEntity("irrelevant"));
entityManager.persist(new Analysis5TestingEntity("irrelevant"));
entityManager.persist(new Analysis6TestingEntity("irrelevant"));
}

@GET
Expand All @@ -59,6 +60,9 @@ public String testAnalysisConfigured() {
assertThat(findTypesMatching("text", "token_inserted_by_index_analysis_5"))
.containsExactlyInAnyOrder(Analysis5TestingEntity.class);

assertThat(findTypesMatching("text", "token_inserted_by_index_analysis_6"))
.containsExactlyInAnyOrder(Analysis6TestingEntity.class);

return "OK";
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.quarkus.it.hibernate.search.orm.elasticsearch.mapping;

import org.hibernate.search.mapper.orm.mapping.HibernateOrmMappingConfigurationContext;
import org.hibernate.search.mapper.orm.mapping.HibernateOrmSearchMappingConfigurer;
import org.hibernate.search.mapper.pojo.mapping.definition.programmatic.TypeMappingStep;

public abstract class AbstractCustomMappingConfigurer implements HibernateOrmSearchMappingConfigurer {

private final Class<?> type;
private final String indexName;

protected AbstractCustomMappingConfigurer(Class<?> type, String indexName) {
this.type = type;
this.indexName = indexName;
}

@Override
public void configure(HibernateOrmMappingConfigurationContext context) {
TypeMappingStep type = context.programmaticMapping().type(this.type);
type.indexed().index(this.indexName);
type.property("id").documentId();
type.property("text").fullTextField();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.quarkus.it.hibernate.search.orm.elasticsearch.mapping;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;

import org.hibernate.search.mapper.orm.mapping.HibernateOrmMappingConfigurationContext;

import io.quarkus.it.hibernate.search.orm.elasticsearch.analysis.MyCdiContext;

@ApplicationScoped
@Named("custom-application-bean-mapping-configurer")
public class CustomApplicationBeanMappingConfigurer extends AbstractCustomMappingConfigurer {
@Inject
MyCdiContext cdiContext;

public CustomApplicationBeanMappingConfigurer() {
super(MappingTestingApplicationBeanEntity.class, MappingTestingApplicationBeanEntity.INDEX);
}

@Override
public void configure(HibernateOrmMappingConfigurationContext context) {
MyCdiContext.checkAvailable(cdiContext);
super.configure(context);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.quarkus.it.hibernate.search.orm.elasticsearch.mapping;

import jakarta.inject.Inject;

import org.hibernate.search.mapper.orm.mapping.HibernateOrmMappingConfigurationContext;

import io.quarkus.it.hibernate.search.orm.elasticsearch.analysis.MyCdiContext;

public class CustomClassMappingConfigurer extends AbstractCustomMappingConfigurer {

@Inject
MyCdiContext cdiContext;

public CustomClassMappingConfigurer() {
super(MappingTestingClassEntity.class, MappingTestingClassEntity.INDEX);
}

@Override
public void configure(HibernateOrmMappingConfigurationContext context) {
MyCdiContext.checkNotAvailable(cdiContext);

super.configure(context);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.quarkus.it.hibernate.search.orm.elasticsearch.mapping;

import jakarta.enterprise.context.Dependent;
import jakarta.inject.Inject;
import jakarta.inject.Named;

import org.hibernate.search.mapper.orm.mapping.HibernateOrmMappingConfigurationContext;

import io.quarkus.it.hibernate.search.orm.elasticsearch.analysis.MyCdiContext;

@Dependent
@Named("custom-dependent-bean-mapping-configurer")
public class CustomDependentBeanMappingConfigurer extends AbstractCustomMappingConfigurer {
@Inject
MyCdiContext cdiContext;

public CustomDependentBeanMappingConfigurer() {
super(MappingTestingDependentBeanEntity.class, MappingTestingDependentBeanEntity.INDEX);
}

@Override
public void configure(HibernateOrmMappingConfigurationContext context) {
MyCdiContext.checkAvailable(cdiContext);
super.configure(context);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.quarkus.it.hibernate.search.orm.elasticsearch.mapping;

import jakarta.inject.Inject;

import org.hibernate.search.mapper.orm.mapping.HibernateOrmMappingConfigurationContext;

import io.quarkus.hibernate.search.orm.elasticsearch.SearchExtension;
import io.quarkus.it.hibernate.search.orm.elasticsearch.analysis.MyCdiContext;

@SearchExtension
public class CustomSearchExtensionMappingConfigurer extends AbstractCustomMappingConfigurer {
@Inject
MyCdiContext cdiContext;

public CustomSearchExtensionMappingConfigurer() {
super(MappingTestingSearchExtensionEntity.class, MappingTestingSearchExtensionEntity.INDEX);
}

@Override
public void configure(HibernateOrmMappingConfigurationContext context) {
MyCdiContext.checkAvailable(cdiContext);
super.configure(context);
}
}
Loading