Search code examples
javahibernateelasticsearchhibernate-search

How to adjust the index mapping name in Hibernate Search 6


We have a few indexed entities that include an address entity in slightly different ways but we want to be able to search on any of these entities using the same mapping path to address fields.

In Hibernate Search 5, it was possible to define the index mapping path explicitly (@IndexedEmbedded(prefix = ...)) and use dots in the path which solved this issue, but Hibernate Search 6 doesn't support explicit dots in the path.

Hopefully this example helps to illustrate. This is pseudo-ish code in transition from HS5 to HS6.

@Entity
@DiscriminatorValue("entity1Address")
public class Entity1Address extends AbstractAddress {

    private Entity1 entity1;

    //...
}
    
@Entity
@Table(name = "address")
@DiscriminatorColumn(name = "dtype")
public abstract class AbstractAddress {

    private Address address = new Address();

    @Embedded
    @IndexedEmbedded
    @AttributeOverrides({
            @AttributeOverride(name = "line1", column = @Column(name = "addr_line_1")),
            //..other address fields
    })
    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }
}

@Embeddable
@AccessType("property")
public class Address {

    private String line1;
    //...other address fields

    //... line1 getter & setter
    
    @Transient
    @IndexingDependency(derivedFrom = @ObjectPath(@PropertyValue(propertyName = "line1")))
    //IndexingDependencies for other address lines
    @FullTextField(name = "line", analyzer = ADDRESS_LINE_ANALYZER)
    public List<String> getLines() {
        List<String> lines = Lists.newArrayListWithExpectedSize(4);
        if (StringUtils.hasText(line1)) {
            lines.add(line1);
        }
        //...add other address lines

        return lines;
    }   
}

Now for 2 examples of using addresses:

@Entity
@Table(name = "entity1")
@Indexed(index = "ENTITY1")
public class Entity1 {
    private Set<Entity1Address> addresses = Sets.newLinkedHashSet();
    
    @OneToMany(mappedBy = "entity1")
    @IndexedEmbedded
    public Set<ClientVendorAddress> getAddresses() {
        return addresses;
    }

    private void setAddresses(Set<EntityAddress> addresses) {
        this.addresses = addresses;
    }   
}

Entity1 has an index mapping path to line1 of "addresses.address.line1"

But Entity2 is where the conflict comes into play b/c it uses address more directly:

@Entity
@Table(name = "entity2")
@Indexed(index = "ENTITY2")
public class Entity2 {

    private Address physicalAddress;
    
    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "line1", column = @Column(name = "physical_addr_line_1")),
            //...other address fields
    })
    public Address getPhysicalAddress() {
        return physicalAddress;
    }

    public void setPhysicalAddress(Address physicalAddress) {
        this.physicalAddress = physicalAddress;
    }

    @Transient
    @IndexingDependency(derivedFrom = @ObjectPath(@PropertyValue(propertyName = "physicalAddress")))
    //@IndexedEmbedded(prefix = "addresses.address.")  // <--- in HS 5, this is how we standardized the mapping path between Entity1 and Entity2, but HS 6 doesn't support dots in the notation
    @IndexedEmbedded
    public Set<Address> getAddresses() {
        Set<Address> addresses = Sets.newLinkedHashSet();
        
        if (hasPhysicalAddress()) {
            addresses.add(physicalAddress);
        }       
        //...add other addresses
        
        return addresses;
    }
}

Entity2 has an index mapping path to line1 of "addresses.line1", which is not the same as Entity1.

There are times we want to search both Entity1 and Entity2 based on the same field mapping, meaning "addresses.address.line1" for both, but we no longer can in HS 6. Or even more common is that the SearchPredicates built are at a much lower level in the code than where the SearchPredicateFactory is created (which is where the entity is supplied) in order to reuse our queries/predicates.

An example of building our predicate:

// At some high level, we create the SearchPredicateFactory:
SearchSession searchSession = Search.session(session);
SearchPredicateFactory pf = searchSession.scope(Entity1.class).predicate();

// then much lower, for reuse reasons, we create predicates:
var myBool = pf.bool();
myBool.should(
    pf.match()
        // need a common mapping definition to reach line1 in any entity
        .field(LINE1_FIELD)
        .matching("100 main street")
);

Is there a way to do this through HS 6 configuration?


Solution

  • In Hibernate Search 5, it was possible to define the index mapping path explicitly (@IndexedEmbedded(prefix = ...)) and use dots in the path which solved this issue, but Hibernate Search 6 doesn't support explicit dots in the path.

    Technically Hibernate Search still supports it (it works), but it's deprecated and not recommended.

    This should work just fine:

    @IndexedEmbedded(prefix = "addresses.address.")
    

    Why don't you explain what doesn't work exactly? An exception? Doesn't behave as expected?

    Is there a way to do this through HS 6 configuration?

    Eventually you'll have more control and will be able to use @IndexedEmbedded programmatically in bridges, but that's not implemented yet. (see the idea of NestedBinding here if you're curious).

    In the meantime, you really should be able to just use @IndexedEmbedded(prefix = ...). It's deprecated, but it's not been removed and it should still work, as explained above.

    Alternatively, have you tried just wrapping the addresses of Entity2 into a new class, Entity2Address, which is not mapped in Hibernate ORM (because you don't need to)?

    public class Entity2Address extends AbstractAddress {
        //...
    }
    
    @Entity
    @Table(name = "entity2")
    @Indexed(index = "ENTITY2")
    public class Entity2 {
    
        private Address physicalAddress;
        
        @Embedded
        @AttributeOverrides({
                @AttributeOverride(name = "line1", column = @Column(name = "physical_addr_line_1")),
                //...other address fields
        })
        public Address getPhysicalAddress() {
            return physicalAddress;
        }
    
        public void setPhysicalAddress(Address physicalAddress) {
            this.physicalAddress = physicalAddress;
        }
    
        @Transient
        @IndexingDependency(derivedFrom = {
            @ObjectPath(@PropertyValue(propertyName = "physicalAddress"), @PropertyValue(propertyName = "lines")),
            // ... other address fields if necessary ...
        })
        @IndexedEmbedded
        public Set<Entity2Address> getAddresses() {
            Set<Entity2Address> addresses = Sets.newLinkedHashSet();
            
            if (hasPhysicalAddress()) {
                addresses.add(new Entity2Address(physicalAddress));
            }       
            //...add other addresses
            
            return addresses;
        }
    }