Skip to content

Commit

Permalink
Merge pull request #35026 from marko-bekhta/feat/i32760-search-progra…
Browse files Browse the repository at this point in the history
…mmatic-mapping

Add support for programmatic mapping and mapping configurers
  • Loading branch information
yrodiere authored Aug 4, 2023
2 parents 1bd2f68 + ce95c11 commit 15ee135
Show file tree
Hide file tree
Showing 20 changed files with 498 additions and 4 deletions.
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

0 comments on commit 15ee135

Please sign in to comment.