原文地址:http://codepub.cn/2017/11/15/lucene-group-statistics-detailed/
拋出問題
在 RDBMS 中,我們可以使用 GROUP BY 來對檢索的數據進行分組,同樣地,想要在 Lucene 中實現分組要如何做呢?首先思考如下幾個問題
- Lucene 是如何實現分組的?
- 用來分組的字段(域)或者說 Field 如何添加?
- 組的大小如何設置?
- 組內大小如何設置?
- 如何實現組的分頁?
- 如果結果集超過了組內大小,可以通過分頁解決,那么如果結果集超過了組大小的上限,如何解決?
- 如何實現單類別分組,即類似SQL中的 GROUP BY A
- 如何實現多類別分組,即類似SQL中的 GROUP BY A, B
從 SQL 的 GROUP BY 說起
如果分組后面只有一個字段,如 GROUP BY A 意思是將所有具有相同A字段值的記錄放到一個分組里。那么如果是GROUP BY A, B呢?其意思是將所有具有相同A字段值和B字段值的記錄放到一個分組里,在這里A和B之間是邏輯與的關系。
通常的,如果在SQL中,我們僅用 GROUP BY 語句而不加 WHERE 條件的話,那么相當於在全部數據中進行分組,對應於 Lucene 中相當於使用 GROUP 加 new MatchAllDocsQuery() 的功能。
而如果在SQL中,我們不僅用 GROUP BY 還有 WHERE 條件語句,那么相當於在滿足 WHERE 條件的記錄中進行分組,這種 WHERE 條件在 Lucene 中可以通過構造各種不同的 Query 進行過濾,然后在符合條件的結果中分組。
Lucene 分組
有關Lucene分組問題,需要有一系列輸入參數,官方Doc在此,核心點如下
- groupField:用來分組的域,在 Lucene 中,這個域只能設置一個,不像 SQL 中可以根據多個列分組。沒有該域的文檔將被分到一個單獨的組里面
- groupSort:組間排序方式,用來指定如何對不同的分組進行排序,而不是組內的文檔排序,默認值是
Sort.RELEVANCE
- topNGroups:保留多少組,例如10只取前十個分組
- groupOffset:指定組偏移量,比如當topNGroups的值是10的時候,groupOffset為3,則意思是返回7個分組,跳過前面3個,在分頁時候很有用
- withinGroupSort:組內排序方式,默認值是
Sort.RELEVANCE
,注意和groupSort的區別,不要求和groupSort使用一樣的排序方式 - maxDocsPerGroup:表示一個組內最多保留多少個文檔
- withinGroupOffset:每組顯示的文檔的偏移量
分組通常有兩個階段,第一階段用FirstPassGroupingCollector
收集不同的分組,第二階段用SecondPassGroupingCollector
收集這些分組內的文檔,如果分組很耗時,建議用CachingCollector
類,可以緩存 hits 並在第二階段快速返回。這種方式讓你相當於只運行了一次 query,但是付出的代價是用 RAM 持有所有的 hits。返回的結果集是TopGroups的實例。
Groups是由GroupSelector(抽象類)的實現來定義的,目前支持兩種實現方式
- TermGroupSelector 基於 SortedDocValues 域進行分組
- ValueSourceGroupSelector 基於 ValueSource 值進行分組
通常不建議直接使用 FirstPassGroupingCollector 和 SecondPassGroupingCollector 來進行分組操作,因為Lucene提供了一個非常簡便的封裝類 GroupingSearch,目前分組操作還不支持 Sharding。
網上有許多講解 Lucene 分組的文章,但是講的都非常淺顯,一般都是取 Top N 個分組,這個 N 是一個確定的值,試問如果我要對全部的結果集進行分組統計,而分組數量超過 Top N 的話,那么這種方式統計的結果顯然是不准確的,因為它並沒有統計全部的數據。還有的是直接把 maxDoc()
函數的值作為 groupLimit
的值,然后對某個分組內的全部文檔進行迭代,無法實現組內分頁的問題。
所以本文就針對這個問題,不僅解決了組內分頁的問題,還解決了組間分頁的問題,可以迭代完全的結果集。
另外一個需要注意的問題就是 maxDoc()
可能返回的是 Integer
型的上限,而將其直接作為 groupLimit 傳入的話,是會報錯的,錯誤如下
組內大小和組間大小如果設置為Integer.MAX_VALUE報
Exception in thread “main” java.lang.NegativeArraySizeException
組內大小和組間大小如果設置為Integer.MAX_VALUE-1報
Exception in thread “main” java.lang.IllegalArgumentException: maxSize must be <= 2147483630; got: 2147483646
完整示例如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
|
import org.apache.lucene.analysis.core.WhitespaceAnalyzer;
import org.apache.lucene.document.*;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.RAMDirectory;
import org.apache.lucene.util.BytesRef;
import java.io.IOException;
/**
* <p>
* Created by wangxu on 2017/11/14 16:41.
* </p>
* <p>
* Description: 基於 Lucene 7.0.0
* </p>
*
* @author Wang Xu
* @version V1.0.0
* @since V1.0.0 <br/>
* WebSite: http://codepub.cn <br>
* Licence: Apache v2 License
*/
public class IndexHelper {
private Document document;
private Directory directory;
private IndexWriter indexWriter;
public Directory getDirectory() {
directory = (directory == null) ? new RAMDirectory() : directory;
return directory;
}
private IndexWriterConfig getConfig() {
return new IndexWriterConfig(new WhitespaceAnalyzer());
}
private IndexWriter getIndexWriter() {
try {
return new IndexWriter(getDirectory(), getConfig());
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public IndexSearcher getIndexSearcher() throws IOException {
return new IndexSearcher(DirectoryReader.open(getDirectory()));
}
public void createIndexForGroup(int ID, String author, String content) {
indexWriter = getIndexWriter();
document = new Document();
//IntPoint默認是不存儲的
document.add(new IntPoint("ID", ID));
//如果想要在搜索結果中獲取ID的值,需要加上下面語句
document.add(new StoredField("ID", ID));
document.add(new StringField("author", author, Field.Store.YES));
//需要使用特定的field存儲分組,需要排序及分組的話,要加上下面語句,注意默認SortedDocValuesField也是不存儲的
document.add(new SortedDocValuesField("author", new BytesRef(author)));
document.add(new StringField("content", content, Field.Store.YES));
try {
indexWriter.addDocument(document);
indexWriter.commit();
indexWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
|
import org.apache.lucene.index.Term;
import org.apache.lucene.search.*;
import org.apache.lucene.search.grouping.GroupDocs;
import org.apache.lucene.search.grouping.GroupingSearch;
import org.apache.lucene.search.grouping.TopGroups;
import org.apache.lucene.util.BytesRef;
import java.io.IOException;
/**
* <p>
* Created by wangxu on 2017/11/14 16:21.
* </p>
* <p>
* Description: 基於 Lucene 7.0.0 開發
* </p>
*
*
@author Wang Xu
*
@version V1.0.0
*
@since V1.0.0 <br/>
* WebSite: http://codepub.cn <br>
* Licence: Apache v2 License
*/
public class GroupingDemo {
public static void main(String[] args) throws Exception {
IndexHelper indexHelper =
new IndexHelper();
indexHelper.createIndexForGroup(
1, "Java", "一周精通Java");
indexHelper.createIndexForGroup(
2, "Java", "一周精通MyBatis");
indexHelper.createIndexForGroup(
3, "Java", "一周精通Struts");
indexHelper.createIndexForGroup(
4, "Java", "一周精通Spring");
indexHelper.createIndexForGroup(
5, "Java", "一周精通Spring Cloud");
indexHelper.createIndexForGroup(
6, "Java", "一周精通Hibernate");
indexHelper.createIndexForGroup(
7, "Java", "一周精通JVM");
indexHelper.createIndexForGroup(
8, "C", "一周精通C");
indexHelper.createIndexForGroup(
9, "C", "C語言詳解");
indexHelper.createIndexForGroup(
10, "C", "C語言調優");
indexHelper.createIndexForGroup(
11, "C++", "一周精通C++");
indexHelper.createIndexForGroup(
12, "C++", "C++語言詳解");
indexHelper.createIndexForGroup(
13, "C++", "C++語言調優");
IndexSearcher indexSearcher = indexHelper.getIndexSearcher();
GroupingDemo groupingDemo =
new GroupingDemo();
//把所有的文檔都查出來,由添加的數據可以知道,一共有三組,Java組有7個文檔,C和C++組分別都有3個文檔
//當然了如果做全匹配的話,還可以用new MatchAllDocsQuery()
BooleanQuery query =
new BooleanQuery.Builder().add(new TermQuery(new Term("author", "Java")), BooleanClause.Occur.SHOULD).add(new TermQuery(new Term
(
"author", "C")),
BooleanClause.Occur.SHOULD).add(
new TermQuery(new Term("author", "C++")), BooleanClause.Occur.SHOULD).build();
//控制每次返回幾組
int groupLimit = 2;
//控制每一頁的組內文檔數
int groupDocsLimit = 2;
//控制組的偏移
int groupOffset = 0;
//為了排除干擾因素,全部使用默認的排序方式,當然你還可以使用自己喜歡的排序方式
//初始值為命中的所有文檔數,即最壞情況下,一個文檔分成一組,那么文檔數就是分組的總數
int totalGroupCount = indexSearcher.count(query);
TopGroups<BytesRef> topGroups;
System.out.println(
"#### 組的分頁大小為:" + groupLimit);
System.out.println(
"#### 組內分頁大小為:" + groupDocsLimit);
while (groupOffset < totalGroupCount) {//說明還有不同的分組
//控制組內偏移,每次開始遍歷一個新的分組時候,需要將其歸零
int groupDocsOffset = 0;
System.out.println(
"#### 開始組的分頁");
topGroups = groupingDemo.group(indexSearcher, query,
"author", groupDocsOffset, groupDocsLimit, groupOffset, groupLimit);
//具體搜了一次之后,就知道到底有多少組了,更新totalGroupCount為正確的值
totalGroupCount = topGroups.totalGroupCount;
GroupDocs<BytesRef>[] groups = topGroups.groups;
//開始對組進行遍歷
for (int i = 0; i < groups.length; i++) {
long totalHits = iterGroupDocs(indexSearcher, groups[i]);//獲得這個組內一共多少doc
//處理完一次分頁,groupDocsOffset要更新
groupDocsOffset += groupDocsLimit;
//如果組內還有數據,即模擬組內分頁的情況,那么應該繼續遍歷組內剩下的doc
while (groupDocsOffset < totalHits) {
topGroups = groupingDemo.group(indexSearcher, query,
"author", groupDocsOffset, groupDocsLimit, groupOffset, groupLimit);
//這里面的組一定要和外層for循環正在處理的組保持一致,其實這里面浪費了搜索數據,為什么?
//因為Lucene是對多個組同時進行組內向后翻頁,而我只是一個組一個組的處理,其它不處理的組相當於是浪費的
//所以從這種角度來說,設置groupLimit為1比較合理,即每次處理一個組,而每次只將一個組的組內文檔向后翻頁
GroupDocs<BytesRef> group = topGroups.groups[i];
totalHits = iterGroupDocs(indexSearcher, group);
//此時需要更新組內偏移量
groupDocsOffset += groupDocsLimit;
}
//至此,一個組內的doc全部遍歷完畢,開始下一組
groupDocsOffset =
0;
}
groupOffset += groupLimit;
System.out.println(
"#### 結束組的分頁");
}
}
private static long iterGroupDocs(IndexSearcher indexSearcher, GroupDocs<BytesRef> groupDocs) throws IOException {
long totalHits = groupDocs.totalHits;
System.out.println(
"\t#### 開始組內分頁");
System.out.println(
"\t分組名稱:" + groupDocs.groupValue.utf8ToString());
ScoreDoc[] scoreDocs = groupDocs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs) {
System.out.println(
"\t\t組內記錄:" + indexSearcher.doc(scoreDoc.doc));
}
System.out.println(
"\t#### 結束組內分頁");
return totalHits;
}
public TopGroups<BytesRef> group(IndexSearcher indexSearcher, Query query, String groupField,
int groupDocsOffset, int groupDocsLimit, int groupOffset, int groupLimit) throws Exception {
return group(indexSearcher, query, Sort.RELEVANCE, Sort.RELEVANCE, groupField, groupDocsOffset, groupDocsLimit, groupOffset, groupLimit);
}
public TopGroups<BytesRef> group(IndexSearcher indexSearcher, Query query, Sort groupSort, Sort withinGroupSort, String groupField,
int groupDocsOffset, int groupDocsLimit, int groupOffset, int groupLimit) throws Exception {
//實例化GroupingSearch實例,傳入分組域
GroupingSearch groupingSearch =
new GroupingSearch(groupField);
//設置組間排序方式
groupingSearch.setGroupSort(groupSort);
//設置組內排序方式
groupingSearch.setSortWithinGroup(withinGroupSort);
//是否要填充每個返回的group和groups docs的排序field
groupingSearch.setFillSortFields(
true);
//設置用來緩存第二階段搜索的最大內存,單位MB,第二個參數表示是否緩存評分
groupingSearch.setCachingInMB(
64.0, true);
//是否計算符合查詢條件的所有組
groupingSearch.setAllGroups(
true);
groupingSearch.setAllGroupHeads(
true);
//設置一個分組內的上限
groupingSearch.setGroupDocsLimit(groupDocsLimit);
//設置一個分組內的偏移
groupingSearch.setGroupDocsOffset(groupDocsOffset);
TopGroups<BytesRef> result = groupingSearch.search(indexSearcher, query, groupOffset, groupLimit);
return result;
}
}
|
例如組的分頁大小是2,組內分頁大小是2,結果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
#### 組的分頁大小為:2
#### 組內分頁大小為:2
#### 開始組的分頁
#### 開始組內分頁
分組名稱:C
組內記錄:Document<stored<ID:8> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<author:C> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<content:一周精通C>>
組內記錄:Document<stored<ID:9> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<author:C> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<content:C語言詳解>>
#### 結束組內分頁
#### 開始組內分頁
分組名稱:C
組內記錄:Document<stored<ID:10> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<author:C> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<content:C語言調優>>
#### 結束組內分頁
#### 開始組內分頁
分組名稱:C++
組內記錄:Document<stored<ID:11> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<author:C++> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<content:一周精通C++>>
組內記錄:Document<stored<ID:12> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<author:C++> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<content:C++語言詳解>>
#### 結束組內分頁
#### 開始組內分頁
分組名稱:C++
組內記錄:Document<stored<ID:13> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<author:C++> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<content:C++語言調優>>
#### 結束組內分頁
#### 結束組的分頁
#### 開始組的分頁
#### 開始組內分頁
分組名稱:Java
組內記錄:Document<stored<ID:5> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<author:Java> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<content:一周精通Spring Cloud>>
組內記錄:Document<stored<ID:6> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<author:Java> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<content:一周精通Hibernate>>
#### 結束組內分頁
#### 開始組內分頁
分組名稱:Java
組內記錄:Document<stored<ID:2> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<author:Java> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<content:一周精通MyBatis>>
組內記錄:Document<stored<ID:3> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<author:Java> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<content:一周精通Struts>>
#### 結束組內分頁
#### 開始組內分頁
分組名稱:Java
組內記錄:Document<stored<ID:4> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<author:Java> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<content:一周精通Spring>>
組內記錄:Document<stored<ID:1> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<author:Java> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<content:一周精通Java>>
#### 結束組內分頁
#### 開始組內分頁
分組名稱:Java
組內記錄:Document<stored<ID:7> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<author:Java> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<content:一周精通JVM>>
#### 結束組內分頁
#### 結束組的分頁
|
例如組的分頁大小是1,組內分頁大小是3,結果如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
#### 組的分頁大小為:1
#### 組內分頁大小為:3
#### 開始組的分頁
#### 開始組內分頁
分組名稱:C
組內記錄:Document<stored<ID:8> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<author:C> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<content:一周精通C>>
組內記錄:Document<stored<ID:9> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<author:C> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<content:C語言詳解>>
組內記錄:Document<stored<ID:10> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<author:C> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<content:C語言調優>>
#### 結束組內分頁
#### 結束組的分頁
#### 開始組的分頁
#### 開始組內分頁
分組名稱:C++
組內記錄:Document<stored<ID:11> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<author:C++> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<content:一周精通C++>>
組內記錄:Document<stored<ID:12> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<author:C++> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<content:C++語言詳解>>
組內記錄:Document<stored<ID:13> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<author:C++> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<content:C++語言調優>>
#### 結束組內分頁
#### 結束組的分頁
#### 開始組的分頁
#### 開始組內分頁
分組名稱:Java
組內記錄:Document<stored<ID:5> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<author:Java> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<content:一周精通Spring Cloud>>
組內記錄:Document<stored<ID:6> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<author:Java> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<content:一周精通Hibernate>>
組內記錄:Document<stored<ID:2> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<author:Java> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<content:一周精通MyBatis>>
#### 結束組內分頁
#### 開始組內分頁
分組名稱:Java
組內記錄:Document<stored<ID:3> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<author:Java> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<content:一周精通Struts>>
組內記錄:Document<stored<ID:4> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<author:Java> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<content:一周精通Spring>>
組內記錄:Document<stored<ID:1> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<author:Java> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<content:一周精通Java>>
#### 結束組內分頁
#### 開始組內分頁
分組名稱:Java
組內記錄:Document<stored<ID:7> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<author:Java> stored,indexed,tokenized,omitNorms,indexOptions=DOCS<content:一周精通JVM>>
#### 結束組內分頁
#### 結束組的分頁
|