前言
感謝CSDN這篇文章,原始程式碼基於這裡。
正常對於“abc@google.com”這段文字,搜尋'@'這個符號是搜不出來的。本文主要修改是擴充套件IK分詞器,增加了對諸如"@ -"這種特殊文字的檢索。
當然這個其實並沒有多少實際意義,所以基本也是出於學習的目的。
正文
IK分詞器分析
這裡不深入原理,只涉及到修改時需要考慮的地方。
- 分詞邏輯:
IKAnalyzer
->IKTokenizer
->IKSegmenter
,最終實際是IKSegmenter
中的next()
方法對輸入流進行分詞,獲取下一個分詞(也叫詞元Lexeme)。 IKSegementer
分詞:包含三個子分詞器,實現ISegmenter
介面,依次呼叫,彼此獨立分詞(既一段文字可以被不通分詞器重複分詞)LetterSegmenter
: 識別字母、數字、符號的組合CN_QuantifierSegmenter
: 識別中文量詞分詞,如一條,三艘CJKSegmenter
: IK的核心,中文分詞,透過從字典資料中查詢匹配分詞。不過不是本文的核心了。
- 子分詞器邏輯:
AnalyzeContext
為輸入引數,儲存輸入流資訊,子分詞器解析每個字元,透過字元型別判斷詞元開始和結尾。當一個分詞結束,透過AnalyzeContext
提供的addLexeme
方法增加一個分詞(詞元) IKArbitrator
歧義裁決:其實這裡涉及到一個IK分詞器的smart
引數,表面上,配置smart
為true
時,分詞比較粗,為false
時分詞粒度更細,這個引數影響的就是這個裁決器。當擁有smart
為true
時,多個分詞如果彼此有覆蓋關係,會自動進行合併,如熱反應堆
,原本會被拆分成反應``反應堆``堆``熱反應堆
等多個詞,但是如果配置了smart
,由於熱反應堆
本身包含了其他詞,則最後分詞結果只會有一個熱反應堆
詞元。所以一般來說,建立索引的時候,不配置smart
,而搜尋前分詞的時候,可以配置smart
。有點扯遠了。
修改說明
核心思路:在原有的分詞器中,建立一個資訊的分詞器SpecialCharSegmenter
,當碰到特殊字元時,插入一個新的Lexeme
。
由於原始的幾個類IKAnalyzer
IKTokenizer
IKSegmenter
等都是final的,所以我直接單獨建立了類MyIKAnalyzer
,MyIKTokenizer
,MyIKSegmenter
替代這幾個類。
比較重要的修改點說明如下:
MyCharacterUtil
擴充套件原有CharacterUtil
工具類,增加特殊字元型別值,在解析字元流時,識別該型別
//需要進行索引的特殊字元
public static final int CHAR_SPECIAL = 0X00000010;
//需要進行索引的特殊字元,可以擴充修改
private static final char[] Letter_Special = new char[]{'#', '-', '@', '.'};
static int identifyCharType(char input) {
.......省略非關鍵部分
} else if (isSpecialChar(input)) {
return CHAR_SPECIAL;
}
}
//特殊字元的判斷
static boolean isSpecialChar(char input) {
int index = Arrays.binarySearch(Letter_Special, input);
return index >= 0;
}
MyAnalyzeContext
讀取字元流的工具類,原本呼叫CharacterUtil
,修改為呼叫我們自定義的MyCharacterUtil
。
/**
* 初始化buff指標,處理第一個字元
*/
void initCursor() {
this.cursor = 0;
this.segmentBuff[this.cursor] = CharacterUtil.regularize(this.segmentBuff[this.cursor]);
//修改點
this.charTypes[this.cursor] = MyCharacterUtil.identifyCharType(this.segmentBuff[this.cursor]);
}
/**
* 指標+1 成功返回 true; 指標已經到了buff尾部,不能前進,返回false 並處理當前字元
*/
boolean moveCursor() {
if (this.cursor < this.available - 1) {
this.cursor++;
this.segmentBuff[this.cursor] = CharacterUtil.regularize(this.segmentBuff[this.cursor]);
//修改點
this.charTypes[this.cursor] = MyCharacterUtil.identifyCharType(this.segmentBuff[this.cursor]);
return true;
} else {
return false;
}
}
MyIKSegmenter
loadSegmenters
中,載入自定義的分詞器
// 處理特殊字元的子分詞器
segmenters.add(new SpecialCharSegmenter());
SpecialCharSegmenter
核心是碰到特殊字元,增加一個詞元,邏輯非常簡單。
public void analyze(AnalyzeContext context) {
if (MyCharacterUtil.CHAR_SPECIAL == context.getCurrentCharType()) {
// 遇到特殊字元,輸出詞元
Lexeme newLexeme = new Lexeme(context.getBufferOffset(), context.getCursor(), 1,
Lexeme.TYPE_LETTER);
context.addLexeme(newLexeme);
}
}
CustomLetterSegmenter
擴充套件原有的LetterSegmenter
,之所以要修改,是發現該類會將部分特殊符號,如abc@google.com
識別為一個整體類,而識別的方式是透過字元型別為CHAR_USELESS
。
當我修改了字元解析時,為特殊符號定義了新的型別CHAR_SPECIAL
,所以需要修改processMixLetter
:
//原始
} else if ((CharacterUtil.CHAR_USELESS == context.getCurrentCharType())
&& this.isLetterConnector(context.getCurrentChar())) {
// 記錄下可能的結束位置
this.end = context.getCursor();
} else {
//修改為
} else if ((CharacterUtil.CHAR_USELESS == context.getCurrentCharType()
//可能被修改為了特殊字元型別
|| MyCharacterUtil.CHAR_SPECIAL == context.getCurrentCharType())
&& this.isLetterConnector(context.getCurrentChar())) {
// 記錄下可能的結束位置
this.end = context.getCursor();
} else {
驗證
程式碼上傳在 github https://github.com/mosakashaka/wbdemos/tree/master/lucene-ik-demo
直接執行後透過地址http://localhost:8080/lucene/createIndex
,建立普通索引(不支援符號),索引中自動新增了幾條資料,其中包含了@
符號。
此時透過搜尋介面http://localhost:8080/test/searchText?text=@
檢索,搜尋不到資料。
如果透過http://localhost:8080/lucene/createIndex2
(地址後多了2),則會建立支援符號的索引,此時搜尋@
-
等符號則可以搜尋到。
結語
本身可能意義不大,純粹是學習的目的。
IK原始的程式碼看起來很久了,我看了下ES用的那個IK分詞器的提交記錄,16年以後幾乎也沒有修改。