1 詞項-文檔關聯矩陣:
在構建倒排索引之前,一個在大規模文檔集中進行查找的方法是建立詞項-文檔關聯矩陣,行爲每個詞項對應的文檔向量,而列爲每個文檔對應的此項向量。根據布爾檢索式,進行向量間的位運算(與、或、取反)等得到檢索結果。但是這種矩陣在大規模文檔條件下,是十分稀疏的,這樣造成了極大的空間浪費,在詞典空間很大的情況下,每篇文檔如果平均包含1000個詞,有50萬的詞項,即使這個文檔對應的詞項向量有全部的1000個1,那也意味着有499000/500000個0。因此,可以考慮只保存1,這就是倒排索引的基本思想。
2 倒排索引(概括):
建立一個倒排索引大致包括如下四個步驟:1. 蒐集需要建立索引的文檔; 2. 詞條化; 3. 語言學處理; 4. 根據所有詞項建立索引,包括一部詞典和一個倒排記錄表。
3 布爾查詢的簡單處理:
首先是倒排記錄的結構:
public static class DocNodeList {
//指向下一個節點
private DocNodeList next;
//當前node的文檔id值
private int docID;
public DocNodeList() {
super();
}
public DocNodeList(int docID) {
super();
this.docID = docID;
}
public DocNodeList next() {
return next;
}
public DocNodeList getNext() {
return next;
}
public void setNext(DocNodeList next) {
this.next = next;
}
public boolean hasNext() {
return next != null;
}
public int getDocID() {
return docID;
}
public void setDocID(int docID) {
this.docID = docID;
}
//添加操作,設置插入到當前節點的下一個位置
public void add(DocNodeList node) {
DocNodeList temp = this.next();
this.setNext(node);
//重置插入node的next值,這要求在執行add()操作時要先緩存被插入節點的next引用
node.setNext(temp);
}
}
3.1 兩個倒排記錄的簡單合併算法:在詞典中分別定位兩個詞項,得到其倒排記錄進行合併;
/**
* 兩個倒排記錄表的合併算法
* @param p1 第一條倒排索引
* @param p2 第二條倒排索引
* @return
*/
public DocNodeList intersect(DocNodeList p1, DocNodeList p2) {
DocNodeList answer = new DocNodeList(0);
while(p1.hasNext() && p2.hasNext()) {
if(p1.getDocID() == p2.getDocID()) {
DocNodeList temp = p1.next();
answer.add(p1);
p1 = temp;
p2 = p2.next();
} else if(p1.getDocID() < p2.getDocID()) {
p1 = p1.next();
} else {
p2 = p2.next();
}
}
return answer;
}
對於多個and連接查詢,可以進行查詢優化,記錄少的先合併。具體的過程如下:
public DocNodeList Intersect(ArrayList<DocNodeList> postings) {
//這裏選擇ArrayList作爲容器,排序使用comparator,Collections.sort()實現
ArrayList<DocNodeList> terms = sortByIncreasingFrequency(postings);
//取出排序後的第一個
DocNodeList result = terms.get(0);
//terms取出餘下的DocNodeList
terms = rest(terms);
while(!terms.isEmpty() && result.hasNext()) {
//posting()取出terms中第一個DocNodeList返回,利用上文的算法進行合併
result = this.intersect(result, posting(terms));
terms = rest(terms);
}
return result;
}
3.2 基於跳錶的倒排記錄快速合併算法:
簡單的來說就是爲了提高next操作的跨度,提高線性查找的速度,從時空複雜度上來看,增加了一個skip域的空間來提高的是多項式的係數部分。
public DocNodeList IntersectWithSkip(DocNodeList p1, DocNodeList p2) {
DocNodeList answer = new DocNodeList(0);
DocNodeList temp = null;
if(p1.getDocID() == p2.getDocID()) {
temp = p1.next();
answer.add(p1);
p1 = temp;
p2 = p2.next();
} else if(p1.getDocID() < p2.getDocID()) {
if(p1.hasSkip() && p1.getSkip().getDocID() <= p2.getDocID()) {
while(p1.getSkip().getDocID() <= p2.getDocID()) {
p1 = p1.getSkip();
}
} else {
p1 = p1.next();
}
} else {
if(p2.hasNext() && p2.getSkip().getDocID() <= p1.getDocID())
while(p2.getSkip().getDocID() <= p1.getDocID())
p2 = p2.getSkip();
else
p2 = p2.next();
}
return answer;
}
注意:跳錶中一次跳多少,可以用一條倒排記錄節點數的根號,也可以選擇斐波那契數列來確定,具體效率,還要看進行比較的倒排記錄docID的分佈。
3.3 帶位置信息的索引
爲了更有效的處理短語查詢問題,二元詞索引顯示不夠的(二元詞索引需要在單元詞索引上建立,同時還增加了相應的索引部分),所以考慮將倒排索引中不僅僅只存儲docID,還要存儲改term(或者token)在這個doc中的每次出現的位置,並按值排序。那可以在DocNodeList中增加一個數據成員:TreeSet<Integer> positions,具體的排序過程就用java封裝好的算法(分情況使用),不再另寫。
/**
* 基於帶位置信息的倒排索引的鄰近搜索算法
* @param p1
* @param p2
* @param k
* @return
*/
public DocNodeList positional_intersect(DocNodeList p1, DocNodeList p2, int k) {
DocNodeList answer = new DocNodeList(0);
TreeSet<Integer> temp_list = new TreeSet<Integer>();
while(p1.hasNext() && p2.hasNext()) {
if(p1.getDocID() == p2.getDocID()) {
//重置清空temp_list
temp_list.clear();
Iterator<Integer> pos1 = p1.getPositions().iterator();
Iterator<Integer> pos2 = p2.getPositions().iterator();
int pp1 = 0;
int pp2 = 0;
while(pos1.hasNext()) {
pp1 = pos1.next();
if(!pos2.hasNext()) break;
while(pos2.hasNext()) {
pp2 = pos2.next();
if(Math.abs(pp1 - pp2) <= k) {
temp_list.add(pp1);
} else if(pp2 - pp1 > k)//注意這裏的break的條件,因爲內循環式遍歷pos2並且position是升序排列的,所以只有在pp2-pp1>k的時候才能確定pp1不可能和pp2和之後的pos2的值相匹配
break;
}
//在temp_list中包含了
while(!temp_list.isEmpty() && Math.abs(temp_list.first() - pp2) > k) {
temp_list.pollFirst();
}
//將和pp1匹配的temp_list中的pp2放入結果集中
for(Integer i : temp_list)
add(answer, pp1, i);
}
p1 = p1.next();
p2 = p2.next();
} else if(p1.getDocID() < p2.getDocID())
p1 = p1.next();
else
p2 = p2.next();
}
return answer;
}
雖然,這個算法包含了兩層循環但實際上基於已排序的鏈表操作,時間複雜度爲O(n+m)