Skip to content

Commit

Permalink
HSEARCH-3711 Document the index field type DSL extension for Elastics…
Browse files Browse the repository at this point in the history
…earch
  • Loading branch information
yrodiere committed Nov 4, 2019
1 parent e1a83f5 commit 754dd32
Show file tree
Hide file tree
Showing 5 changed files with 223 additions and 2 deletions.
1 change: 1 addition & 0 deletions documentation/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@
<wildflyUrl>https://wildfly.org/</wildflyUrl>
<quarkusUrl>https://quarkus.io/</quarkusUrl>
<springUrl>https://spring.io/</springUrl>
<gsonUrl>https://github.com/google/gson</gsonUrl>
</attributes>
</configuration>
</plugin>
Expand Down
59 changes: 57 additions & 2 deletions documentation/src/main/asciidoc/backend-elasticsearch.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,18 @@ which is supported by the Elasticsearch backend.
See <<mapper-orm-directfieldmapping-supported-types>> for more information.
====

[NOTE]
====
Field types that are not in this list can still be used with a little bit more work:
* If a property in the entity model has an unsupported type,
but can be converted to a supported type, you will need a bridge.
See <<mapper-orm-bridge>>.
* If you need an index field with a specific type that is not supported by Hibernate Search,
you will need a bridge that defines a native field type.
See <<backend-elasticsearch-field-types-extension>>.
====

[cols="l,1",options="header"]
.Field types supported by the Elasticsearch backend
|====
Expand Down Expand Up @@ -420,8 +432,51 @@ The Elasticsearch `date` type does not support the whole range of years that can
[[backend-elasticsearch-field-types-extension]]
=== Index field type DSL extension

include::todo-placeholder.asciidoc[]
// TODO HSEARCH-3711 also document "extension" types
Not all Elasticsearch field types have built-in support in Hibernate Search.
Unsupported field types can still be used, however,
by taking advantage of the "native" field type.
Using this field type, the Elasticsearch "mapping" can be defined as JSON directly,
giving access to everything Elasticsearch can offer.

Below is an example of how to use the Elasticearch "native" type.

.Using the Elasticearch "native" type
====
[source, JAVA, indent=0, subs="+callouts"]
----
include::{sourcedir}/org/hibernate/search/documentation/backend/elasticsearch/type/asnative/IpAddressValueBinder.java[tags=include]
----
<1> Define a <<mapper-orm-bridge,custom binder>> and its bridge.
The "native" type can only be used from a binder,
it cannot be used directly with annotation mapping.
Here we're defining a <<mapper-orm-bridge-valuebridge,value binder>>,
but a <<mapper-orm-bridge-typebridge,type binder>>,
or a <<mapper-orm-bridge-propertybridge,property binder>>
would work as well.
<2> Get the context's type factory.
<3> Apply the Elasticsearch extension to the type factory.
<4> Call `asNative` to start defining a native type.
<5> Pass the Elasticsearch mapping as JSON.
<6> Values of native fields are represented as strings in Hibernate Search,
but those strings are expected to be JSON.
Do not forget to format them correctly before you pass them to the backend.
Here we're using link:{gsonUrl}[Gson]
to convert the string to a JSON string,
escaping characters as appropriate,
but any JSON library will do.
<7> Same goes for projections: JSON is passed to the bridge,
and it should be parsed appropriately.
[source, JAVA, indent=0, subs="+callouts"]
----
include::{sourcedir}/org/hibernate/search/documentation/backend/elasticsearch/type/asnative/CompanyServer.java[tags=include,!getters-setters]
----
<1> Map the property to an index field.
Note that value bridges using a non-standard type (such as Elasticsearch's "native" type)
must be mapped using the `@NonStandardField` annotation:
other annotations such as `@GenericField` will fail.
<2> Instruct Hibernate Search to use our custom value binder.
====

[[backend-elasticsearch-analysis]]
== Analysis
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Hibernate Search, full-text search for your domain model
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.search.documentation.backend.elasticsearch.type.asnative;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

import org.hibernate.search.mapper.pojo.bridge.mapping.annotation.ValueBinderRef;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.NonStandardField;

// tag::include[]
@Entity
@Indexed
public class CompanyServer {

@Id
@GeneratedValue
private Integer id;

@NonStandardField( // <1>
valueBinder = @ValueBinderRef(type = IpAddressValueBinder.class) // <2>
)
private String ipAddress;

// Getters and setters
// ...

// tag::getters-setters[]
public Integer getId() {
return id;
}

public String getIpAddress() {
return ipAddress;
}

public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}
// end::getters-setters[]
}
// end::include[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Hibernate Search, full-text search for your domain model
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.search.documentation.backend.elasticsearch.type.asnative;


