package ca.intelliware.ihtsdo.mlds.repository; import java.util.Arrays; import java.util.List; import javax.annotation.Resource; import javax.persistence.EntityManager; import org.apache.lucene.search.Query; import org.hibernate.search.SearchFactory; import org.hibernate.search.errors.EmptyQueryException; import org.hibernate.search.jpa.FullTextEntityManager; import org.hibernate.search.jpa.FullTextQuery; import org.hibernate.search.jpa.Search; import org.hibernate.search.query.dsl.BooleanJunction; import org.hibernate.search.query.dsl.MustJunction; import org.hibernate.search.query.dsl.QueryBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import ca.intelliware.ihtsdo.mlds.domain.Affiliate; import ca.intelliware.ihtsdo.mlds.domain.Member; import ca.intelliware.ihtsdo.mlds.domain.StandingState; /** * Turn on trace level logging to get lucene score and explain info on query. * * @author buckleym */ @Service public class AffiliateSearchRepository { private static final String FIELD_ALL = "ALL"; static final Logger LOG = LoggerFactory.getLogger(AffiliateSearchRepository.class); @Resource EntityManager entityManager; PageableUtil pageableUtil = new PageableUtil(); public void reindex(Affiliate a) { getFullTextEntityManager().index(a); } public Page<Affiliate> findFullTextAndMember(String q, Member homeMember, StandingState standingState, boolean standingStateNot, Pageable pageable) { Query query = buildQuery(q, homeMember, standingState, standingStateNot); LOG.debug("Query: {}", query); FullTextQuery ftQuery = getFullTextEntityManager().createFullTextQuery(query, Affiliate.class); ftQuery.setFirstResult(pageableUtil.getStartPosition(pageable)); ftQuery.setMaxResults(pageable.getPageSize()); @SuppressWarnings("unchecked") List<Affiliate> resultList = ftQuery.getResultList(); dumpDebugInfoWithScores(ftQuery); LOG.debug("Found {} results for query: {}", ftQuery.getResultSize(), q); return new PageImpl<>(resultList, pageable, ftQuery.getResultSize()); } private Query buildQuery(String q, Member homeMember, StandingState standingState, boolean standingStateNot) { QueryBuilder queryBuilder = getSearchFactory().buildQueryBuilder() .forEntity(Affiliate.class).get(); //Odd issue with Camel case text not finding exact matches. Workaround using lowercase. Query textQuery = buildWildcardQueryForTokens(queryBuilder, q.toLowerCase()); if (homeMember == null && standingState == null) { return textQuery; } else { BooleanJunction <MustJunction> building = queryBuilder .bool() .must(textQuery); if (homeMember != null) { Query homeMemberQuery = buildQueryMatchingHomeMember(queryBuilder, homeMember); building = building.must(homeMemberQuery); } if (standingState != null) { Query standingStateQuery = buildQueryMatchingStandingState(queryBuilder, standingState); MustJunction standingStateBuilding = building.must(standingStateQuery); if (standingStateNot) { // Caught up in .not() returning boolean rather than must... building = standingStateBuilding.not(); } else { building = standingStateBuilding; } } return building.createQuery(); } } private SearchFactory getSearchFactory() { return getFullTextEntityManager().getSearchFactory(); } private FullTextEntityManager getFullTextEntityManager() { return Search.getFullTextEntityManager(entityManager); } @SuppressWarnings("unchecked") void dumpDebugInfoWithScores(FullTextQuery ftQuery) { if (LOG.isTraceEnabled()) { ftQuery.setProjection( FullTextQuery.DOCUMENT_ID, FullTextQuery.SCORE, FullTextQuery.EXPLANATION, "affiliateDetails.organizationName" ); for (Object[] projection : (List<Object[]>) ftQuery.getResultList()) { LOG.trace("Projection: {}",Arrays.asList(projection)); } } } Query buildQueryMatchingHomeMember(QueryBuilder queryBuilder, Member homeMember) { Query homeMemberQuery = queryBuilder.keyword() .onField("homeMember") .matching(homeMember.getKey()) .createQuery(); return homeMemberQuery; } Query buildQueryMatchingStandingState(QueryBuilder queryBuilder, StandingState standingState) { Query standingStateQuery = queryBuilder.keyword() .onField("standingState").matching(standingState) .createQuery(); return standingStateQuery; } Query buildWildcardQueryForTokens(QueryBuilder queryBuilder, String q) { BooleanJunction<?> bool = queryBuilder.bool(); //Analyzer searchtokenAnalyzer = getSearchFactory().getAnalyzer("searchtokenanalyzer"); try { // We're letting the default analyzer tokenize the main query for the ALL field Query allKeywordQuery = queryBuilder.keyword() .onField(FIELD_ALL).ignoreFieldBridge().matching(q) .createQuery(); bool.should(allKeywordQuery); } catch (EmptyQueryException e) { // ignore it, and allow the full query since we have a limit. } // And then we do a dumb split on whitespace and turn every string into a wildcard query // on all the fields. String[] tokens = q.split("\\s+"); for (String token : tokens) { bool.should(queryBuilder.keyword() .wildcard() .onField(FIELD_ALL).ignoreFieldBridge().matching(token+"*") .createQuery()); bool.should(queryBuilder.keyword() .wildcard() .onField("address.ALL").ignoreFieldBridge().matching(token+"*") .createQuery()); bool.should(queryBuilder.keyword() .wildcard() .onField("address.country.commonName").ignoreFieldBridge().matching(token+"*") .createQuery()); bool.should(queryBuilder.keyword() .wildcard() .onField("billingAddress.ALL").matching(token+"*") .createQuery()); bool.should(queryBuilder.keyword() .wildcard() .onField("billingAddress.country.commonName").matching(token+"*") .createQuery()); bool.should(queryBuilder.keyword() .wildcard() .onField("email").matching(token+"*") .createQuery()); } Query textQuery = bool.createQuery(); return textQuery; } }