使用 Lucene 解析搜索查询并基于此构建 Hibernate 条件

Parse search query with Lucene and build Hibernate criteria based on that

要求在单独的单个 table 中保存的有限数量的字段上构建简化的搜索功能。目前不能使用 Solr 或类似工具,一切都必须在一个 webapp 中运行。数据库是MSSQL。我想做的是利用 Lucene 查询解析器并从中构建 Hibernate 标准。尽管我最初的印象是它不会太难,但我不知道如何为复杂查询建立条件。

这是我创建的一个快速测试,用于使用 Lucene (4.7.2) 解析查询字符串

Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_47);
QueryParser luceneParser = new QueryParser(Version.LUCENE_47, "", analyzer);
String queryString = "(name:\"Luke Skywalker\" AND father:unknown OR fname:Luke) or (name:yoda)";
Query luceneQuery = luceneParser.parse(queryString);

.....

public class QueryInterpreter {
    public void parse(Query query) {
        if (query instanceof TermQuery) {
            termQuery((TermQuery) query);
        } else if (query instanceof BooleanQuery) {
            booleanQuery((BooleanQuery) query);
        } else if (query instanceof PhraseQuery) {
            phraseQuery((PhraseQuery) query);
        } else {
            throw new IllegalArgumentException("");
        }
    }
    public void booleanQuery(BooleanQuery query) {
        for (BooleanClause clause : query.getClauses()) {
            parse(clause.getQuery());
        }
    }
    public void phraseQuery(PhraseQuery query) {
        StringBuilder sb = new StringBuilder();
        for (Term term : query.getTerms()) {
            sb.append(term.text());
            sb.append(" ");
        }

    }
    public void termQuery(TermQuery query) {
        Term term = query.getTerm();
    }
}

Lucene 首先将搜索字符串转换为 (+name:\"Luke Skywalker\" +father:unknown fname:Luke) name:yoda。基本上,然后它会遍历为每个术语设置 isRequired() 的术语。 Hibernate 的工作方式不同——您创建一个条件对象并不断添加具有值对的条件。而且我无法弄清楚如何将一个转换为另一个。我想我需要的是一个通用的 Junction 对象来附加 Criterions。

终于弄明白了,将在这里分享我的解决方案,以防有人遇到同样的问题。

朝着正确方向迈出的第一步是认识到 QueryParser 不能很好地处理布尔逻辑。例如 (+name:\"Luke Skywalker\" +father:unknown fname:Luke) name:yoda 是与 (name:\"Luke Skywalker\" AND father:unknown OR fname:Luke) or (name:yoda) 不同的搜索。不知道为什么 QueryParser 甚至接受布尔逻辑,这简直令人困惑。

解决方法是使用PrecedenceQueryParser.

Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_47);
PrecedenceQueryParser luceneParser = new PrecedenceQueryParser(analyzer);
luceneParser.setAllowLeadingWildcard(true);
Query luceneQuery = luceneParser.parse(searchQuery, "name");

然后从中创建一个 Hibernate 标准。显然,您不能在关系数据库中支持所有 Lucene 搜索功能,但这从来都不是必需的。

public Criterion buildHibernateQuery(Query luceneQuery) {
    return parse(luceneQuery);
}

private Criterion parse(Query query) {
    if (query instanceof TermQuery) {
        return parse((TermQuery) query);
    } else if (query instanceof BooleanQuery) {
        return parse((BooleanQuery) query);
    } else if (query instanceof PhraseQuery) {
        return parse((PhraseQuery) query);
    } else if (query instanceof PrefixQuery) {
        return parse((PrefixQuery) query);
    } else if (query instanceof WildcardQuery) {
        return parse((WildcardQuery) query);
    } else {
        LOG.error(String.format("%s unsupported", query.getClass()));
    }
}

private Criterion parse(TermQuery query) {
    Term term = query.getTerm();
    return createNameValueRestriction(term.field(), term.text());
}

private Criterion parse(BooleanQuery query) {
    if (query.getClauses().length == 1) {
        return parse(query.getClauses()[0].getQuery());
    }
    Junction junction = createJunction(query.getClauses()[0]);

    for (BooleanClause clause: query.getClauses()) {
        junction.add(parse(clause.getQuery()));
    }
    return junction;
}

private Junction createJunction(BooleanClause booleanClause) {
    if (booleanClause.isRequired()) {
        return Restrictions.conjunction();
    } else {
        return Restrictions.disjunction();
    }
}

private Criterion parse(PhraseQuery query) {
    String field = query.getTerms()[0].field();
    StringBuilder phraseBuilder = new StringBuilder();
    for (Term term : query.getTerms()) {
        phraseBuilder.append(term.text());
        phraseBuilder.append(" ");
    }

    return createNameValueRestriction(field, phraseBuilder.toString().trim());
}

private Criterion createNameValueRestriction(String field, String value) {
    return Restrictions.and(
            Restrictions.eq("jsonPath", field),
            Restrictions.eq("answer", value)
            );
}

private Criterion parse(PrefixQuery query) {
    Term term = query.getPrefix();
    return parseLikeQuery(term.field(), term.text(), MatchMode.START);
}

private Criterion parse(WildcardQuery query) {
    Term term = query.getTerm();
    String wildCardEscaped = Pattern.quote(String.valueOf(WildcardQuery.WILDCARD_STRING));
    String termText = term.text().replaceAll(wildCardEscaped, "");
    return parseLikeQuery(term.field(), termText, MatchMode.ANYWHERE);
}


private Criterion parseLikeQuery(String field, String value, MatchMode matchMode) {
    return Restrictions.and(
            Restrictions.eq("jsonPath", field),
            Restrictions.like("answer", value, matchMode)
            );
}

希望有人会觉得这有用。