ValueBinder for Collection<String>

According to Hibernate Search 6.1.7.Final: Reference Documentation, my binder should be easy.
But it won’t compile - I get an error on context.bridge():

The method bridge(Class<V2>, ValueBridge<V2,F>) in the type ValueBindingContext<capture#4-of ?> is not applicable for the arguments (Class<Collection>, BarcodesBinder.BarcodesBridge)

So far that I understand the manual, V2 should be Collection, right? Tried also with Set (more specific interface) and HashSet (just to be sure it’s not the interface causing problem).

P.S. I guess @Converter on entity property is not the problem, at least not to cause compile error because my bridge does not know about it at design time.

What am I doing wrong?


Article.java

@Entity @Indexed
class Article {

	// ... ID, name...

	@FullTextField(
		projectable = Projectable.NO,
		searchable = Searchable.YES,
		valueBinder = @ValueBinderRef(type = BarcodesBinder.class)
	)
	@Convert(converter = StringSetConverter.class)
	private Set<String> barcodes;

	// ... more properties

}

BarcodesBinder.java

// Normalize Set of barcode values by removing spaces inside each barcode
// and then join all by single space.
class BarcodesBinder implements ValueBinder {

	@Override
	public void bind( ValueBindingContext<?> context ) {
		// ERROR HERE
		context.bridge( Collection.class, new BarcodesBridge() );
	}

	private static class BarcodesBridge
			implements ValueBridge<Collection<String>, String>
	{
		@Override
		public String toIndexedValue( Collection<String> value,
			ValueBridgeToIndexedValueContext context
		) {
			if (value == null) { return ""; }
			return StringUtils.trimToNull(
				value.stream()
					.map(StringUtils::trimToNull)
					.filter(Objects::nonNull)
					.map(barcode -> barcode.replace(" ", ""))
					.collect(Collectors.joining(" ")) );
		}
	}
}

@horvoje Reminder that ValueBinder is for immutable values only, so it would not work for collections. See this section of the documentation , in the table, “Supports mutable types => NO”.

The way to go is indeed to bind the type of items of the collection, and Hibernate Search will apply your bridge to each item. See that same section, in the table, “Supports container extractors” => Yes.

By the way @horvoje , @gsmet pointed out to me that the only purpose of your bridge was to index multiple String values and remove spaces within those values.

With Hibernate Search 6, you can easily do that with one annotation + a normalizer:

@KeywordField(normalizer = "barcode")
private Collection<String> barcodes;

Just define the normalizer in your analysis configurer:

package org.hibernate.search.documentation.analysis;

import org.hibernate.search.backend.lucene.analysis.LuceneAnalysisConfigurationContext;
import org.hibernate.search.backend.lucene.analysis.LuceneAnalysisConfigurer;

public class MyLuceneAnalysisConfigurer implements LuceneAnalysisConfigurer {
    @Override
    public void configure(LuceneAnalysisConfigurationContext context) {
        context.normalizer( "barcode" ).custom() 
                .tokenFilter( "lowercase" ) // If you need it
                .tokenFilter( "patternReplace" )
                        .param( "pattern", " " )
                        .param( "replacement", "" );
    }
}

And make sure to instruct Hibernate Search to use your analysis configurer:

hibernate.search.backend.analysis.configurer = class:org.hibernate.search.documentation.analysis.MyLuceneAnalysisConfigurer

You can do something similar for Elasticsearch, you will just have to use a different interface and pass different names to the tokenFilter/param methods: Hibernate Search 6.1.7.Final: Reference Documentation

1 Like

Thank you very much!

The solution with normalizer - I guess it would take a year for me to come to this solution without your help. I’ll use this solution because it fits my needs.

But if I wanted to do more with each item of Collection, for example call my custom util method on them, then I should use ValueBridge<String, String> which would be applied to each collection element, right? I mean, I’m pretty sure it’s the case, but just to be 100% sure.

Also I guess this will be good example for others not knowing how to handle Collection so we can assume our time is not wasted on this. :slight_smile:

Thanks!

No problem.

Right, it would.

Though keep in mind that there are lots of different token filters, so one of them might be able to do other things that you currently do in your utils, too.

1 Like

Hi there, it’s me again :slight_smile:
Now I have almost the same situation but…

The idea is to have URIs indexed as Collection which should give me option to search using .match() on field uris without any Lucene analyzers. For each Locale there is one URI in collection.

Is it possible to map and object that has a List inside to Set of keywords?

