Migrating @FieldBridge to v6.1

Hello,

In Hibernate Search 5 I had this definition:

@Field(index = org.hibernate.search.annotations.Index.YES, analyze = Analyze.NO, store = Store.YES)
@FieldBridge(impl = LongBridge.class)
protected Long tenant;

And now its like this but I don’t know if the previous @FieldBridge need to be migrated or now it’s not necessary:

@GenericField(searchable = Searchable.YES, projectable = Projectable.YES)
protected Long tenant;

I guess a sort of migration seems to be needed because when I search by this field, no results are returned.

Hello,

The bridge you were using was indexing your Long as text instead of a numeric value, which is an odd choice probably motivated by backwards compatibility requirements (numeric values used not to exist at all in Lucene, a long time ago). As a result, you had to use text queries instead of (newer, better performing) numeric queries to search on this field.

So, it really depends how you search. If you use Hibernate Search’s DSL, it will always use the right type of query. If you’re using native Lucene queries, you need to be careful. What’s the code that runs your search query?

See also: .fromLuceneQuery(…) not matching @GenericField fields - #2 by yrodiere

Hi there,

At this point I want to stick to Lucene queries, so .fromLuceneQuery is used. In the near future I want to switch to Hibernate Search DSL but right now it implies a lot of changes. I also store text Lucene queries on database to be executed at another point and I don’t know if I can get a text representation of the Hibernate Search DSL (I’ve found a reference to simpleQueryString in the “See also” link, but seems more restrictive than QueryParser)

Thanks!

Hi,

Ok, well if you want to use native Lucene queries, you need to use those that match the type of your field. If you’re using QueryParser, then my previous link is relevant: .fromLuceneQuery(…) not matching @GenericField fields - #2 by yrodiere . See solution #3 in particular.

Also, no, there isn’t any text representation of Hibernate Search predicates at the moment. You’ll have to use your own. There are plans to add support for QueryParser syntax (HSEARCH-4563), but as I already mentioned elsewhere, I’m already busy with other features, so you’ll have to wait or contribute a pull request if you want it soon.

Hello,

Good to know there is an issue already created HSEARCH-4563 :slight_smile:

Meanwhile, I will try the solutions provided at .fromLuceneQuery(…) not matching @GenericField fields - #2 by yrodiere, in special #3 as you recommended.

Thanks a lot.

I’ve implemented a NumericValueBridge this way:

public class NumericValueBridge implements ValueBridge<Long, String> {
	private static final Logger log = LoggerFactory.getLogger(NumericValueBridge.class);

	@Override
	public String toIndexedValue(Long value, ValueBridgeToIndexedValueContext context) {
		log.info("toIndexedValue({})", value);
		return String.valueOf(value);
	}
}

And added to the tenant field in my NodeBase class:

@KeywordField(searchable = Searchable.YES, projectable = Projectable.YES, valueBridge = @ValueBridgeRef(type = NumericValueBridge.class))
protected Long tenant;

But when I perform a search, this error is thrown:

HSEARCH000601: Inconsistent configuration for field 'tenant' in a search query across multiple indexes: HSEARCH000603: Attribute 'projectionConverter' differs: 'ProjectionConverter[valueType=java.lang.Long,delegate=PojoValueBridgeDocumentValueConverter[com.openkm.search.NumericValueBridge@32b6d6f9]]' vs. 'ProjectionConverter[valueType=java.lang.Long,delegate=PojoValueBridgeDocumentValueConverter[com.openkm.search.NumericValueBridge@6fdf4887]]'.
Context: indexes [node_document, node_folder]

Note: NodeDocument and NodeFolder extends from NodeBase.

You need to let Hibernate Search know that two instances of your bridge always behave the same.

The documentation mentions that: Hibernate Search 6.1.5.Final: Reference Documentation . It mentions predicates, but it’s the same problem with projections.

Just solved implementing the ValueBridge this way, but it’s quite odd :confused::

public class NumericValueBridge implements ValueBridge<Long, String> {
	private static final Logger log = LoggerFactory.getLogger(NumericValueBridge.class);

	@Override
	public String toIndexedValue(Long value, ValueBridgeToIndexedValueContext context) {
		log.info("toIndexedValue({})", value);
		return String.valueOf(value);
	}

