JPA Criteria API-可以使用通配符进行带前缀的标记化搜索吗?

时间:2018-12-05 08:37:08

标签: jpa spring-data-jpa criteria criteria-api

我们有一个问题,目前不允许使用ElasticSearch,因此我们需要使用MySQL实现搜索功能。一个理想的功能是前缀的标记化搜索,因此像

“敏捷的棕色狐狸跳过了懒狗” 搜索“跳转”时可以找到。我想我需要定义一个规则,例如(pseudocode):

(*)(beginning OR whitespace)(prefix)(*)

我认为使用JPA(标准API)可以做到这一点吗?但是,如果我们有两个条件呢?所有这些都必须由AND合并,例如上述规则应至少在一列中对两个术语得出TRUE。这意味着“跳狐”将导致命中,而“跳兔”则不会。使用Criteria API也可以吗?

或者您知道比Criteria API更好的解决方案吗?我听说Hibernate可以更优雅地执行LIKE查询(使用更少的代码),但是不幸的是我们使用EclipseLink。

基于以下答案,这是我的完整解决方案。这是一种简单的方法(不过“简单的JPA条件API”是矛盾的)。如果有人想使用它,请考虑进行一些重构

public List<Customer> findMatching(String searchPhrase) {
    List<String> searchTokens = TextService.splitPhraseIntoNonEmptyTokens(searchPhrase);
    if (searchTokens.size() < 1 || searchTokens.size() > 5) { // early out and denial of service attack prevention
        return new ArrayList<>();
    }

    CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
    CriteriaQuery<Customer> criteriaQuery = criteriaBuilder.createQuery(Customer.class);

    Root<Customer> rootEntity = criteriaQuery.from(Customer.class);

    Predicate[] orClausesArr = new Predicate[searchTokens.size()];

    for (int i = 0; i < searchTokens.size() ; i++) {
        // same normalization methods are used to create the indexed searchable data
        String assumingKeyword = TextService.normalizeKeyword(searchTokens.get(i));
        String assumingText = TextService.normalizeText(searchTokens.get(i));
        String assumingPhoneNumber = TextService.normalizePhoneNumber(searchTokens.get(i));

        String assumingKeywordInFirstToken = assumingKeyword + '%';
        String assumingTextInFirstToken = assumingText + '%';
        String assumingPhoneInFirstToken = assumingPhoneNumber + '%';
        String assumingTextInConsecutiveToken = "% " + assumingText + '%';

        Predicate query = criteriaBuilder.or(
            criteriaBuilder.like(rootEntity.get("normalizedCustomerNumber"), assumingKeywordInFirstToken),
            criteriaBuilder.like(rootEntity.get("normalizedPhone"), assumingPhoneInFirstToken),
            criteriaBuilder.like(rootEntity.get("normalizedFullName"), assumingTextInFirstToken),
            // looking for a prefix after a whitespace:
            criteriaBuilder.like(rootEntity.get("normalizedFullName"), assumingTextInConsecutiveToken)
        );
        orClausesArr[i] = query;
    }

    criteriaQuery = criteriaQuery
            .select(rootEntity) // you can also select only the display columns and ignore the normalized/search columns
            .where(criteriaBuilder.and(orClausesArr))
            .orderBy(
                    criteriaBuilder.desc(rootEntity.get("customerUpdated")),
                    criteriaBuilder.desc(rootEntity.get("customerCreated"))
            );
    try {
        return entityManager
                .createQuery(criteriaQuery)
                .setMaxResults(50)
                .getResultList();
    } catch (NoResultException nre) {
        return new ArrayList<>();
    }
}

1 个答案:

答案 0 :(得分:0)

Criteria API当然不打算用于此目的,但可以用于create LIKE predicates

因此,对于每个搜索词和要搜索的每个列,您都将创建如下内容:

column like :term + '%'
or column like ' ' + :term + '%'
or column like ',' + :term + '%'
// repeat for all other punctuation marks and forms of whitespace you want to support.

这将导致效率低下的查询!

我看到以下选择:

  1. 使用特定于数据库的功能。一些数据库具有某些文本搜索功能。 如果您可以将应用程序限制为一个或几个可能有效的数据库。

  2. 创建您自己的索引:使用适当的令牌生成器分析要搜索的列,并将生成的令牌放置在具有对原始表的反向引用的单独表中。 现在搜索所需的术语。 只要您只进行前缀搜索,数据库索引就应该能够保持这种合理的效率,并且比单独使用Criteria API所获得的索引更易于维护和更灵活。