目錄
1. 倒排索引算法
1.1 什麼是倒排索引?
見其名知其意,有倒排索引,對應肯定,有正向索引。
正向索引(forward index),反向索引(inverted index)更熟悉的名字是倒排索引。
在搜索引擎中每個文件都對應一個文件ID,文件內容被表示爲一系列關鍵詞的集合(實際上在搜索引擎索引庫中,關鍵詞也已經轉換爲關鍵詞ID)。例如“文檔1”經過分詞,提取了20個關鍵詞,每個關鍵詞都會記錄它在文檔中的出現次數和出現位置。
正向索引的結構如下:
“文檔1”的ID > 單詞1:出現次數,出現位置列表;單詞2:出現次數,出現位置列表;…………。
“文檔2”的ID > 此文檔出現的關鍵詞列表。
一般是通過key,去找value。
當用戶在主頁上搜索關鍵詞“華爲手機”時,假設只存在正向索引(forward index),那麼就需要掃描索引庫中的所有文檔,找出所有包含關鍵詞“華爲手機”的文檔,再根據打分模型進行打分,排出名次後呈現給用戶。因爲互聯網上收錄在搜索引擎中的文檔的數目是個天文數字,這樣的索引結構根本無法滿足實時返回排名結果的要求。
所以,搜索引擎會將正向索引重新構建爲倒排索引,即把文件ID對應到關鍵詞的映射轉換爲關鍵詞到文件ID的映射,每個關鍵詞都對應着一系列的文件,這些文件中都出現這個關鍵詞。
倒排索引的結構如下:
“關鍵詞1”:“文檔1”的ID,“文檔2”的ID,…………。
“關鍵詞2”:帶有此關鍵詞的文檔ID列表。
從詞的關鍵字,去找文檔。
1.2. 單詞——文檔矩陣
單詞-文檔矩陣是表達兩者之間所具有的一種包含關係的概念模型,如下圖的每列代表一個文檔,每行代表一個單詞,打對勾的位置代表包含關係。
從縱向即文檔這個維度來看,每列代表文檔包含了哪些單詞,比如文檔1包含了詞彙1和詞彙4,而不包含其它單詞。從橫向即單詞這個維度來看,每行代表了哪些文檔包含了某個單詞。比如對於詞彙1來說,文檔1和文檔4中出現過單詞1,而其它文檔不包含詞彙1。矩陣中其它的行列也可作此種解讀。
搜索引擎的索引其實就是實現“單詞-文檔矩陣”的具體數據結構。可以有不同的方式來實現上述概念模型,比如“倒排索引”、“簽名文件”、“後綴樹”等方式。但是各項實驗數據表明,“倒排索引”是實現單詞到文檔映射關係的最佳實現方式,所以本博文主要介紹“倒排索引”的技術細節。
1.3. 倒排索引基本概念
文檔(Document):一般搜索引擎的處理對象是互聯網網頁,而文檔這個概念要更寬泛些,代表以文本形式存在的存儲對象,相比網頁來說,涵蓋更多種形式,比如Word,PDF,html,XML等不同格式的文件都可以稱之爲文檔。再比如一封郵件,一條短信,一條微博也可以稱之爲文檔。在本書後續內容,很多情況下會使用文檔來表徵文本信息。
文檔集合(Document Collection):由若干文檔構成的集合稱之爲文檔集合。比如海量的互聯網網頁或者說大量的電子郵件都是文檔集合的具體例子。
文檔編號(Document ID):在搜索引擎內部,會將文檔集合內每個文檔賦予一個唯一的內部編號,以此編號來作爲這個文檔的唯一標識,這樣方便內部處理,每個文檔的內部編號即稱之爲“文檔編號”,後文有時會用DocID來便捷地代表文檔編號。
單詞編號(Word ID):與文檔編號類似,搜索引擎內部以唯一的編號來表徵某個單詞,單詞編號可以作爲某個單詞的唯一表徵。
倒排索引(Inverted Index):倒排索引是實現“單詞-文檔矩陣”的一種具體存儲形式,通過倒排索引,可以根據單詞快速獲取包含這個單詞的文檔列表。倒排索引主要由兩個部分組成:“單詞詞典”和“倒排文件”。
單詞詞典(Lexicon):搜索引擎的通常索引單位是單詞,單詞詞典是由文檔集合中出現過的所有單詞構成的字符串集合,單詞詞典內每條索引項記載單詞本身的一些信息以及指向“倒排列表”的指針。
倒排列表(PostingList):倒排列表記載了出現過某個單詞的所有文檔的文檔列表及單詞在該文檔中出現的位置信息,每條記錄稱爲一個倒排項(Posting)。根據倒排列表,即可獲知哪些文檔包含某個單詞。
倒排文件(Inverted File):所有單詞的倒排列表往往順序地存儲在磁盤的某個文件裏,這個文件即被稱之爲倒排文件,倒排文件是存儲倒排索引的物理文件。
關於這些概念之間的關係,通過圖2可以比較清晰的看出來。
1.4 倒排索引簡單實例
倒排索引從邏輯結構和基本思路上來講非常簡單。下面我們通過具體實例來進行說明,使得讀者能夠對倒排索引有一個宏觀而直接的感受。
假設文檔集合包含五個文檔,每個文檔內容如圖3所示,在圖中最左端一欄是每個文檔對應的文檔編號。我們的任務就是對這個文檔集合建立
中文和英文等語言不同,單詞之間沒有明確分隔符號,所以首先要用分詞系統將文檔自動切分成單詞序列。這樣每個文檔就轉換爲由單詞序列構成的數據流,爲了系統後續處理方便,需要對每個不同的單詞賦予唯一的單詞編號,同時記錄下哪些文檔包含這個單詞,在如此處理結束後,我們可以得到最簡單的倒排索引 如下圖,“單詞ID”一欄記錄了每個單詞的單詞編號,第二欄是對應的單詞,第三欄即每個單詞對應的倒排列表。比如單詞“谷歌”,其單詞編號爲1,倒排列表爲{1,2,3,4,5},說明文檔集合中每個文檔都包含了這個單詞。
之所以說上圖所示倒排索引是最簡單的,是因爲這個索引系統只記載了哪些文檔包含某個單詞,而事實上,索引系統還可以記錄除此之外的更多信息。下圖是一個相對複雜些的倒排索引,與上圖的基本索引系統比,在單詞對應的倒排列表中不僅記錄了文檔編號,還記載了單詞頻率信息(TF),即這個單詞在某個文檔中的出現次數,之所以要記錄這個信息,是因爲詞頻信息在搜索結果排序時,計算查詢和文檔相似度是很重要的一個計算因子,所以將其記錄在倒排列表中,以方便後續排序時進行分值計算。在圖5的例子裏,單詞“創始人”的單詞編號爲7,對應的倒排列表內容爲:(3:1),其中的3代表文檔編號爲3的文檔包含這個單詞,數字1代表詞頻信息,即這個單詞在3號文檔中只出現過1次,其它單詞對應的倒排列表所代表含義與此相同。
實用的倒排索引還可以記載更多的信息,下圖所示索引系統除了記錄文檔編號和單詞頻率信息外,額外記載了兩類信息,即每個單詞對應的“文檔頻率信息”(對下圖的第三欄)以及在倒排列表中記錄單詞在某個文檔出現的位置信息。
“文檔頻率信息”:代表了在文檔集合中有多少個文檔包含某個單詞,之所以要記錄這個信息,其原因與單詞頻率信息一樣,這個信息在搜索結果排序計算中是非常重要的一個因子。而單詞在某個文檔中出現的位置信息並非索引系統一定要記錄的,在實際的索引系統裏可以包含,也可以選擇不包含這個信息,之所以如此,因爲這個信息對於搜索系統來說並非必需的,位置信息只有在支持“短語查詢”的時候才能夠派上用場。
以單詞“拉斯”爲例,其單詞編號爲8,文檔頻率爲2,代表整個文檔集合中有兩個文檔包含這個單詞,對應的倒排列表爲:{(3;1;<4>),(5;1;<4>)},其含義爲在文檔3和文檔5出現過這個單詞,單詞頻率都爲1,單詞“拉斯”在兩個文檔中的出現位置都是4,即文檔中第四個單詞是“拉斯”。
1.5 樹形結構
B樹(或者B+樹)是另外一種高效查找結構,下圖是一個 B樹結構示意圖。B樹與哈希方式查找不同,需要字典項能夠按照大小排序(數字或者字符序),而哈希方式則無須數據滿足此項要求。
B樹形成了層級查找結構,中間節點用於指出一定順序範圍的詞典項目存儲在哪個子樹中,起到根據詞典項比較大小進行導航的作用,最底層的葉子節點存儲單詞的地址信息,根據這個地址就可以提取出單詞字符串。
1.6 總結
單詞ID:記錄每個單詞的單詞編號;
單詞:對應的單詞;
文檔頻率:代表文檔集合中有多少個文檔包含某個單詞
倒排列表:包含單詞ID及其他必要信息
DocId:單詞出現的文檔id
TF:單詞在某個文檔中出現的次數
POS:單詞在文檔中出現的位置
以單詞“加盟”爲例,其單詞編號爲6,文檔頻率爲3,代表整個文檔集合中有三個文檔包含這個單詞,對應的倒排列表爲{(2;1;<4>),(3;1;<7>),(5;1;<5>)},含義是在文檔2,3,5出現過這個單詞,在每個文檔的出現過1次,單詞“加盟”在第一個文檔的POS是4,即文檔的第四個單詞是“加盟”,其他的類似。
這個倒排索引已經是一個非常完備的索引系統,實際搜索系統的索引結構基本如此。
2. 手寫倒排索引
2.1 trie定義:
在計算機科學中,trie又稱前綴樹(Prefix Tree),單詞查找樹或 鍵樹,是一種有序樹,用於保存關聯數組,其中的鍵通常是字符串。與二叉樹不同,鍵不是直接保存在節點中,而是由節點在書中的位置決定。一個節點的所有子孫都有相同的前綴,也就是這個節點對應的字符串,而根節點對應字符串。一般情況下,不是所有的節點都有對應的值,只有葉子節點和部分內部節點所對應的鍵纔有相關的值。
trie中的鍵通常是字符串,但也可以是其它的結構,trie的算法可以很容易的修改爲處理其它結構的有序序列,比如一串數字或者形狀的排列。
2.2 基本性質:
- 跟節點不包含字符,除跟節點以外每個節點只包含要給字符。
- 從跟節點到某一個節點,路徑上經過的字符連接起來,爲該節點對應的字符串。
- 每個節點的所有子節點包含的字符串不相同。
2.3 缺點:
- 當hash函數很好時,Trie樹的查找效率會低於哈希搜索
- 空間消耗比較大。
2.4 例子
2.4.1文檔
《華爲 華爲手機 華爲平板 華爲牛逼 鴻蒙 華爲鴻蒙操作系統》
2.4.2 分詞
--華爲
--華爲手機
--華爲平板
--華爲牛逼
--鴻蒙
--華爲鴻蒙操作系統
2.4.3 分析字典樹圖
(isEnd標記是否爲詞的尾節點 ,碰到節點爲true時,便是一個詞語)
2.4.4 代碼實現:
創建Node
import lombok.Data;
import java.util.LinkedList;
/**
* 字典樹的節點
*/
@Data
public class Node {
private char content;//存在當前節點的字
private boolean isEnd;//是否是詞的結尾
private int count;//這個詞在這個字下面的分支的個數
private LinkedList<Node> childList;//子節點
/**
* 構造方法 初始化節點使用
* @param c
*/
public Node(char c){
childList=new LinkedList<>();
isEnd=false;
content=c;
count=0;
}
/**
* 提供一個遍歷node中的linkedList中是否有這個字。有就意味着可以繼續查找下去,沒有就沒有
* @param c
* @return
*/
public Node subNode(char c){
if(null!=childList&&!childList.isEmpty()){
for (Node node : childList) {
if(node.content==c){
return node;
}
}
}
return null;
}
}
創建TrieTree
/**
* 字典樹
*/
public class TrieTree {
private Node root;//根
public TrieTree(){
root=new Node(' ');//構造一個空的根節點
}
/***
* @Description: 查詢
* @Param: word 要判斷的詞
* @return: 是否存在
*/
public boolean search(String word){ //華爲
Node current=root;//從根節點開始找
if(null!=word){
//轉成字符數組
char[] chars = word.toCharArray();
if(null!=chars&&chars.length>0){
for (char c : chars) {
Node node = current.subNode(c);
if(null==node){//如果返回的子節點爲空 說明不存在
return false;
}else{
current=current.subNode(c);
}
}
//判斷當前節點是否是結束節點
if(current.isEnd()){
return true;
}else{
return false;
}
}else{
return false;
}
}else{
return false;
}
}
/***
* @Description: 插入方法,先判斷是否有這個詞,(通過上面的寫的查詢方法) 如果沒有,。就一個一個按順序判斷裏面的字
* 如果有這個字,繼續判斷下一個,當沒有字個字的時候,對空上字new Node對象,放到上一個字的LindkedList裏面
* @Param: [word] 要插入的分詞
* @return: void
*/
public void insert(String word){ //華爲電腦
//判斷有沒有這個詞 有就直接說這個詞在整個字典數已存在
if(this.search(word)){
return;
}
//如果不存在 ,就從根節點一個一個找
Node current=root;
if(null!=word){
char[] chars = word.toCharArray();
if(null!=chars&&chars.length>0){
for (char c : chars) {
Node child = current.subNode(c);
if(null!=child){
current=child;
}else{
//構造新的
current.getChildList().add(new Node(c));
current=current.subNode(c);
}
current.setCount(current.getCount()+1);//出現次數+1
}
//循環結束之後把最後一個字變成isEnd是true
current.setEnd(true);
}
}
}
/***
* @Description: 刪除分詞
* @Param: [word] 要刪除的分詞
* @return: void
*/
public void deleteWord(String word) {
//查詢一個詞在不在字典樹
if (this.search(word) == false) {
return;
}
Node current = root;
if (null != word) {
char[] chars = word.toCharArray();
if (null != chars && chars.length > 0) {
for (char c : chars) {
Node node = current.subNode(c);
if (node.getCount() == 1) {
current.getChildList().remove(node);
return;
} else {
current.setCount(current.getCount() - 1);
current = node;
}
}
current.setEnd(false);//isend設置爲false代表當前路上的字連起來不是一相詞了
}
}
}
}
測試:
public class TestTrieTree {
public static void main(String[] args) {
String content="華爲-華爲手機-華爲平板-華爲牛逼-鴻蒙-華爲鴻蒙操作系統";
//模擬分詞
String[] split = content.split("-");
//構造字典樹
TrieTree trie = new TrieTree();
//把分詞插入
for (String s : split) {
trie.insert(s);
}
System.out.println(trie.search("華爲"));
System.out.println(trie.search("華爲手"));
trie.deleteWord("華爲");
System.out.println(trie.search("華爲"));
System.out.println(trie.search("華爲手機"));
}
}
結果:
申明:博文內容均來自網絡,僅供學習筆記使用