BooleanQuery常用来对实现多个Query子类对象的进行组合,这些Query子类对象会组成一个Cluase实现组合查询。每一个Query都有四种可选,分别描述了匹配的文档需要满足的要求,定义在 BooleanClause类中,如下:
xxxxxxxxxx
181public static enum Occur {
2 /** Use this operator for clauses that <i>must</i> appear in the matching documents. */
3 MUST { public String toString() { return "+"; } },
4 /** Like {@link #MUST} except that these clauses do not participate in scoring. */
5 FILTER { public String toString() { return "#"; } },
6 /** Use this operator for clauses that <i>should</i> appear in the
7 * matching documents. For a BooleanQuery with no <code>MUST</code>
8 * clauses one or more <code>SHOULD</code> clauses must match a document
9 * for the BooleanQuery to match.
10 * @see BooleanQuery.Builder#setMinimumNumberShouldMatch
11 */
12 SHOULD { public String toString() { return ""; } },
13 /** Use this operator for clauses that <i>must not</i> appear in the matching documents.
14 * Note that it is not possible to search for queries that only consist
15 * of a <code>MUST_NOT</code> clause. These clauses do not contribute to the
16 * score of documents. */
17 MUST_NOT { public String toString() { return "-"; } };
18 }
满足查询要求的文档中必须包含查询的关键字。
满足查询要求的文档中包含一个或多个查询的关键字。
满足查询要求的文档中必须包含查询的关键字,但是这个Query不会参与文档的打分。
满足查询要求的文档中必须不能包含查询的关键字。
xxxxxxxxxx
11例子:“+a b -c d”
转化为代码如下:
xxxxxxxxxx
51BooleanQuery.Builder query = new BooleanQuery.Builder();
2query.add(new TermQuery(new Term("content", "a")), BooleanClause.Occur.MUST);
3query.add(new TermQuery(new Term("content", "b")), BooleanClause.Occur.SHOULD);
4query.add(new TermQuery(new Term("content", "c")), BooleanClause.Occur.MUST_NOT);
5query.add(new TermQuery(new Term("content", "d")), BooleanClause.Occur.SHOULD);
满足查询要求的文档必须包含 "a",,不能包含“c”,,可以包含“b”,“d”中一个或者多个,包含的越多,文档的分数越高
xxxxxxxxxx
31 public Builder setMinimumNumberShouldMatch(int min) {
2 ......
3 }
当查询有多个SHOULD的Query对象时,满足查询要求的文档中必须包含minimumNumberShouldMatch个Query的关键字
xxxxxxxxxx
41public Weight createWeight(IndexSearcher searcher, boolean needsScores, float boost) throws IOException {
2 ...
3 return new BooleanWeight(query, searcher, needsScores, boost);
4 }
Query对象的子类都会重写这个方法。对于BooleanQuery的createWeight(...)实现,只是调用了对象组合中的所有Query子类的createWeight(...)方法分别生成Weight对象,然后将这些对象封装到BooleanWeight对象中。TermQuery的createWeight()的具体实现看博客的文章 TermQuery。
xxxxxxxxxx
31public Query rewrite(IndexReader reader) throws IOException {
2 ...
3 }
BooleanQuery的rewrite(..)跟createWeight(...)相同的是都是调用对象组合中所有Query子类的rewrite(...)方法,但是并不是所有的Query都需要重写。比如TermQuery,他就没有重写父类的rewrite(...)方法,而对于PrefixQuery(前缀查询),则必须要重写, 重写后的PrefixQuery会生成多个TermQuery,最后组合成BooleanQuery。
xxxxxxxxxx
11例子:前缀查询关键字 "ca*", 重写后,会变成 "car", "cat", ...每一个关键字作为TermQuery,组合成BooleanQuery进行查询,所以一般都禁用PreFixQuery,容易抛出TooManyClause的异常。
BooleanQuery的rewrite(...)实现中一共有9个逻辑(下面的会对每一种逻辑进行标注,比如说 逻辑一),根据BooleanQuery中的不同的组合(MUST, SHOULD, MUST_NOT, FILTER的任意组合), 会至少执行1个多个重写逻辑,我们对最常用的组合来描述重写的过程。
直接返回。。。不需要重写。
xxxxxxxxxx
231// 逻辑一
2if (clauses.size() == 1) {
3 BooleanClause c = clauses.get(0);
4 Query query = c.getQuery();
5 if (minimumNumberShouldMatch == 1 && c.getOccur() == Occur.SHOULD) {
6 return query;
7 } else if (minimumNumberShouldMatch == 0) {
8 switch (c.getOccur()) {
9 case SHOULD:
10 case MUST:
11 // 直接返回原Query。
12 return query;
13 case FILTER:
14 // no scoring clauses, so return a score of 0
15 return new BoostQuery(new ConstantScoreQuery(query), 0);
16 case MUST_NOT:
17 // no positive clauses
18 return new MatchNoDocsQuery("pure negative BooleanQuery");
19 default:
20 throw new AssertionError();
21 }
22 }
23 }
首先遍历BooleanQuery中的所有Query对象,调用他们自身的重写方法,由于TermQuery不需要重写,所以直接返回自身。
xxxxxxxxxx
221// 逻辑二
2{
3 // 重新生成一个BooleanQuery的构建器,准备对重写后的Query进行组合。
4 BooleanQuery.Builder builder = new BooleanQuery.Builder();
5 // 设置一样的MinimumNumberShouldMatch。
6 builder.setMinimumNumberShouldMatch(getMinimumNumberShouldMatch());
7 boolean actuallyRewritten = false;
8 for (BooleanClause clause : this) {
9 Query query = clause.getQuery();
10 // 调用Query子类的rewrite(...)方法
11 // 我们的例子中都是TermQuery,所以直接返回自身this。
12 Query rewritten = query.rewrite(reader);
13 if (rewritten != query) {
14 actuallyRewritten = true;
15 }
16 builder.add(rewritten, clause.getOccur());
17 }
18 // 由于我们例子中的的BooleanQuery的Query子类都是TermQuery,不需要重写,所以就不用生成新的BooleanQuery对象
19 if (actuallyRewritten) {
20 return builder.build();
21 }
22}
如果minimumNumberShouldMatch的值 <= 1那么需要执行第二步。 当有多个相同的TermQuery,并且是SHOULD,会将这些相同的TermQuery封住成一个BoostQuery,增加boost的值。
xxxxxxxxxx
321// 逻辑七
2// 这段代码的逻辑跟 逻辑八 一毛一样,往下找一找^-^,就不赘述了
3if (clauseSets.get(Occur.SHOULD).size() > 0 && minimumNumberShouldMatch <= 1) {
4 Map<Query, Double> shouldClauses = new HashMap<>();
5 for (Query query : clauseSets.get(Occur.SHOULD)) {
6 double boost = 1;
7 while (query instanceof BoostQuery) {
8 BoostQuery bq = (BoostQuery) query;
9 boost *= bq.getBoost();
10 query = bq.getQuery();
11 }
12 shouldClauses.put(query, shouldClauses.getOrDefault(query, 0d) + boost);
13 }
14 if (shouldClauses.size() != clauseSets.get(Occur.SHOULD).size()) {
15 BooleanQuery.Builder builder = new BooleanQuery.Builder()
16 .setMinimumNumberShouldMatch(minimumNumberShouldMatch);
17 for (Map.Entry<Query,Double> entry : shouldClauses.entrySet()) {
18 Query query = entry.getKey();
19 float boost = entry.getValue().floatValue();
20 if (boost != 1f) {
21 query = new BoostQuery(query, boost);
22 }
23 builder.add(query, Occur.SHOULD);
24 }
25 for (BooleanClause clause : clauses) {
26 if (clause.getOccur() != Occur.SHOULD) {
27 builder.add(clause);
28 }
29 }
30 return builder.build();
31 }
32 }
下图中左边是重写前的BooleanQuery,右边是重写后的BooleanQuery。
同样先要执行 逻辑二,不赘述。
同样先要执行 逻辑七,不赘述。
下图中左边是重写前的BooleanQuery,右边是重写后的BooleanQuery。
当有多个相同的TermQuery,并且是MUST,会将这些相同的TermQuery封住成一个BoostQuery,增加boost的值。
xxxxxxxxxx
381// 逻辑八
2if (clauseSets.get(Occur.MUST).size() > 0) {
3 Map<Query, Double> mustClauses = new HashMap<>();
4 // 这里遍历所有的MUST的Clause,如果有重复的Clause,boost值就加1,描述了这个关键字的重要性
5 for (Query query : clauseSets.get(Occur.MUST)) {
6 double boost = 1;
7 while (query instanceof BoostQuery) {
8 BoostQuery bq = (BoostQuery) query;
9 boost *= bq.getBoost();
10 query = bq.getQuery();
11 }
12 // 调用getOrDefault()查看是否有相同的clause,如果有,那么取出boost,然后对boost进行+1后,覆盖已经存在的clause。
13 mustClauses.put(query, mustClauses.getOrDefault(query, 0d) + boost);
14 }
15 // 运行至此,如果BooleanQuery有相同的query,并且是MUST,那么将这些MUST的query合并为一个query,并且增加boost的值。
16 // if语句为true:说明有重复的clause(MUST), 那么需要对boost不等于1的query重写,然后跟其他的query一起写到新的BooleanQuery中。
17 if (mustClauses.size() != clauseSets.get(Occur.MUST).size()) {
18 BooleanQuery.Builder builder = new BooleanQuery.Builder()
19 .setMinimumNumberShouldMatch(minimumNumberShouldMatch);
20 // 这个for循环是将那些boost值不等于1的query重写为BoostQuery。
21 for (Map.Entry<Query,Double> entry : mustClauses.entrySet()) {
22 Query query = entry.getKey();
23 float boost = entry.getValue().floatValue();
24 if (boost != 1f) {
25 // 重写为BoostQuery。
26 query = new BoostQuery(query, boost);
27 }
28 builder.add(query, Occur.MUST);
29 }
30 // 把其他不是MUST的clause重写添加到新的BooleanQuery中。
31 for (BooleanClause clause : clauses) {
32 if (clause.getOccur() != Occur.MUST) {
33 builder.add(clause);
34 }
35 }
36 return builder.build();
37 }
38 }
下图中左边是重写前的BooleanQuery,右边是重写后的BooleanQuery。
同样先要执行 逻辑二,不赘述。
当有多个相同的TermQuery,并且是MUST,会将这些相同的TermQuery封住成一个BoostQuery,增加boost的值。然后执行逻辑八,已说明,不赘述。
同样先要执行 逻辑二,不赘述。
如果在这个逻辑中返回了,那么就会返回一个MatchNoDocsQuery对象,也就是不会搜索到任何结果。
xxxxxxxxxx
131// 逻辑四
2 final Collection<Query> mustNotClauses = clauseSets.get(Occur.MUST_NOT);
3 if (!mustNotClauses.isEmpty()) {
4 final Predicate<Query> p = clauseSets.get(Occur.MUST)::contains;
5 // 判断是否MUST_NOT跟MUST或FILTER是否有相同的term
6 if (mustNotClauses.stream().anyMatch(p.or(clauseSets.get(Occur.FILTER)::contains))) {
7 return new MatchNoDocsQuery("FILTER or MUST clause also in MUST_NOT");
8 }
9 // 判断是否有MatchAllDocsQuery的Query
10 if (mustNotClauses.contains(new MatchAllDocsQuery())) {
11 return new MatchNoDocsQuery("MUST_NOT clause is MatchAllDocsQuery");
12 }
13 }
当有多个相同的TermQuery,并且是MUST,会将这些相同的TermQuery封住成一个BoostQuery,增加boost的值。然后执行逻辑八,已说明,不赘述。
下图中左边是重写前的BooleanQuery,右边是重写后的BooleanQuery。
同样先要执行 逻辑二,不赘述。
执行逻辑四,不赘述。
执行逻辑七,不赘述。
下图中左边是重写前的BooleanQuery,右边是重写后的BooleanQuery。
同样先要执行 逻辑二,不赘述。
执行逻辑四,不赘述。
执行逻辑七,不赘述。
执行逻辑八,不赘述。
下图中左边是重写前的BooleanQuery,右边是重写后的BooleanQuery。
同样先要执行 逻辑二,不赘述。
因为对于FILTER的Query中的term,他只是不参与打分,但是搜索结果必须包含这个term,如果SHOULD的Query中也有这个term,那么将这个Query的SHOULD改为MUST, 然后minShouldMatch的值就必须少一个,注意的是FILTER的这个Query没有放到新的BooleanQuery中。
xxxxxxxxxx
321// 逻辑六
2if (clauseSets.get(Occur.SHOULD).size() > 0 && clauseSets.get(Occur.FILTER).size() > 0) {
3 final Collection<Query> filters = clauseSets.get(Occur.FILTER);
4 final Collection<Query> shoulds = clauseSets.get(Occur.SHOULD);
5
6 Set<Query> intersection = new HashSet<>(filters);
7 // 在intersection中保留 FILTER跟SHOUL有相同的term的Query
8 intersection.retainAll(shoulds);
9
10 // if语句为真:说明至少有一个term,他即有FILTER又有SHOULD的Query
11 if (intersection.isEmpty() == false) {
12 // 需要重新生成一个BooleanQuery
13 BooleanQuery.Builder builder = new BooleanQuery.Builder();
14 int minShouldMatch = getMinimumNumberShouldMatch();
15
16 for (BooleanClause clause : clauses) {
17 if (intersection.contains(clause.getQuery())) {
18 if (clause.getOccur() == Occur.SHOULD) {
19 // 将SHOULD 改为 MUST
20 builder.add(new BooleanClause(clause.getQuery(), Occur.MUST));
21 // 对minShouldMatch的值减一,因为这个SHOULD的Query的term,同样是FILTER的term,满足匹配要求的文档必须包含这个term
22 minShouldMatch--;
23 }
24 } else {
25 builder.add(clause);
26 }
27 }
28 // 更新minShouldMatch
29 builder.setMinimumNumberShouldMatch(Math.max(0, minShouldMatch));
30 return builder.build();
31 }
32 }
执行逻辑七,不赘述。
执行逻辑八,不赘述。
下图中左边是重写前的BooleanQuery,右边是重写后的BooleanQuery。
同样先要执行 逻辑二,不赘述。
判断是否存在一个term对应的Query即是 MUST又是FILTER,如果存在,那么移除FILTER的Query。
xxxxxxxxxx
211// 逻辑六
2if (clauseSets.get(Occur.MUST).size() > 0 && clauseSets.get(Occur.FILTER).size() > 0) {
3 // 获得所有的FILTER的Query
4 final Set<Query> filters = new HashSet<Query>(clauseSets.get(Occur.FILTER));
5 boolean modified = filters.remove(new MatchAllDocsQuery());
6 // 从filters中移除既是FILTER又是MUST的Query
7 modified |= filters.removeAll(clauseSets.get(Occur.MUST));
8 if (modified) {
9 BooleanQuery.Builder builder = new BooleanQuery.Builder();
10 builder.setMinimumNumberShouldMatch(getMinimumNumberShouldMatch());
11 for (BooleanClause clause : clauses) {
12 if (clause.getOccur() != Occur.FILTER) {
13 builder.add(clause);
14 }
15 }
16 for (Query filter : filters) {
17 builder.add(filter, Occur.FILTER);
18 }
19 return builder.build();
20 }
21 }
执行逻辑八,不赘述。
下图中左边是重写前的BooleanQuery,右边是重写后的BooleanQuery。
BooleanQuery类中最重要的方法就是rewrite()方法,上面的例子中列举了最常用的几种BooleanQuery的情况,MUST,SHOULD,FILTER,MUST_NOT的他们之间不同数量的组合有着不一样的rewrite过程,无法一一详细列出。BooleanQuery中的rewrite一共有9个逻辑,都在关键处给出了注释,大家可以到我们的GitHub看这个类的源码https://github.com/luxugang/Lucene-7.5.0/blob/master/solr-7.5.0/lucene/core/src/java/org/apache/lucene/search/BooleanQuery.java。
点击下载Markdown文件