有關ansj的IndexAnalysis的分詞對elasticsearch的fast vector highlight高亮會產生BUG的問題分析
IndexAnalysis是ansj分詞工具針對搜尋引擎提供的一種分詞方式,會進行最細粒度的分詞,例如下面這句話:
看熱鬧:2014年度足壇主教練收入榜公佈,溫格是真·阿森納代言人啊~
這句話會被拆分成:[看熱鬧/v, :/w, 2014/m, 年度/n, 足壇/n, 主教練/n, 收入/n, 榜/n, 公佈/v, ,/w, 溫格/nr, 是/v, 真/d, ·/w, 阿森納/nr, 代言人/n, 啊/y, ~, , 熱鬧, 主教, 教練]
也就是“看熱鬧”和“主教練”這兩個詞會進一步細分出三個詞:熱鬧, 主教, 教練
這樣分其實並沒有問題,問題就出在這三個詞放置的位置,細分出來的詞被放置到了末尾!原始碼中是這樣寫的:
先遍歷所有的term,新增到result列表中去,然後再對result中的term看能否進一步分詞,能的話,再細分,最終將細分後的結果新增到result的末尾。/** * 檢索的分詞 * * @return */ private List<Term> result() { String temp = null; List<Term> result = new LinkedList<Term>(); int length = graph.terms.length - 1; for (int i = 0; i < length; i++) { if (graph.terms[i] != null) { result.add(graph.terms[i]); } } LinkedList<Term> last = new LinkedList<Term>() ; for (Term term : result) { if (term.getName().length() >= 3) { GetWordsImpl gwi = new GetWordsImpl(term.getName()); while ((temp = gwi.allWords()) != null) { if (temp.length() < term.getName().length() && temp.length()>1) { last.add(new Term(temp, gwi.offe + term.getOffe(), TermNatures.NULL)); } } } } result.addAll(last) ; setRealName(graph, result); return result; }
當我們搜尋“阿森納教練”並且需要進行fast vector highlight時,就會報錯,我們寫個demo來測試一下:
package org.ansj.ansj_lucene4_plug; import org.ansj.lucene4.AnsjAnalysis; import org.ansj.lucene4.AnsjIndexAnalysis; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.TextField; import org.apache.lucene.index.*; import org.apache.lucene.queryparser.classic.QueryParser; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.Query; import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.TopDocs; import org.apache.lucene.search.vectorhighlight.FastVectorHighlighter; import org.apache.lucene.search.vectorhighlight.SimpleFragListBuilder; import org.apache.lucene.store.Directory; import org.apache.lucene.store.RAMDirectory; import org.apache.lucene.util.Version; import org.elasticsearch.search.highlight.vectorhighlight.SourceSimpleFragmentsBuilder; import java.util.HashSet; /** * Created by xiaojun on 2016/1/11. */ public class Test3 { public static void main(String[] args) throws Exception { HashSet<String> hs = new HashSet<String>(); hs.add("的"); Analyzer analyzer = new AnsjIndexAnalysis(hs, false); Directory directory = null; IndexWriter iwriter = null; // String text = "我發現帖子系統有個BUG啊,怎麼解決?怎麼破"; String text = "看熱鬧:2014年度足壇主教練收入榜公佈,溫格是真·阿森納代言人啊~"; // String text = "我去過白楊樹林!"; // UserDefineLibrary.insertWord("阿森納", "n", 1000); // UserDefineLibrary.insertWord("系統", "n", 1000); IndexWriterConfig ic = new IndexWriterConfig(Version.LUCENE_4_10_4, analyzer); directory = new RAMDirectory(); iwriter = new IndexWriter(directory, ic); Document document = new Document(); document.add(new TextField("_id", "1", Field.Store.YES)); document.add(new Field("content", text, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS)); iwriter.addDocument(document); // iwriter.commit(); // iwriter.close(); IndexReader reader = DirectoryReader.open(iwriter, true); IndexSearcher searcher = new IndexSearcher(reader); FastVectorHighlighter highlighter = new FastVectorHighlighter(); TopDocs topDocs = searcher.search(new TermQuery(new Term("_id", "1")), 1); // //// assertThat(topDocs.totalHits, equalTo(1)); // // // String fragment = highlighter.getBestFragment(highlighter.getFieldQuery(new TermQuery(new Term("content", "阿森納"))), // reader, topDocs.scoreDocs[0].doc, "content", 30); //// assertThat(fragment, notNullValue()); //// assertThat(fragment, equalTo("the big <b>bad</b> dog")); // System.out.println(fragment); Analyzer queryAnalyzer = new AnsjAnalysis(hs, false); QueryParser tq = new QueryParser("content", queryAnalyzer); String queryStr = "阿森納教練"; Query query = tq.createBooleanQuery("content", queryStr); System.out.println("query:" + query); TopDocs hits = searcher.search(query, 5); System.out.println(queryStr + ":共找到" + hits.totalHits + "條記錄!"); String fragment2 = highlighter.getBestFragment(highlighter.getFieldQuery(query), reader, topDocs.scoreDocs[0].doc, "content", 30); // assertThat(fragment, notNullValue()); // assertThat(fragment, equalTo("the big <b>bad</b> dog")); System.out.println(fragment2); } }
執行就會報以下錯誤:
Exception in thread "main" java.lang.StringIndexOutOfBoundsException: String index out of range: -16 at java.lang.String.substring(String.java:1911) at org.apache.lucene.search.vectorhighlight.BaseFragmentsBuilder.makeFragment(BaseFragmentsBuilder.java:178) at org.apache.lucene.search.vectorhighlight.BaseFragmentsBuilder.createFragments(BaseFragmentsBuilder.java:144) at org.apache.lucene.search.vectorhighlight.BaseFragmentsBuilder.createFragment(BaseFragmentsBuilder.java:111) at org.apache.lucene.search.vectorhighlight.BaseFragmentsBuilder.createFragment(BaseFragmentsBuilder.java:95) at org.apache.lucene.search.vectorhighlight.FastVectorHighlighter.getBestFragment(FastVectorHighlighter.java:116) at org.ansj.ansj_lucene4_plug.Test3.main(Test3.java:78) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:606) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
找到報錯的方法:
protected String makeFragment( StringBuilder buffer, int[] index, Field[] values, WeightedFragInfo fragInfo,
String[] preTags, String[] postTags, Encoder encoder ){
StringBuilder fragment = new StringBuilder();
final int s = fragInfo.getStartOffset();
int[] modifiedStartOffset = { s };
String src = getFragmentSourceMSO( buffer, index, values, s, fragInfo.getEndOffset(), modifiedStartOffset );
int srcIndex = 0;
for( SubInfo subInfo : fragInfo.getSubInfos() ){
for( Toffs to : subInfo.getTermsOffsets() ){
fragment
.append( encoder.encodeText( src.substring( srcIndex, to.getStartOffset() - modifiedStartOffset[0] ) ) )
.append( getPreTag( preTags, subInfo.getSeqnum() ) )
.append( encoder.encodeText( src.substring( to.getStartOffset() - modifiedStartOffset[0], to.getEndOffset() - modifiedStartOffset[0] ) ) )
.append( getPostTag( postTags, subInfo.getSeqnum() ) );
srcIndex = to.getEndOffset() - modifiedStartOffset[0];
}
}
fragment.append( encoder.encodeText( src.substring( srcIndex ) ) );
return fragment.toString();
}
標高亮的邏輯是這樣的:
剛開始執行的時候srcIndex=0,首先找到第一個命中的詞:“阿森納”,這裡解釋一下為什麼"阿森納"是第一個詞而“教練”不是第一個詞,明明在原句中教練這兩個字出現在阿森納之前?上面我們隊IndexAnalysis的原始碼已經分析過了,”教練“作為”主教練“的細分詞,被放置在了詞向量的末尾。所以阿森納的position會比教練的position要靠前。
從然後第一個高亮詞前面的片段就是從0到這個詞的起始偏移位置,阿森納的起止位置為:[26,29],也就是0~26,然後下一次迴圈的時候srcIndex會變為29,即從29開始再找高亮片段,然後第二個詞"教練"的起止偏移為:[13,16],src.substring(29,13)顯然會導致異常,substring方法引數指定的是擷取字串的起止位置,顯然第二個引數不能小於第一個引數,不然就會報索引越界的異常。
解釋了這麼多,其實只要保證迴圈的時候先找”教練“這個詞,再找”阿森納這個詞“那就沒有問題了,也就是跟fragInfo.getSubInfos()這個list中詞的順序有關,而這個list中詞的順序最終與Term的position有關,具體可以看到FieldTermStack這個類的構造方法:
public FieldTermStack( IndexReader reader, int docId, String fieldName, final FieldQuery fieldQuery ) throws IOException {
this.fieldName = fieldName;
Set<String> termSet = fieldQuery.getTermSet( fieldName );
// just return to make null snippet if un-matched fieldName specified when fieldMatch == true
if( termSet == null ) return;
final Fields vectors = reader.getTermVectors(docId);
if (vectors == null) {
// null snippet
return;
}
final Terms vector = vectors.terms(fieldName);
if (vector == null) {
// null snippet
return;
}
final CharsRefBuilder spare = new CharsRefBuilder();
final TermsEnum termsEnum = vector.iterator(null);
DocsAndPositionsEnum dpEnum = null;
BytesRef text;
int numDocs = reader.maxDoc();
while ((text = termsEnum.next()) != null) {
spare.copyUTF8Bytes(text);
final String term = spare.toString();
if (!termSet.contains(term)) {
continue;
}
dpEnum = termsEnum.docsAndPositions(null, dpEnum);
if (dpEnum == null) {
// null snippet
return;
}
dpEnum.nextDoc();
// For weight look here: http://lucene.apache.org/core/3_6_0/api/core/org/apache/lucene/search/DefaultSimilarity.html
final float weight = ( float ) ( Math.log( numDocs / ( double ) ( reader.docFreq( new Term(fieldName, text) ) + 1 ) ) + 1.0 );
final int freq = dpEnum.freq();
for(int i = 0;i < freq;i++) {
int pos = dpEnum.nextPosition();
if (dpEnum.startOffset() < 0) {
return; // no offsets, null snippet
}
termList.add( new TermInfo( term, dpEnum.startOffset(), dpEnum.endOffset(), pos, weight ) );
}
}
// sort by position
Collections.sort(termList);
// now look for dups at the same position, linking them together
int currentPos = -1;
TermInfo previous = null;
TermInfo first = null;
Iterator<TermInfo> iterator = termList.iterator();
while (iterator.hasNext()) {
TermInfo current = iterator.next();
if (current.position == currentPos) {
assert previous != null;
previous.setNext(current);
previous = current;
iterator.remove();
} else {
if (previous != null) {
previous.setNext(first);
}
previous = first = current;
currentPos = current.position;
}
}
if (previous != null) {
previous.setNext(first);
}
}
其中有一句比較關鍵的程式碼:
// sort by position
Collections.sort(termList);
也就是termList會按position從小到大排序。
說到這裡問題已經闡述的很清楚了,我們最終的目的就是要改變“教練”這個詞出現的位置,也就是讓細分詞直接緊跟著出現在原始詞的後面,問題就能解決了。
於是可以對IndexAnalysis的result()方法進行如下修改:
/**
* 檢索的分詞
*
* @return
*/
private List<Term> result() {
String temp = null;
List<Term> result = new LinkedList<Term>();
int length = graph.terms.length - 1;
for (int i = 0; i < length; i++) {
if (graph.terms[i] != null) {
Term term = graph.terms[i];
result.add(term);
if (term.getName().length() >= 3 && !Arrays.asList(new String[]{"nr","nt","nrf","nnt","nsf","adv","nz"}).contains(term.getNatureStr())) {
GetWordsImpl gwi = new GetWordsImpl(term.getName());
while ((temp = gwi.allWords()) != null) {
if (temp.length() < term.getName().length() && temp.length()>1) {
result.add(new Term(temp, gwi.offe + term.getOffe(), TermNatures.NULL));
}
}
}
}
}
setRealName(graph, result);
return result;
}
即在遍歷每個詞的同時緊接著在判斷該詞是否需要細分,這裡對一些詞性做了一些限制,在原始碼裡緊緊是判斷了term數量是否大於3,我覺得一些專有名詞其實是不需要再細分了,專有名詞細分了反而搜尋質量變差。
進行如上修改之後,再從新執行以下分詞,結果變成如下:
[看熱鬧/v, 熱鬧, :/w, 2014/m, 年度/n, 足壇/n, 主教練/n, 主教, 教練, 收入/n, 榜/n, 公佈/v, ,/w, 溫格/nrf, 是/v, 真/d, ·/w, 阿森納/nz, 代言人/n, 啊/y, ~, ]
可以看到“主教”和“教練”這兩個詞緊跟著“主教練”這個詞後面出現了。
我們再執行上面的高亮demo,已經可以得到如下正確的結果了:
看熱鬧:2014年度足壇主<b>教練</b>收入榜公佈,溫格是真·<b>阿森納</b>代言人啊~
相關推薦
有關ansj的IndexAnalysis的分詞對elasticsearch的fast vector highlight高亮會產生BUG的問題分析
IndexAnalysis是ansj分詞工具針對搜尋引擎提供的一種分詞方式,會進行最細粒度的分詞,例如下面這句話: 看熱鬧:2014年度足壇主教練收入榜公佈,溫格是真·阿森納代言人啊~ 這句話會被拆分成:[看熱鬧/v, :/w, 2014/m, 年度/n, 足壇/n, 主
python中文分詞,使用結巴分詞對python進行分詞
php 分詞 在采集美女站時,需要對關鍵詞進行分詞,最終采用的是python的結巴分詞方法.中文分詞是中文文本處理的一個基礎性工作,結巴分詞利用進行中文分詞。其基本實現原理有三點:基於Trie樹結構實現高效的詞圖掃描,生成句子中漢字所有可能成詞情況所構成的有向無環圖(DAG)采用了動態規劃查找最大概率
分散式搜尋elasticsearch java API 之 highlighting (對搜尋結果的高亮顯示)
搜尋請求的Body如下:: { "query" : {...}, "highlight" : { "fields" : { "title":{}, "intro" : {}
elasticsearch 5.x highlight 高亮
public static Map<String, Object> search(String key,String index,String type,int start,int row
Vue-cli實現Markdown解析為Html以及highlight高亮程式碼塊
marked用來幹什麼的? 一個功能齊全的markdown**解析器**和**編譯器**,用JavaScript編寫。速度建成。marked該怎麼使用? **安裝** npm install
Sublime3 中對 matlab 檔案語法高亮(Highlighting)
Sublime2 中的設定請看這位的介紹https://blog.csdn.net/yangyangyang20092010/article/details/49780237在Sublime 3 中開啟packages資料夾(Windows在安裝目錄,Mac在應用程式中右鍵s
Lucene.net(4.8.0) 學習問題記錄五: JIEba分詞和Lucene的結合,以及對分詞器的思考
+= d+ ext eth reac chart rdl ret start 前言:目前自己在做使用Lucene.net和PanGu分詞實現全文檢索的工作,不過自己是把別人做好的項目進行遷移。因為項目整體要遷移到ASP.NET Core 2.0版本,而Lucene使用的版本
利用java實現對文字的去除停用詞以及分詞處理
功能: 對txt文件進行分詞處理,並去除停用詞。 工具: IDEA,java,hankcs.hanlp.seg.common.Term等庫。 程式: import java.util.*; import java.io.*; import java.lang.String; imp
python3-對某目錄下的文字檔案分詞
from pathlib import Path import os import re pathName='./' fnLst=list(filter(lambda x:not x.is_dir(),Path(pathName).glob('**/*.txt'))) print(fnLst) for fn
python3-對某目錄下的文本文件分詞
dynamic rom help any end eal txt orm script from pathlib import Path import os import re pathName=‘./‘ fnLst=list(filter(lambda x:not x.i
【java HanNLP】HanNLP 利用java實現對文字的去除停用詞以及分詞處理
HanNLP 功能很強大,利用它去停用詞,加入使用者自定義詞庫,中文分詞等,計算分詞後去重的個數、 maven pom.xml 匯入 <dependency> <groupId>com.hankcs</g
使用結巴分詞(jieba)對自然語言進行特徵預處理(Python、Java 實現)
一、前言 之前使用基於 Python 語言的 Spark 進行機器學習,程式設計起來是十分簡單。 ① 但是演算法部署到雲伺服器上,是一個障礙。 ② 得藉助 Flask/Django 等 Python W
資料處理-------利用jieba對資料集進行分詞和統計頻數
一,對txt檔案中出現的詞語的頻數統計再找出出現頻率多的 二,程式碼: import re from collections import Counter import jieba def cut_word(datapath): with open(
ES 對各欄位建立分詞 和mapping建立 個人操作記錄
最近在搞es的查詢和,需要使用到模糊查詢 匹配 在之前使用的時候,java 中的String 在 es 預設建立的mapping type是 String 是可以模糊查詢的 ,但是新版的ES 廢棄了 string 變為 text 和 keyword 這樣一來 不管是 tr
使用python中的結巴分詞作詞雲圖,對微信功能點進行輔助分析
工作室任務:基於知乎評論,分析微信功能點,做一次分享會。 一、原料和準備 1.從網上爬蟲的文件,儲存為txt文件,本例來源https://www.zhihu.com/question/23178234?from=groupmessage&isappinstalled
Elasticsearch——分詞器對String的作用
關於String型別——分詞與不分詞 在Elasticsearch中String是最基本的資料型別,如果不是數字或者標準格式的日期等這種很明顯的型別,其他的一般都會優先預設儲存成String。同樣的資料型別,Elasticsearch也提供了多種儲存與分詞的模式,不同的模式應用於不同的場景。 很多人在初次使
ElasticSearch:為中文分詞器增加對英文的支援(讓中文分詞器可以處理中英文混合文件)
本文地址,需轉載請註明出處: 當我們使用中文分詞器的時候,其實也希望它能夠支援對於英文的分詞。試想,任何一個儲存文字的欄位都有可能是中英文夾雜的。 我們的專案中使用IKAnalyzer作為中文分詞器,它在處理文件過程中遇到英文時,利用空格和標點將英文單詞取出來,同時也
【自然語言處理入門】01:利用jieba對資料集進行分詞,並統計詞頻
一、基本要求 使用jieba對垃圾簡訊資料集進行分詞,然後統計其中的單詞出現的個數,找到出現頻次最高的top100個詞。 二、完整程式碼 # -*- coding: UTF-8 -*- fr
spark + ansj 對大資料量中文進行分詞
目前的分詞器大部分都是單機伺服器進行分詞,或者使用hadoop mapreduce對儲存在hdfs中大量的資料文字進行分詞。由於mapreduce的速度較慢,相對spark來說程式碼書寫較繁瑣。本文使用spark + ansj對儲存在hdfs中的中文文字
對Python中文分詞模組結巴分詞演算法過程的理解和分析
結巴分詞是國內程式設計師用python開發的一箇中文分詞模組, 原始碼已託管在github, 地址在: https://github.com/fxsjy/jieba 作者的文件寫的不是很全, 只寫了怎麼用, 有一些細節的文件沒有寫. 以下是作者說明檔案中提到的結巴分