	@Override
	public Long fromIndexedValue(String value, ValueBridgeFromIndexedValueContext context) {
		log.info("fromIndexedValue({})", value);
		return Long.parseLong(value);
	}

	@Override
	public boolean isCompatibleWith(ValueBridge<?, ?> other) {
		return getClass().equals(other.getClass());
	}
}

:man_shrugging:

There are multiple instances of the bridge involved, so Hibernate Search needs some way to say “those two instances behave the same” so it can just pick one and use it when projecting. You’ll find more information here: Hibernate Search 7.0.0.Final: Reference Documentation

Technically we could avoid that for projections (not for predicates), by applying a different bridge depending on which index the projected document comes from, but that’s not implemented at the moment.

If the class is the same, it should be the same. This is what I did with isCompatibleWith, which seems a bit unecessary, or perhaps I’m wrong:

@Override
public boolean isCompatibleWith(ValueBridge<?, ?> other) {
	return getClass().equals(other.getClass());
}

Thanks anyway :slight_smile:

I thought that was obvious, but you can have two instances of the same class that behave differently. That’s why equals(Object) exists, and isCompatibleWith defaults to calling equals.

Two instances generally behave differently when using a ValueBinder, and there’s an example in this section of the documentation.


Even without a ValueBinder, there are cases where Hibernate Search is not aware of how the bridges are constructed at all, and thus it cannot determine whether they will behave the same way.
For example you could have this:

public class BooleanAsStringBridge implements ValueBridge<Boolean, String> { 

    private final String trueAsString;
    private final String falseAsString;

    public BooleanAsStringBridge(String trueAsString, String falseAsString) { 
        this.trueAsString = trueAsString;
        this.falseAsString = falseAsString;
    }

    @Override
    public String toIndexedValue(Boolean value, ValueBridgeToIndexedValueContext context) {
        if ( value == null ) {
            return null;
        }
        return value ? trueAsString : falseAsString;
    }
}

With two instances declared as CDI beans:

public class BridgeFactory {

      @Produces
      @Named("truefalse")
      public BooleanAsStringBridge createBooleanAsTrueFalseBridge() {
          Order order = new Order(product, shop);
          return new BooleanAsStringBridge("true", "false");
      }

      @Produces
      @Named("yesno")
      public BooleanAsStringBridge createBooleanAsYesNoBridge() {
          Order order = new Order(product, shop);
          return new BooleanAsStringBridge("yes", "no");
      }

}

And the bridge assigned separately to a field in NodeDocument and NodeFolder:

@Entity
@Indexed
public class NodeBase {

     // ... 

     private final boolean active;

     // ... 

     public boolean isActive() {
         return active;
     }

     // ... 
}

@Entity
@Indexed
public class NodeDocument extends NodeBase {
     // ... 

     @GenericField(valueBridge = @ValueBridgeRef(name = "truefalse"))
     @Override
     public boolean isActive() {
         return active;
     }

     // ... 
}

@Entity
@Indexed
public class NodeDocument extends NodeBase {
     // ... 

     @GenericField(valueBridge = @ValueBridgeRef(name = "yesno"))
     @Override
     public boolean isActive() {
         return active;
     }

     // ... 
}

And it could get even weirder: even when Hibernate Search itself calls the default constructor of the bridge, there’s no guarantee this constructor will always initialize the bridge to the same values. For example I could do this:

public class IntWithOffsetValueBridge implements ValueBridge<Integer, Integer> {
        private static final AtomicInteger counter = new AtomicInteger(0);

        private final int offset;

        public IntWithOffsetValueBridge() {
                offset = counter.incrementAndGet();
        }

	@Override
	public Integer toIndexedValue(Integer value, ValueBridgeToIndexedValueContext context) {
		return value + offset;
	}

	@Override
	public boolean isCompatibleWith(ValueBridge<?, ?> other) {
		return getClass().equals(other.getClass()) && offset == ((IntWithOffsetValueBridge)other).offset;
	}
}

So, indeed, those are not the simplest use cases. But my point is: we just can’t assume that two instances of a same class behave the same.

Ok, I see.

Thanks for the explanation!