倒排索引及布爾查詢的處理算法

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)








發佈了40 篇原創文章 · 獲贊 10 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章