假如使用Sphinx來做搜索引擎,就一定會遇到分詞問題。對於中文,有兩個選擇,選擇1是使用Sphinx自帶的一元分詞,選擇2是使用CoreSeek或者Sphinx-for-Chinese,這兩個都使用了mmseg來進行分詞。據我了解,CoreSeek在支持細粒度的分詞,而Sphinx-for-Chinese不支持。而公司使用的是Sphinx-for-Chinese,所以就遇到了分詞的粒度問題。
根據產品人員的反饋,有許多這樣的例子。例如搜索西海或者海岸時,搜不到大華西海岸酒店,搜索兵馬俑時,搜不到秦始皇兵馬俑博物館,搜索肯尼亞時搜不到肯尼亞山。這都是因為Sphinx-for-Chinese使用mmseg得到最優結果后,就不在進行細分的結果。拿大華西海岸酒店這個例子來說,詞典里有大華,西海岸,酒店,華西,西海,海岸這些詞,根據mmseg得到的最優分詞結果,分成大華+西海岸+酒店,這個分詞的結果也是正確的,可是搜索西海,海岸就搜不到它的。問過Sphinx-for-Chinese的開發人員后,要想支持更細粒度的分詞,只有修改源碼。
在組長划出一條線,需要在哪一部分代碼后,一個最簡單的想法是,對於mmseg中每一步得到的最優結果,都進行更細粒度的划分。例如上面的例子,對於西海岸進行更細粒度划分后,就可以得到西海和海岸,這樣搜索西海和海岸時,就可以搜索到。於是立馬動手寫,折騰一個上午后,果然可以搜到了,這樣就達到了同城旅游中酒店搜索的效果了。可是搜索華西還是搜不到,而攜程則可以搜到,在攜程里,搜索西也可以搜到它。仔細考慮后,在原來的代碼里只需要很少的修改就可以做到搜索華西是也可以搜到它,搜索效果已經超過了同程旅游。在增加單字索引后,搜索效果和攜程相當接近。
相信許多使用Sphinx-for-Chinese都會遇到類似的問題,也都將用各自的辦法解決這個問題。這里將這一部分代碼開源,也算是對開源事業的一點點貢獻。事實上,需要修改的地方並不是很多。這里我使用的是sphinx-for-chinese-2.2.1-dev-r4311版本,相信其它版本也可以進行類似的修改。需要修改的文件只有一個,那就是sphinx.cpp。
在2244行附近,class CSphTokenizer_UTF8Chinese : public CSphTokenizer_UTF8_Base這個類中,增加以下數據成員
int m_totalParsedWordsNum; //總共得到的分詞結果 int m_processedParsedWordsNum; //已經處理的分詞個數 int m_isIndexer; //標示是否是indexer程序 bool m_needMoreParser; //標示是否需要更細粒度分詞 const char * m_pTempCur; //標示在m_BestWord中的位置 char m_BestWord[3 * SPH_MAX_WORD_LEN + 3]; //記錄使用mmseg得到的最優分詞結果 int m_iBestWordLength; //最優分詞結果的長度
在6404行附近CSphTokenizer_UTF8Chinese::CSphTokenizer_UTF8Chinese ()這個構造函數中,增加以下語句進行初始化。
char *penv = getenv("IS_INDEXER"); if (penv != NULL) { m_isIndexer = 1; } else { m_isIndexer = 0; } m_needMoreParser = false;
在6706行附近BYTE * CSphTokenizer_UTF8Chinese::GetToken ()函數中int iNum;語句后面增加如下語句
if(m_isIndexer && m_needMoreParser) { //對最優結果進行進一步細分 while (m_pTempCur < m_BestWord + m_iBestWordLength) { if(m_processedParsedWordsNum == m_totalParsedWordsNum) { size_t minWordLength = m_pResultPair[0].length; for(int i = 1; i < m_totalParsedWordsNum; i++) { if(m_pResultPair[i].length < minWordLength) { minWordLength = m_pResultPair[i].length; } } m_pTempCur += minWordLength; m_pText=(Darts::DoubleArray::key_type *)(m_pCur + (m_pTempCur - m_BestWord)); iNum = m_tDa.commonPrefixSearch(m_pText, m_pResultPair, 256, m_pBufferMax-(m_pCur+(m_pTempCur-m_Best Word))); m_totalParsedWordsNum = iNum; m_processedParsedWordsNum = 0; } else { iWordLength = m_pResultPair[m_processedParsedWordsNum].length; m_processedParsedWordsNum++; if (m_pTempCur == m_BestWord && iWordLength == m_iBestWordLength) { //是最優分詞結果,跳過 continue; } memcpy(m_sAccum, m_pText, iWordLength); m_sAccum[iWordLength]='\0'; m_pTokenStart = m_pCur + (m_pTempCur - m_BestWord); m_pTokenEnd = m_pCur + (m_pTempCur - m_BestWord) + iWordLength; return m_sAccum; } } m_pCur += m_iBestWordLength; m_needMoreParser = false; iWordLength = 0; }
在 iNum = m_tDa.commonPrefixSearch(m_pText, m_pResultPair, 256, m_pBufferMax-m_pCur);語句后面,增加如下語句
if(m_isIndexer && iNum > 1) { m_iBestWordLength=getBestWordLength(m_pText, m_pBufferMax-m_pCur); //使用mmseg得到最優分詞結果 memcpy(m_sAccum, m_pText, m_iBestWordLength); m_sAccum[m_iBestWordLength]='\0'; m_pTokenStart = m_pCur; m_pTokenEnd = m_pCur + m_iBestWordLength; m_totalParsedWordsNum = iNum; m_needMoreParser = true; m_processedParsedWordsNum = 0; memcpy(m_BestWord, m_pText, m_iBestWordLength); m_BestWord[m_iBestWordLength]='\0'; m_pTempCur = m_BestWord; return m_sAccum; }
需要修改的地方就這么多。重新編譯,生成后indexer后,設置環境變量,export IS_INDEXER=1,重建索引即可。這里需要注意的一點是,必須使用修改代碼之前的searchd,這樣才會符合我們的需求,如果使用修改代碼之后的searchd,搜索西海時,會分成西海,西,海,然后去搜索,這就不是我們想要的。
對於代碼,有幾個關鍵的地方需要分明的。
1.GetToken函數
這個行數每次返回一個詞,也就是分詞的結果,返回前,需要設置m_pTokenStart和m_pTokenEnd,標示這個詞在內容中的開始位置和結束位置。當返回值為NULL時,標示分詞結束
2.m_pCur
這個用來標示當前的指針在內容的偏移位置,前面說到的設置m_pTokenStart和m_pTokenEnd就需要用到這個值
3.commonPrefixSearch函數
調用這個函數會返回所有共同前綴的詞,結果保存在m_pResultPair中。例如m_pText當前位置是西,則會返回西,西海,西海岸這三個有共同前綴的詞。
4.getBestWordLength函數
這個函數使用mmseg算法,得到下次分詞最優結果的長度。例如m_pText當前位置是西,最優分詞結果是西海岸,而在utf-8中,一個字為三個字節,所以函數返回8。
因為代碼簡單,所以就不細說了。這個修改,唯一不足的是,無法做到精確匹配。也是說,假設兩個地點,一個是北京,一個是北京大學,搜索北京時,無法保證北京是排在第一個,即使它和搜索詞精確匹配。這是因為在對北京進行更細粒度分詞時,將北京分成北京,北,京這個三個詞,這樣破壞了Sphinx用來判斷精確匹配的一些設置。為了糾正這個錯誤,組長和我又寫了一些代碼,這部分新增的代碼就沒有上面那部分好理解了,同時寫的也有一些別扭。
文章來自:
http://program.dengshilong.org/2014/06/28/Sphinx-for-Chinese%E7%9A%84%E5%88%86%E8%AF%8D%E7%BB%86%E7%B2%92%E5%BA%A6%E9%97%AE%E9%A2%98/