樓主在上篇文章中,提出了將詞和字分開,用不同的分詞器分別構(gòu)建索引,來解決match_phrase在中文中的短語或者句子匹配問題。詳細(xì)的內(nèi)容請(qǐng)看上一篇文章:
ES中文分詞器之精確短語匹配(解決了match_phrase匹配不全的問題)
為什么要自己寫分詞器?
樓主想要一種分詞器,分詞器完全按照詞典分詞,只要是詞典有的詞語,分詞器就一定要分出來。測(cè)試了兩個(gè)分詞器比如說IK,MMseg,都不能按照樓主的要求分詞。
MMSeg有考慮到詞頻,即使使用mmseg_max_word,也不能完全按照詞典分詞。
IK理論上是按照詞典分詞的,但是經(jīng)測(cè)試,還是發(fā)現(xiàn)了些問題。比如說“一群穆斯林聚在一起”,單獨(dú)用這句話測(cè)試,“穆斯林”可以分出,而這句話放入一篇文章中,卻無法分出“穆斯林”。
樓主是用ik和standard對(duì)比命中量發(fā)現(xiàn)不一致,導(dǎo)出不一致數(shù)據(jù)后,才發(fā)現(xiàn)的這個(gè)問題(ik和mmseg都修改了源碼,過濾掉中文之間的特殊符號(hào),因此不存在詞語中間有特殊符號(hào)standard可以分出,ik分不出而導(dǎo)致的不一致情況)。
沒辦法了,自己寫一個(gè)吧。
ES自定義分詞器
由于ES是采用juice依賴注入的方式,所以要實(shí)現(xiàn)一個(gè)工廠類和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;
}
}
因?yàn)槲覀冎恍枰凑赵~典分詞,所以這邊只有一種最大分詞模式,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;
//記錄最后一個(gè)詞元的結(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 自動(dòng)被調(diào)用, input 自動(dòng)被設(shè)置。
test.reset(input);
}
}
自定義分詞器主要操作的是incrementToken方法,每次從TestSegmenter中取出一個(gè)詞,如果改詞存在,設(shè)置改詞的token屬性,返回true,即還有下一個(gè)token。如果改詞不存在,返回false,標(biāo)志著沒有數(shù)據(jù)了,結(jié)束分詞。
自定義分詞的詳細(xì)內(nèi)容
由于代碼太多了,這里就不一一貼出,只介紹下算法思想。
匹配類型
1)不匹配
2)前綴
3)匹配
4)匹配且是前綴
算法思想
先將數(shù)據(jù)分類組裝成句子,然后經(jīng)過句子處理器將句子分為多個(gè)word,存入queue中,再由increateToken()方法依次取出。
組裝句子
依次掃描,將同類的數(shù)據(jù)組裝成句子。比如說“你好哈233節(jié)日,快樂!233dad”,掃描第一個(gè)字符發(fā)現(xiàn)是中文,則繼續(xù)向下掃描,一直掃描到‘2’,發(fā)現(xiàn)‘2’不是中文,則將“你好哈”組成句子交給句子處理器處理,將處理結(jié)果放入queue中。繼續(xù)掃描,遍歷到‘節(jié)’,發(fā)現(xiàn)‘節(jié)’不是數(shù)組,則將“233”組成一個(gè)word,放入queue。繼續(xù)掃描,將“節(jié)”,“日”依次放入句子中,掃描到“,”,因?yàn)橐蛃tandard 對(duì)比效果,所以我在代碼中過濾了中文間所有的符號(hào),忽略“,”繼續(xù)掃描,依次將“快”“樂”存入句子。后面類似處理即可。
句子分詞
依次掃描句子,如果相鄰的數(shù)據(jù)可以組裝成一個(gè)詞,則將詞放入queue中,繼續(xù)遍歷下一個(gè)。例如“節(jié)日快樂”,分詞時(shí)首先掃描“節(jié)”,在詞典中查詢“節(jié)”,發(fā)現(xiàn)“節(jié)”是一個(gè)前綴,則繼續(xù)掃描“日”,發(fā)現(xiàn)“節(jié)日”是一個(gè)詞匹配,且是一個(gè)前綴,則將“節(jié)日”存入queue中,繼續(xù)掃描“節(jié)日快”,發(fā)現(xiàn)“節(jié)日快”是一個(gè)前綴,繼續(xù)掃描“節(jié)日快樂”,發(fā)現(xiàn)“節(jié)日快樂”僅是一個(gè)詞匹配,則將“節(jié)日快樂”存入queue中,結(jié)束從“節(jié)”開始的掃描。接下來按照上述方法從“日”字開始掃描。依次處理完整個(gè)句子。
詞典
詞典采用樹的結(jié)構(gòu),比如說“節(jié)日愉快”,“節(jié)日快樂”和“萬事如意”這三個(gè)詞,在詞典中如下表示:
查找時(shí),記錄上一次前綴匹配的DicSegment,在前綴的DicSegment中,直接查找當(dāng)前掃描字符,可以加快匹配速度。
比如說已經(jīng)匹配到了”節(jié)日快“這個(gè)前綴,在匹配”節(jié)日快樂“時(shí),直接在”快“對(duì)應(yīng)的DicSegment中查找,這樣就不用再次匹配”節(jié)日“兩個(gè)字符。
問題
測(cè)試的過程中同樣的發(fā)現(xiàn)了一些問題,比如說:
原文:長(zhǎng)白山脈
test分詞:長(zhǎng)白 1 長(zhǎng)白山 2 長(zhǎng)白山脈 3 白山4 山脈5
查找詞語:長(zhǎng)白山
test分詞:長(zhǎng)白 1 長(zhǎng)白山 2 白山 3
通過分詞可以看出在“長(zhǎng)白山脈”中查詢不到“長(zhǎng)白山”的。問題在于match_phrase的限制,長(zhǎng)白山的分詞順序在原文構(gòu)建索引時(shí)的位置不一樣,中間多出了一個(gè)“長(zhǎng)白山脈”。
解決方案:
不能匹配的原因是,查找詞語在原文中和后面的字組成了詞語。用最小粒度分詞即可解決。也就是說只用長(zhǎng)度為2和3的詞語。不存在長(zhǎng)度為4的詞語,所以一個(gè)詞長(zhǎng)度為3時(shí),在原文中不會(huì)和后面的數(shù)據(jù)組成詞。當(dāng)詞的長(zhǎng)度為2時(shí),和后面的一個(gè)字匹配,可以組成一個(gè)長(zhǎng)度為3的詞,按照我們分詞的規(guī)則,是先分出兩個(gè)字的詞,再分出三個(gè)字的詞,所以,兩個(gè)字的詞是可以匹配的到的。