樓主在上篇文章中液斜,提出了將詞和字分開霜大,用不同的分詞器分別構(gòu)建索引癞谒,來解決match_phrase在中文中的短語或者句子匹配問題望蜡。詳細(xì)的內(nèi)容請看上一篇文章:
ES中文分詞器之精確短語匹配(解決了match_phrase匹配不全的問題)
為什么要自己寫分詞器绒窑?
樓主想要一種分詞器棕孙,分詞器完全按照詞典分詞,只要是詞典有的詞語些膨,分詞器就一定要分出來蟀俊。測試了兩個分詞器比如說IK,MMseg订雾,都不能按照樓主的要求分詞肢预。
MMSeg有考慮到詞頻,即使使用mmseg_max_word洼哎,也不能完全按照詞典分詞烫映。
IK理論上是按照詞典分詞的沼本,但是經(jīng)測試,還是發(fā)現(xiàn)了些問題锭沟。比如說“一群穆斯林聚在一起”抽兆,單獨用這句話測試,“穆斯林”可以分出族淮,而這句話放入一篇文章中辫红,卻無法分出“穆斯林”。
樓主是用ik和standard對比命中量發(fā)現(xiàn)不一致祝辣,導(dǎo)出不一致數(shù)據(jù)后贴妻,才發(fā)現(xiàn)的這個問題(ik和mmseg都修改了源碼,過濾掉中文之間的特殊符號蝙斜,因此不存在詞語中間有特殊符號standard可以分出名惩,ik分不出而導(dǎo)致的不一致情況)。
沒辦法了孕荠,自己寫一個吧娩鹉。
ES自定義分詞器
由于ES是采用juice依賴注入的方式,所以要實現(xiàn)一個工廠類和Provider類岛琼。
public class TestAnalyzerProvider extends AbstractIndexAnalyzerProvider<InfosecAnalyzer> {
public TestAnalyzerProvider(IndexSettings indexSettings, Environment env, String name, Settings settings) {
super(indexSettings, name, settings);
}
public static AnalyzerProvider<? extends Analyzer> getMaxWord(IndexSettings indexSettings, Environment environment, String s, Settings settings) {
return new TestAnalyzerProvider(indexSettings,environment,s,settings);
}
@Override public InfosecAnalyzer get() {
return new InfosecAnalyzer();
}
}
public class TestTokenizerFactory extends AbstractTokenizerFactory {
public TestTokenizerFactory(IndexSettings indexSettings, Environment env, String name, Settings settings) {
super(indexSettings, name, settings);
}
public static TokenizerFactory getMaxWord(IndexSettings indexSettings, Environment environment, String name, Settings settings) {
return new TestTokenizerFactory(indexSettings,environment,name,settings);
}
@Override
public Tokenizer create() {
return new TestTokenizor();
}
}
接下來寫自己的插件配置類:
public class AnalysisTestPlugin extends Plugin implements AnalysisPlugin {
public static String PLUGIN_NAME = "analysis-test;
@Override
public Map<String, AnalysisModule.AnalysisProvider<TokenizerFactory>> getTokenizers() {
Map<String, AnalysisModule.AnalysisProvider<TokenizerFactory>> extra = new HashMap<>();
extra.put("test_max_word", TestTokenizerFactory::getMaxWord);
return extra;
}
@Override
public Map<String, AnalysisModule.AnalysisProvider<AnalyzerProvider<? extends Analyzer>>> getAnalyzers() {
Map<String, AnalysisModule.AnalysisProvider<AnalyzerProvider<? extends Analyzer>>> extra = new HashMap<>();
extra.put("test_max_word", TestAnalyzerProvider::getMaxWord);
return extra;
}
}
因為我們只需要按照詞典分詞底循,所以這邊只有一種最大分詞模式,test_max_word槐瑞。接下來就是Analyzer 和Tokenizor。
public class TestAnalyzer extends Analyzer {
public TestAnalyzer(){
super();
}
@Override
protected TokenStreamComponents createComponents(String fieldName) {
Tokenizer _TestTokenizer = new TestTokenizor();
return new TokenStreamComponents(_TestTokenizer);
}
}
public class TestTokenizor extends Tokenizer {
//詞元文本屬性
private final CharTermAttribute termAtt;
//詞元位移屬性
private final OffsetAttribute offsetAtt;
//詞元分類屬性(該屬性分類參考o(jì)rg.wltea.analyzer.core.Lexeme中的分類常量)
private final TypeAttribute typeAtt;
//記錄最后一個詞元的結(jié)束位置
private int endPosition;
private TestSegmenter test =null;
public InfosecTokenizor(){
super();
offsetAtt = addAttribute(OffsetAttribute.class);
termAtt = addAttribute(CharTermAttribute.class);
typeAtt = addAttribute(TypeAttribute.class);
test = new TestSegmenter(input);
}
@Override
public boolean incrementToken() throws IOException {
clearAttributes();
Word word = test.getNext();
if(word != null) {
termAtt.copyBuffer(word.getSen(), word.getWordOffset(), word.getLength());
offsetAtt.setOffset(word.getStartOffset(), word.getEndOffset());
typeAtt.setType(word.getType());
return true;
} else {
end();
return false;
}
}
public void reset() throws IOException {
super.reset();
//setReader 自動被調(diào)用, input 自動被設(shè)置阁苞。
test.reset(input);
}
}
自定義分詞器主要操作的是incrementToken方法困檩,每次從TestSegmenter中取出一個詞,如果改詞存在那槽,設(shè)置改詞的token屬性悼沿,返回true,即還有下一個token骚灸。如果改詞不存在糟趾,返回false,標(biāo)志著沒有數(shù)據(jù)了甚牲,結(jié)束分詞义郑。
自定義分詞的詳細(xì)內(nèi)容
由于代碼太多了,這里就不一一貼出丈钙,只介紹下算法思想非驮。
匹配類型
1)不匹配
2)前綴
3)匹配
4)匹配且是前綴
算法思想
先將數(shù)據(jù)分類組裝成句子,然后經(jīng)過句子處理器將句子分為多個word雏赦,存入queue中劫笙,再由increateToken()方法依次取出芙扎。
組裝句子
依次掃描,將同類的數(shù)據(jù)組裝成句子填大。比如說“你好哈233節(jié)日戒洼,快樂!233dad”允华,掃描第一個字符發(fā)現(xiàn)是中文圈浇,則繼續(xù)向下掃描,一直掃描到‘2’例获,發(fā)現(xiàn)‘2’不是中文汉额,則將“你好哈”組成句子交給句子處理器處理,將處理結(jié)果放入queue中榨汤。繼續(xù)掃描蠕搜,遍歷到‘節(jié)’,發(fā)現(xiàn)‘節(jié)’不是數(shù)組收壕,則將“233”組成一個word妓灌,放入queue。繼續(xù)掃描蜜宪,將“節(jié)”虫埂,“日”依次放入句子中,掃描到“圃验,”掉伏,因為要和standard 對比效果,所以我在代碼中過濾了中文間所有的符號澳窑,忽略“斧散,”繼續(xù)掃描,依次將“快”“樂”存入句子摊聋。后面類似處理即可鸡捐。
句子分詞
依次掃描句子,如果相鄰的數(shù)據(jù)可以組裝成一個詞麻裁,則將詞放入queue中箍镜,繼續(xù)遍歷下一個。例如“節(jié)日快樂”煎源,分詞時首先掃描“節(jié)”色迂,在詞典中查詢“節(jié)”,發(fā)現(xiàn)“節(jié)”是一個前綴薪夕,則繼續(xù)掃描“日”脚草,發(fā)現(xiàn)“節(jié)日”是一個詞匹配,且是一個前綴原献,則將“節(jié)日”存入queue中馏慨,繼續(xù)掃描“節(jié)日快”埂淮,發(fā)現(xiàn)“節(jié)日快”是一個前綴,繼續(xù)掃描“節(jié)日快樂”写隶,發(fā)現(xiàn)“節(jié)日快樂”僅是一個詞匹配倔撞,則將“節(jié)日快樂”存入queue中,結(jié)束從“節(jié)”開始的掃描慕趴。接下來按照上述方法從“日”字開始掃描痪蝇。依次處理完整個句子。
詞典
詞典采用樹的結(jié)構(gòu)冕房,比如說“節(jié)日愉快”躏啰,“節(jié)日快樂”和“萬事如意”這三個詞,在詞典中如下表示:
查找時耙册,記錄上一次前綴匹配的DicSegment给僵,在前綴的DicSegment中,直接查找當(dāng)前掃描字符详拙,可以加快匹配速度帝际。
比如說已經(jīng)匹配到了”節(jié)日快“這個前綴,在匹配”節(jié)日快樂“時饶辙,直接在”快“對應(yīng)的DicSegment中查找蹲诀,這樣就不用再次匹配”節(jié)日“兩個字符。
問題
測試的過程中同樣的發(fā)現(xiàn)了一些問題弃揽,比如說:
原文:長白山脈
test分詞:長白 1 長白山 2 長白山脈 3 白山4 山脈5
查找詞語:長白山
test分詞:長白 1 長白山 2 白山 3
通過分詞可以看出在“長白山脈”中查詢不到“長白山”的脯爪。問題在于match_phrase的限制,長白山的分詞順序在原文構(gòu)建索引時的位置不一樣矿微,中間多出了一個“長白山脈”披粟。
解決方案:
不能匹配的原因是,查找詞語在原文中和后面的字組成了詞語冷冗。用最小粒度分詞即可解決。也就是說只用長度為2和3的詞語惑艇。不存在長度為4的詞語蒿辙,所以一個詞長度為3時,在原文中不會和后面的數(shù)據(jù)組成詞滨巴。當(dāng)詞的長度為2時思灌,和后面的一個字匹配,可以組成一個長度為3的詞恭取,按照我們分詞的規(guī)則泰偿,是先分出兩個字的詞,再分出三個字的詞蜈垮,所以耗跛,兩個字的詞是可以匹配的到的裕照。