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

JPA Criteria API - possible to do a prefixed, tokenized search with wildcards?

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

这样的句子

"The quick brown fox jumped over the lazy dog" 搜索 "jump" 时可能会找到。我想我需要定义一个像(伪代码)这样​​的规则:

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

我假设可以使用 JPA(标准 API)来做到这一点?但是如果我们有两个术语怎么办?所有这些都必须用 AND 组合,例如上面的规则应该导致至少一列中的两个术语为 TRUE。这意味着 "jump fox" 会导致命中,但 "jump rabbit" 不会。 Criteria API 也可以吗?

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

基于下面的答案,这里是我的完整解决方案。一切都在一个方法中以保持这里的简单(尽管 "simple JPA criteria 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<>();
    }
}

条件 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. 创建您自己的索引:使用适当的分词器分析您要搜索的列,并将生成的分词放在单独的 table 中,并反向引用原始 table . 现在搜索您要查找的术语。 只要您只进行前缀搜索,数据库索引就应该能够保持这种合理的效率,并且比您通过单独使用条件 API 获得的更容易维护和更灵活。