class Provider {
    @KeywordField(
        projectable = Projectable.NO,
        searchable = Searchable.YES, sortable = Sortable.NO,
        valueBridge = @ValueBridgeRef(type = UrisBridge.class)
    )
    private Uris uris;
}

// not working because of Set
class TagsBridge implements ValueBridge<Uris, Set<String>> {

	@Override
	public Set<String> toIndexedValue(
		Uris value, ValueBridgeToIndexedValueContext context
	) {
		if (value == null) {
			return new HashSet<>();
		}
		return IndexingUtil.Uris.toSet(value);
	}

}

Tried also to create ValueBinder, but no success:

// not working
public class UrisValueBinder implements ValueBinder {

	private final Locale language;

	public UrisValueBinder( Locale language ) {
		this.language = language;
	}

	@Override
	public void bind( ValueBindingContext<?> context ) {
		context.bridge( Uris.class, new Bridge( this.language ) );
	}

	public static class Bridge implements ValueBridge<Uris, String> {

		private final Locale language;

		private Bridge( Locale language ) {
			this.language = language;
		}

		@Override
		public String toIndexedValue(
			Uris value,
			ValueBridgeToIndexedValueContext context
		) {
			if (value == null) {
				return null;
			}
			return UrisSupport.normalizeForLucene(value, this.language);
		}

	}

}

Tried also to create an annotation, but no success.

Hey :slightly_smiling_face:,

I think you’d need [HSEARCH-3688] - Hibernate JIRA implemented to make it work.
How about simply using a getter? E.g.:

class Provider {
    private Uris uris;

    // ....

    @KeywordField(
        name = "uris",
        projectable = Projectable.NO,
        searchable = Searchable.YES, sortable = Sortable.NO
    )
    @IndexingDependency(derivedFrom = @ObjectPath({ 
            @PropertyValue(propertyName = "uris")
    }))
    public Set<URI> getUrisForIndexing() {
        return IndexingUtil.Uris.toSet(this.uris);
    }
}

This.

Alternatively, define a property bridge. It’s a bit more complex to set up, but it’s also more powerful, and allows what you want to do in particular.

1 Like

Hi Marko!

Thank you very much for your reply!
To be honest, here is a simplified code to point out my problem. Real class that I wrote has the other name (TextId) and much more inside - a Map where Locale is a key and a value of List<String> (for example, first element can be manufactuter URI, the next one can be product URI; or in case with article providers it can be consisted of parent provider URI, child provider URI and product URI).
Also this class has some additional methods, e.g. to create URI from its own elements depending on Locale used. I wouldn’t be happy to replace this class with simple Set<String> because it’s deeply integrated with other project code and I would have to write util classes to handle it all over the project. Now I just use private TextId textId and I get all those as methods of textId.
Otherwise I’d use a Set for sure.
Pretty much every URL you can find on thevegcat.com is handled by this class because it supports localization more or less out of the box.

P.S. Currently I have a brige over a FullTextField that converts all possible URIs from this class into a single Setand then I use join method with space as delimiter to convert it into plain String and later I use simple query to find appropriate document. But I thing having it as KeywodField would be better because I don’t need analyzers on this, just basic plain match and no substrings and no any other options rather than match.

BR,
Hrvoje

@yrodiere
Thanks! I’ll give it a try but you can bet I’ll be back here with questions. :rofl: :sob:

@horvoje Np.

I don’t know if it makes sense, but if your Uris/TextId classes are as complex as you imply, maybe you want to use @IndexedEmbedded on the Uris uris field and deal with the complexity in the Uris type (or in a type bridge on the Uris type).

Hi all there! Thanks a million for your help. Now it works!
I love you all! <3

Just created a TextIdPropertyBridge and then created an annotation TextIdField.
My entity property looks like this:

@TextIdField( fieldName = "textId" )
private TextId textId;

Inside a bridge there is call to convert to a Set which then contains URIs for any Locale I support (en_us__food__semi_prepared_meals_and_mixtures, hr_hr__hrana__polugotova_jela_i_smjese,
sr_rs__hrana__polugotova_jela_i_mesavine - where dobule underscore means a slash from path).
And then simply those values are written inside overriden method write() inside bridge:

target.addValue(this.valueFieldReference, uri);

I didn’t like to use @IndexedEmbedded as I use e.g. for Manufacturer of products or Provider inside a price structure because it looks to me natural to search over field textId as a set, not to use any textId.locale or something similar.
If you look at this as a tag list, that’s pretty much the same thing - you have few tags and you want to find an item over one specific tag.

Thanks again for your effort and time!

2 Likes