Is there a way to get sorted results which depends on dynamically selected locale?
For example, the name of product is stored in map:
Product {
"name" : {
"en-US" : "Dumplings",
"hr-HR" : "Knedle",
// ...
},
"price" : 200,
// ...
(pseudo code to avoid dumping the whole Java entity)
I guess a bridge could deal with it but I have no idea where to start.
P.S. I use the latest available versions of Hiberbnate ORM and Hibernate Search.
Hey,
you might want to take a look at how we’ve implemented similar thing here: GitHub - quarkusio/search.quarkus.io: Search backend for Quarkus websites
Look for I18nKeywordField.java I18nDataBinder.java
In short the steps are:
- Add your own annotation so that you can apply it to your map field(
I18nKeywordField
)
- Implement a
PropertyMappingAnnotationProcessor
for it (I18nKeywordField.Processor
) in which you will generate the fields for each locale and pass a binder to it that will work with maps
- Implement a binder+bridge that will get a map, extract a string for a locale and return it as index value.
- put the new annotation on your map
// annotation and the processor:
@Documented
@Target({ ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(I18nKeywordField.List.class)
@PropertyMapping(processor = @PropertyMappingAnnotationProcessorRef(type = I18nKeywordField.Processor.class, retrieval = BeanRetrieval.CONSTRUCTOR))
public @interface I18nKeywordField {
String name() default "";
@Documented
@Target({ ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@interface List {
I18nKeywordField[] value();
}
// this is how Search will process the field you've annotated with the new annotation
class Processor implements PropertyMappingAnnotationProcessor<I18nKeywordField> {
// predefined list of required/expected locales:
private static final java.util.List<Locale> LOCALES =
java.util.List.of( Locale.forLanguageTag( "hr-HR" ), Locale.US );
@Override
public void process(PropertyMappingStep mapping, I18nKeywordField annotation,
PropertyMappingAnnotationProcessorContext context) {
String fieldNamePrefix =
annotation.name().isEmpty() ? context.annotatedElement().name() + "_" : annotation.name();
// Create one field per language, populated from the relevant data in the map
for ( Locale language : LOCALES ) {
// append a language code to the field name so that we have unique fields:
mapping.keywordField( fieldNamePrefix + language.getLanguage() )
.sortable( Sortable.YES ) // as you want to sort ...
.noExtractors() // so that Hibernate Search will not try extract from map itself
.valueBinder( new I18nDataBinder( language ) );
}
}
}
}
// binder
public class I18nDataBinder implements ValueBinder {
private final Locale language;
public I18nDataBinder(Locale language) {
this.language = language;
}
@Override
public void bind(ValueBindingContext<?> context) {
context.bridge( Map.class, new Bridge( language ) );
}
static class Bridge implements ValueBridge<Map, String> {
private final Locale language;
private Bridge(Locale language) {
this.language = language;
}
@Override
public String toIndexedValue(Map value, ValueBridgeToIndexedValueContext context) {
if ( value == null ) {
return null;
}
return (String) value.get( language );
}
}
}
//in entity
@Entity(name = "product")
@Indexed
public static class Product {
@Id
@GeneratedValue
private Integer id;
@I18nKeywordField // <-- use the annotation
@JdbcTypeCode(SqlTypes.JSON)
private Map<Locale, String> name;
}
and with that you’d be able to use it for sorting. Note that you’d have to pass the name+localesuffix or however you’ve constructed the field-name-per-locale in the processor, e.g.:
Search.session( entityManager ).search( Product.class )
.select()
.where( f -> f.matchAll() )
.sort( s -> s.field( "name_hr" ) ) // <-- name as you've constructed it in the processor
.fetchAllHits();
1 Like
Thanks a million!
It’s working!
Should I be worried about potential resource leak that Eclipse reports?
Great glad that helped!
Should I be worried about potential resource leak that Eclipse reports?
Mmm, it should be fine. The bridge does not hold any resources that you’d want to release (at least in the version I posted in the comment). And then, from what I see Search will take care of calling close if the call fails for some other reason.
1 Like
Can confirm, Hibernate Search is responsible for closing bridges.
1 Like