Sorting by locale, help needed

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:

  1. Add your own annotation so that you can apply it to your map field(I18nKeywordField)
  2. 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
  3. Implement a binder+bridge that will get a map, extract a string for a locale and return it as index value.
  4. 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 :smiley: 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