高效能搜尋引擎sphinx原始碼解析之搜尋過程和評分公式
技術交流qq群: 659201069
sphinx搜尋(select)邏輯
用輸入的查詢詞在索引檔案中挨個進行比較,找到滿足關係的文件的過程,並讀出文件,給每個檔案打分,最後打分完成後進行排序,隨後獲取到排序後的文件列表的過程。
sphinx搜尋過程包括以下步驟
1) 搜收使用者輸入,並存儲,儲存格式CsphString,字串形式,例如select id,weight() ,list_namefrom LISTING wherematch('金');
2) 解析使用者輸入,解析完成後每條語句以SqlStmt_t格式儲存
下面對SqlStmt_t作出簡要分析
3) SqlStmt_t結構成員CsphQuery,此類儲存著查詢所需的所有資訊
示意圖如下,全部成員見類圖,下圖只給出主成員
4) 根據SqlStmt_t結構中命令動詞,來匹配不同的程式分支,這裡只分析select流程
5) pLocalSorter =sphCreateQueue( tQueueSettings );建立優先順序佇列,用於對結果排序,預設以weight排序,weight越大在佇列中的優先順序越高,按照優先順序依次出隊就完成了排序
6) 得到ram chunk 和disk chunk的指標,建立分詞器
7) 對搜尋字串進行分詞,分詞結束後語法樹邏輯結構如下
8) 建立ranker
9) 通過查詢hash表,獲得分詞和DocID關係結構圖
分詞和DocId的結構圖
獲得每個分詞DocID所佔連續記憶體入口結構圖
在hash表ExtQwordsHash_t中ExtQword_t是Key與入口地址是一對一的關係
獲得DocID過程
a) 由分詞搜尋ram chunk中的hash表,得結構ExtQword_t
b) 結構中儲存著每個分詞DocID的數目
c) 通ExtQword_t可獲得每個分詞DocID記憶體所對應的入口地址
d) 分詞的DocID在記憶體中佔著連續的記憶體空間,這樣就可以從記憶體中直接讀出所有的DocID 即可
e) 過濾掉KILLlist中的DocID
f) search RAM chunk,給每個DocID安裝DocInfo資訊,給每條DocID打分
下面是如何查詢屬性資訊,這裡只是找到這條文件的屬性在RtSegment_t中的指標,用於屬性過濾,以下是docinfo的記憶體儲存結構
struct RtSegment_t : ISphNoncopyable
{
CSphTightVector<CSphRowitem> m_dRows; ///<row data
CSphTightVector<BYTE> m_dStrings; ///< strings storage
CSphTightVector<DWORD> m_dMvas; ///<MVAs storage
}
通過二分查詢,找到這條文件在m_dRows的位置,就可獲得這條檔案檔屬性在m_dStrings和m_dMvas的指標和位移,然後進行屬性過濾
10) 把結果儲存在ISphMatchSorter中,在multiQuery函中完成對Ram chunk 、Disk chunk的搜尋
11) 把結果儲存到SearchHandler_c的成員,m_dResults中,同時釋放ISphMatchSorter結構記憶體,在RunLocalSearches中完成結果集的轉存
搜尋Diskchunk的過程
搜尋Disk chunk過程與記憶體大致相同,多了一步讀檔案,關於索引檔案(spa,spd,…)的處理方法在第三部分講解
權重演算法詳解
權重因子
1)Hits
舉例說明
insert into LISTING(id,rtf_list_name) values(-99, '金龍魚金龍魚特香純正花生油5L'');
insert into LISTING(id,rtf_list_name, rtf_channel) values(-98, '金龍魚金龍魚特香純正花生油5L',’金龍魚大小龍魚');
用如下 selecl id,weight from LISTING where (‘龍魚’);show meta;
此時,
龍魚,hits:6
-99,uMatchHits:2
-98,uMatchHits:4
Fields, 代表每個分詞所對應的field的欄位,索引建立時確定,舉例說明,假設索引欄位編碼如下,sphinx用一個32無符號整數對每個索引欄位編碼
insert into LISTING(id,rtf_list_name, rtf_standard_channel) values(-98, ‘金龍魚特香純正花生油5L’, ‘金龍魚特香純正花生油5L’)
這條文件的m_uDocFields為6,對應分詞,龍魚
pDoc->m_uDocFields
tDoc.m_uDocFields =m_pQword->m_dQwordFields.GetMask32() & m_dQueriedFields.GetMask32();
m_iWeight = m_iWeight + uRank*SPH_BM25_SCALE其中,m_iWeight是BM25演算法得到,uRank相似度
1、使用者可以為每個field指定weight,格式optionfield_weights=(rtf_list_name=10),預設為1,這樣可以加大或減小每個field所佔的權重比例
2、每個分詞的IDF計算方法, IDF是指在整個文件集中的反向文件頻率。常見詞(如“the” or “to”等)的IDF值小,罕見詞的IDF值大,當一個關鍵詞只在一個文件中出現時,達到峰值IDF=1,
而當關鍵詞在每個索引文件都出現時,IDF=-1
float fLogTotal = logf (float ( 1+iTotalClamped ) );
fIDF = logf (float (iTotalClamped-iTermDocs+1 ) / float ( iTermDocs ) ) /( 2*fLogTotal );
fIDF /= iQwords;
引數說明:
1) iTotalClamped索引中總的DOcID數目,就是select count(*) 的輸出結果
2) iTermDocs每個分詞對應的DocID數目,對金,為docs[0],對 龍魚,為docs[1]
3) iQwords,一個查詢語句中,分詞的個數,對下面的例子是2
舉例:
select id,weight() ,list_name from LISTING where match('金龍魚') limit 44;show meta;
輸出如下:
每條文件的uRank,,相似度計算方法:
struct ExtDoc_t
{
SphDocID_t m_uDocid;
CSphRowitem * m_pDocinfo; ///< for inline storage only
SphOffset_t m_uHitlistOffset;
DWORD m_uDocFields;
float m_fTFIDF;
};
for ( inti=0; i<iWeights; i++ )
if( pDoc->m_uDocFields & (1<<i) )
{
uRank += m_pWeights[i];
}
引數說明:
1) iWeights總的field數目
2) m_pWeights[i]使用者給每個field指定的權重值,預設是1
3) pDoc->m_uDocFields
tDoc.m_uDocFields =m_pQword->m_dQwordFields.GetMask32() & m_dQueriedFields.GetMask32();
dQueriedFields,查詢時指定的欄位,例如match(‘@rtf_list_name 金龍魚’),值是4,如果沒有指定field欄位32位全是1
BM25引數,tDoc.m_fTFIDF每個分詞對應多個文件,每個文件對應一個此引數
tDoc.m_fTFIDF= float(m_pQword->m_uMatchHits)/ float(m_pQword->m_uMatchHits+SPH_BM25_K1) * m_fIDF;
引數說明:
m_pQword->m_uMatchHits每個文件對應hit數目
SPH_BM25_K1=1.2f
m_fIDF為上面計算出的值
m_iWeight,為臨時變數,m_iWeight = (int)((m_fTFIDF+0.5f)*SPH_BM25_SCALE )
引數說明:SPH_BM25_SCALE=1000
最終權重
m_iWeight = m_iWeight + uRank*SPH_BM25_SCALE
uRank最小為1,由此可能權重一定大於1000
多關鍵詞處理方法
AND:將兩個孩子節點獲取到的doc合併起來,過濾掉docId不相同的,權重相加,程式碼如下
tDoc.m_uDocFields = pCur0->m_uDocFields| pCur1->m_uDocFields;
tDoc.m_fTFIDF = pCur0->m_fTFIDF + pCur1->m_fTFIDF;
OR:依次取前兩個結點,如果DocID相同,權重相加,程式碼如下
m_dDocs[iDoc].m_uDocFields =pCur0->m_uDocFields | pCur1->m_uDocFields;
m_dDocs[iDoc].m_fTFIDF = pCur0->m_fTFIDF+ pCur1->m_fTFIDF;