import static org.assertj.core.api.Assertions.assertThat;

import java.util.List;
import javax.persistence.EntityManagerFactory;

import org.hibernate.search.backend.elasticsearch.ElasticsearchExtension;
import org.hibernate.search.documentation.testsupport.ElasticsearchBackendConfiguration;
import org.hibernate.search.mapper.orm.Search;
import org.hibernate.search.mapper.orm.automaticindexing.AutomaticIndexingSynchronizationStrategyName;
import org.hibernate.search.mapper.orm.cfg.HibernateOrmMapperSettings;
import org.hibernate.search.mapper.orm.session.SearchSession;
import org.hibernate.search.util.impl.integrationtest.mapper.orm.OrmSetupHelper;
import org.hibernate.search.util.impl.integrationtest.mapper.orm.OrmUtils;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

public class ElasticsearchNativeTypeIT {
@Rule
public OrmSetupHelper setupHelper = OrmSetupHelper.withSingleBackend( new ElasticsearchBackendConfiguration() );

private EntityManagerFactory entityManagerFactory;

@Before
public void setup() {
entityManagerFactory = setupHelper.start()
.withProperty(
HibernateOrmMapperSettings.AUTOMATIC_INDEXING_SYNCHRONIZATION_STRATEGY,
AutomaticIndexingSynchronizationStrategyName.SEARCHABLE
)
.setup( CompanyServer.class );
}

@Test
public void smoke() {
OrmUtils.withinJPATransaction( entityManagerFactory, entityManager -> {
CompanyServer companyServer = new CompanyServer();
companyServer.setIpAddress( "192.168.10.2" );
entityManager.persist( companyServer );
} );

OrmUtils.withinJPATransaction( entityManagerFactory, entityManager -> {
SearchSession searchSession = Search.session( entityManager );

List<CompanyServer> result = searchSession.search( CompanyServer.class )
.extension( ElasticsearchExtension.get() )
.predicate( f -> f.fromJson(
"{\n" +
" \"term\": {\n" +
" \"ipAddress\": \"192.168.0.0/16\"\n" +
" }\n" +
"}"
) )
.fetchHits( 20 );

assertThat( result ).hasSize( 1 );
} );
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Hibernate Search, full-text search for your domain model
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.search.documentation.backend.elasticsearch.type.asnative;

import org.hibernate.search.backend.elasticsearch.ElasticsearchExtension;
import org.hibernate.search.mapper.pojo.bridge.ValueBridge;
import org.hibernate.search.mapper.pojo.bridge.binding.ValueBindingContext;
import org.hibernate.search.mapper.pojo.bridge.mapping.programmatic.ValueBinder;
import org.hibernate.search.mapper.pojo.bridge.runtime.ValueBridgeFromIndexedValueContext;
import org.hibernate.search.mapper.pojo.bridge.runtime.ValueBridgeToIndexedValueContext;

import com.google.gson.Gson;

//tag::include[]
public class IpAddressValueBinder implements ValueBinder { // <1>
@Override
public void bind(ValueBindingContext<?> context) {
context.setBridge(
String.class,
new IpAddressValueBridge(),
context.getTypeFactory() // <2>
.extension( ElasticsearchExtension.get() ) // <3>
.asNative( // <4>
"{\"type\": \"ip\"}" // <5>
)
);
}

private static class IpAddressValueBridge implements ValueBridge<String, String> {
private static final Gson GSON = new Gson();

@Override
public String toIndexedValue(String value, ValueBridgeToIndexedValueContext context) {
return value == null ? null : GSON.toJson( value ); // <6>
}

@Override
public String fromIndexedValue(String value, ValueBridgeFromIndexedValueContext context) {
return value == null ? null : GSON.fromJson( value, String.class ); // <7>
}
}
}
//end::include[]

0 comments on commit 754dd32

Please sign in to comment.