Rasmus Franke
Rasmus Franke

Reputation: 4524

AND-searching child objects in Lucene / Hibernate-search

In my web application, competence data for users can be stored with a ranking 0-5. Using Hibernate search (built on top of Lucene), I wish to construct a query that finds all users with competence X having a minimum ranking A, for example Java with ranking 3. Furthermore, I would like increased ranking to be boosted in the result, placing a user with ranking 4 before a user with ranking 3. I feel this should be possible, but I cannot figure out how to combine fields in child objects. My datastructure looks like this (simplified):

User.class

@Entity
@Table(schema = "COMPETENCE", name = "users")
@Indexed
public class User{    
    @Id
    @Field(store = Store.YES, index = Index.UN_TOKENIZED)
    private Long id;

    @OneToMany(mappedBy = "user")
    @IndexedEmbedded
    private List<UserCompetence> competenceList= new ArrayList<UserCompetence>();

    // Snip: Other irrelevant fields and get/setters

}

UserCompetence.class

@Entity
@Table(name = "user_competence")
public class Competence {

    @ManyToOne
    @JoinColumn(name = "user_id", referencedColumnName = "id")
    @NotNull
    @ContainedIn
    private User user;

    @ManyToOne
    @JoinColumn(name = "competence_id", referencedColumnName = "id")
    @NotNull
    @IndexedEmbedded
    private Competence competence;

    @Basic
    @Field(index = Index.UN_TOKENIZED, store = Store.YES)
    @NumericField
    private Integer level;

     // Snip: Other irrelevant fields and get/setters
}

@Entity()
@Table(name = "competence")
public class Competence{

    @Column(name = "name")
    @Field(index = Index.TOKENIZED, store = Store.YES)
    private String name;

    // Snip: Other irrelevant fields and get/setters
}

When I try to construct a query forcing a certain competence ANDa minimum level, it seems to find all people having that competence AND any competence with the specified level. How do I make it return only Users with correct UserCompetence children? I suspect I will need to remap some of my indexes to make this work.

Upvotes: 1

Views: 1773

Answers (2)

Rasmus Franke
Rasmus Franke

Reputation: 4524

In the end I ended up with a custom bridge that combines the two field in one, then use phrase search to search in the combined field.

public class UserCompetenceBridge implements FieldBridge {
    @Override
    public void set(
            String name, Object value, Document document, LuceneOptions luceneOptions) {
        UserCompetence pc = (UserCompetence ) value;

        // Add competence level + competence id combined field for specific competence querying
        String lvl = pc.getLevel() == null ? "0" : pc.getLevel().toString();
        String comp = pc.getCompetence().getId().toString();

        String fieldValue = comp + SearchFields.FIELD_SEPERATOR + lvl;
        Field compLvl = new Field(SearchFields.COMPETENCE_LEVEL, fieldValue, Field.Store.NO, Field.Index.NOT_ANALYZED);
        compLvl.setBoost(luceneOptions.getBoost());
        document.add(compLvl);

        // Add competence names for free text search
        Field compName = new Field(SearchFields.COMPETENCE_NAME, pc.getCompetence().getName(), Field.Store.NO, Field.Index.ANALYZED);
        document.add(compName);

    }
}

@Entity
@Table(name = "user_competence")
@ClassBridge(impl = UserCompetenceBridge.class)
public class UserCompetence {

    @ManyToOne
    @JoinColumn(name = "user_id", referencedColumnName = "id")
    @ContainedIn
    private User user;

    @ManyToOne
    @JoinColumn(name = "competence_id", referencedColumnName = "id")
    private Competence competence;

    @Basic
    @Column(name = "competence_level")
    private Integer level;
}

Searching for level > x like this:

for (CompetenceParam cp : param.getCompetences()) {
    BooleanJunction or = qb.bool();
    for(int i = cp.getMinLevel(); i <= 5 ; i++){
        or = or.should(qb.phrase()
                .onField(SearchFields.COMPETENCE_LEVEL)
                .boostedTo(1 + i/5f)
                .sentence(cp.getCompetenceId() + " " + i)
                .createQuery());
    }
    queries.add(or.createQuery());
}

This boosts people with high level and seems very fast.

Upvotes: 2

Hardy
Hardy

Reputation: 19109

What you try to do is not possible with your current mapping. All the UserCompetence instances of the user will be indexed as part of the same user Document. The data gets in this case flatend. If you write the AND query, you get maybe one competence hit in one UserCompetence instance and one hit of minimum level in another. They belong to the same user, but are not part of the same instances of the user will be indexed as part of the same UserCompetence instance.

One way around would be to index UserCompetence. Then you can search with AND logic to match a single UserCompetence instance.

Upvotes: 2

Related Questions