1. 程式人生 > >有關ansj的IndexAnalysis的分詞對elasticsearch的fast vector highlight高亮會產生BUG的問題分析

有關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, ~, , 熱鬧, 主教, 教練]

也就是“看熱鬧”和“主教練”這兩個詞會進一步細分出三個詞:熱鬧, 主教, 教練

這樣分其實並沒有問題,問題就出在這三個詞放置的位置,細分出來的詞被放置到了末尾!原始碼中是這樣寫的:

/**
			 * 檢索的分詞
			 * 
			 * @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;
			}
先遍歷所有的term,新增到result列表中去,然後再對result中的term看能否進一步分詞,能的話,再細分,最終將細分後的結果新增到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 作者的文件寫的不是很全, 只寫了怎麼用, 有一些細節的文件沒有寫. 以下是作者說明檔案中提到的結巴分