1. 程式人生 > >Lucene.net(4.8.0) 學習問題記錄五: JIEba分詞和Lucene的結合,以及對分詞器的思考

Lucene.net(4.8.0) 學習問題記錄五: JIEba分詞和Lucene的結合,以及對分詞器的思考

+= d+ ext eth reac chart rdl ret start

前言:目前自己在做使用Lucene.net和PanGu分詞實現全文檢索的工作,不過自己是把別人做好的項目進行遷移。因為項目整體要遷移到ASP.NET Core 2.0版本,而Lucene使用的版本是3.6.0 ,PanGu分詞也是對應Lucene3.6.0版本的。不過好在Lucene.net 已經有了Core 2.0版本(4.8.0 bate版),而PanGu分詞,目前有人正在做,貌似已經做完,只是還沒有測試~,Lucene升級的改變我都會加粗表示。

Lucene.net 4.8.0

https://github.com/apache/lucenenet

PanGu分詞

https://github.com/LonghronShen/Lucene.Net.Analysis.PanGu/tree/netcore2.0

Lucene.net 4.8.0 和之前的Lucene.net 3.6.0 改動還是相當多的,這裏對自己開發過程遇到的問題,做一個記錄吧,希望可以幫到和我一樣需要升級Lucene.net的人。我也是第一次接觸Lucene ,也希望可以幫助初學Lucene的同學。

目錄

  • Lucene.net(4.8.0) 學習問題記錄一:分詞器Analyzer的構造和內部成員ReuseStategy
  • Lucene.net(4.8.0) 學習問題記錄二: 分詞器Analyzer中的TokenStream和AttributeSource
  • Lucene.net(4.8.0) 學習問題記錄三: 索引的創建 IndexWriter 和索引速度的優化
  • Lucene.net(4.8.0) 學習問題記錄四: IndexWriter 索引的優化以及思考

一,PanGu分詞與JIEba分詞

1.中文分詞工具

Lucene的自帶分詞工具對中文分詞的效果很是不好。因此在做中文的搜索引擎的時候,我們需要用額外的中文分詞組件。這裏可以總結一下中文分詞工具有哪些,在下面這個銜接中,有對很多中文分詞工具的性能測試:

https://github.com/ysc/cws_evaluation

可惜我們看不到PanGu分詞的性能,在PanGu分詞的官網我們可以看到:Core Duo 1.8 GHz 下單線程 分詞速度為 390K 字符每秒,2線程分詞速度為 690K 字符每秒。

在上面的排行榜中屬於中等吧。但由於我做的是基於.net的搜索引擎,所以我只找到了IK分詞器,PanGu分詞器,JIEba分詞器的.net core2.0 版本。

1.1 PanGu分詞 .net core 版

這是PanGu分詞.net core 2.0版本的遷移項目:

https://github.com/LonghronShen/Lucene.Net.Analysis.PanGu/tree/netcore2.0

這是一個沒有遷移完全的項目,在使用過程中遇到了一些問題,前面的目錄中記錄過。我修改了一些bug,下面的是修改過後的可以直接使用的PanGu分詞.net core2.0版本:

https://github.com/SilentCC/Lucene.Net.Analysis.PanGu/tree/netcore2.0

我提交了一個Pull Request ,作者還沒有合並。我已經用了一段時間,很穩定。

1.2 JIEba分詞 .net core 版

JIEba分詞的.net core 版本遷移項目:

https://github.com/linezero/jieba.NET

但是這是.net core1.0的版本,拿過來也不能直接給Lucene使用,所以我升級到了2.0並且做了一個接口,讓其支持Lucene,經過測試可以穩定的進行分詞和高亮。當然在其中也遇到了一些問題,在下文中會詳細闡述。這是改過之後的Lucene版:

https://github.com/SilentCC/JIEba-netcore2.0

1.3 IK分詞 .net core 版

在Nuget中可以搜索到(IKNetAnalyzer)

在GitHub中 https://github.com/stanzhai/IKAnalyzer.NET 顯示正在開發中。由於一些原因,我並沒有使用IK分詞。所以也就沒有細看了。

2.PanGu分詞和JIEba分詞的對比

Lucene和PanGu分詞搭配,已經是Lucene.net 的經典搭配,但是PanGu分詞已經很久沒有更新,PanGu分詞的字典也是很久以前維護的字典。在網上可以找到很多Lucene和PanGu分詞搭配的例子。在PanGu分詞和JIEba分詞對比中,我選擇了JIEba分詞。因為我的搜索引擎一直是使用PanGu分詞,然後卻時常出現有些比較新的冷的詞,無法被分詞,導致搜索效果很差。究其原因,是PanGu分詞的字典不夠大,但是人工維護字典很煩。當然PanGu分詞有新詞錄入的功能,我一直打開這個功能的開關:

 MatchOptions m = new MatchOptions();
 m.UnknownWordIdentify = true;

然而並沒有改善。後來我使用了JIEba分詞測試分詞效果,發現JIEba分詞使用搜索引擎模式,和PanGu分詞打開多元分詞功能開關時的分詞效果如下:

測試樣例:小明碩士畢業於中國科學院計算所,後在日本京都大學深造

結巴分詞(搜索引擎模式):小明/ 碩士/ 畢業/ 於/ 中國/ 科學/ 學院/ 科學院/ 中國科學院/ 計算/ 計算所/ ,/ 後/ 在/ 日本/ 京都/ 大學/ 日本京都大學/ 深造

盤古分詞(開啟多元分詞開關): 小  明  碩士  畢業  於  中國科學院  計算所  後  在  日本  京都  大學  深造

顯然PanGu分詞並沒有細粒度分詞,這是導致有些搜索召回率很低的原因。

這裏就不對PanGu分詞,和JIEba分詞的具體分詞方法進行比較了。本篇博文的還是主要講解Lucene和JIEba分詞

二,JIEba分詞支持Lucene

在上面的JIEba分詞.net core版本中,JIEba分詞只是將給到的一個字符串進行分詞,然後反饋給你分詞信息,分詞信息也只是一個一個字符串。顯然這是無法接入到Lucene中。那麽如何把一個分詞工具成功的接入到Lucene中呢?

1.建立Analyzer類

所有要接入Lucene中的分詞工具,都要有一個繼承Lucene.Net.Analyzer的類,在這個類:JIEbaAnalyzer中,必須要覆寫TokenStreamComponents函數,因為Lucene正是通過這個函數獲取分詞器分詞之後的TokenStream(一些列分詞信息的集合)我們可以在這個函數中給tokenStream中註入我們想要得到的屬性,在Lucene.net 4.8.0中分詞的概念已經是一些列分詞屬性的組合

  public class JieBaAnalyzer
        :Analyzer
    {
        public TokenizerMode mode;
        public JieBaAnalyzer(TokenizerMode Mode)
            :base()
        {
            this.mode = Mode;
        }

        protected override TokenStreamComponents CreateComponents(string filedName,TextReader reader)
        {
            var tokenizer = new JieBaTokenizer(reader,mode);

            var tokenstream = (TokenStream)new LowerCaseFilter(Lucene.Net.Util.LuceneVersion.LUCENE_48, tokenizer);

            tokenstream.AddAttribute<ICharTermAttribute>();
            tokenstream.AddAttribute<IOffsetAttribute>();

            return new TokenStreamComponents(tokenizer, tokenstream);
        }
    }
}

這裏可以看到,我只使用了ICharTermAttribute 和IOffsetAttribute 也就是分詞的內容屬性和位置屬性。這裏的Mode要提一下,這是JIEba分詞的特性,JIEba分詞提供了三種模式:

  • 精確模式,試圖將句子最精確地切開,適合文本分析;
  • 全模式,把句子中所有的可以成詞的詞語都掃描出來, 速度非常快,但是不能解決歧義;
  • 搜索引擎模式,在精確模式的基礎上,對長詞再次切分,提高召回率,適合用於搜索引擎分詞。

這裏的Model只有Default和Search兩種,一般的,寫入索引的時候使用Search模式,查詢的時候使用Default模式

上面的JieBaTokenizer類正是我們接下來要定義的類

1.建立Tokenizer類

繼承Lucene.Net.Tokenizer 。Tokenizer 是正真將大串文本分成一系列分詞的類,在Tokenizer類中,我們必須要覆寫 Reset()函數,IncrementToken()函數,上面的Analyzer類中:

var tokenstream = (TokenStream)new LowerCaseFilter(Lucene.Net.Util.LuceneVersion.LUCENE_48, tokenizer);

tokenizer是生產tokenstream。實際上Reset()函數是將文本進行分詞,IncrementToken()是遍歷分詞的信息,然後將分詞的信息註入的tokenstream,這樣就得到我們想要的分詞流。在Tokenizer類中我們調用JIEba分詞的Segment實例,對文本進行分詞。再將獲得分詞包裝,遍歷。

 public class JieBaTokenizer
        : Tokenizer
    {
        private static object _LockObj = new object();
        private static bool _Inited = false;
        private System.Collections.Generic.List<JiebaNet.Segmenter.Token> _WordList = new List<JiebaNet.Segmenter.Token>();
        private string _InputText;
        private bool _OriginalResult = false;

        private ICharTermAttribute termAtt;
        private IOffsetAttribute offsetAtt;
        private IPositionIncrementAttribute posIncrAtt;
        private ITypeAttribute typeAtt;

        private List<string> stopWords = new List<string>();
        private string stopUrl="./stopwords.txt";
        private JiebaSegmenter segmenter;

        private System.Collections.Generic.IEnumerator<JiebaNet.Segmenter.Token> iter;
        private int start =0;

        private TokenizerMode mode;



        public JieBaTokenizer(TextReader input,TokenizerMode Mode)
            :base(AttributeFactory.DEFAULT_ATTRIBUTE_FACTORY,input)
        {
            segmenter = new JiebaSegmenter();
            mode = Mode;
            StreamReader rd = File.OpenText(stopUrl);
            string s = "";
            while((s=rd.ReadLine())!=null)
            {
                stopWords.Add(s);
            }
           
            Init();
            
        }

        private void Init()
        {
            termAtt = AddAttribute<ICharTermAttribute>();
            offsetAtt = AddAttribute<IOffsetAttribute>();
            posIncrAtt = AddAttribute<IPositionIncrementAttribute>();
            typeAtt = AddAttribute<ITypeAttribute>();
        }

        private string ReadToEnd(TextReader input)
        {
            return input.ReadToEnd();
        }

        public sealed override Boolean IncrementToken()
        {
            ClearAttributes();

            Lucene.Net.Analysis.Token word = Next();
            if(word!=null)
            {
                var buffer = word.ToString();
                termAtt.SetEmpty().Append(buffer);
                offsetAtt.SetOffset(CorrectOffset(word.StartOffset),CorrectOffset(word.EndOffset));
                typeAtt.Type = word.Type;
                return true;
            }
            End();
            this.Dispose();
            return false;
            
        }

        public Lucene.Net.Analysis.Token Next()
        {
           
            int length = 0;
            bool res = iter.MoveNext();
            Lucene.Net.Analysis.Token token;
            if (res)
            {
                JiebaNet.Segmenter.Token word = iter.Current;

                token = new Lucene.Net.Analysis.Token(word.Word, word.StartIndex,word.EndIndex);
               // Console.WriteLine("xxxxxxxxxxxxxxxx分詞:"+word.Word+"xxxxxxxxxxx起始位置:"+word.StartIndex+"xxxxxxxxxx結束位置"+word.EndIndex);
                start += length;
                return token;

            }
            else
                return null;    
            
        }

        public override void Reset()
        {
            base.Reset();

            _InputText = ReadToEnd(base.m_input);
            RemoveStopWords(segmenter.Tokenize(_InputText,mode));


            start = 0;
            iter = _WordList.GetEnumerator();

        }

        public void RemoveStopWords(System.Collections.Generic.IEnumerable<JiebaNet.Segmenter.Token> words)
        {
            _WordList.Clear();
            
            foreach(var x in words)
            {
                if(stopWords.IndexOf(x.Word)==-1)
                {
                    _WordList.Add(x);
                }
            }

        }

    }

一開始我寫的Tokenizer類並不是這樣,因為遇到了一些問題,才逐漸改成上面的樣子,下面就說下自己遇到的問題。

3.問題和改進

3.1 JIEba CutForSearch

一開始在Reset函數中,我使用的是JIEba分詞介紹的CutForSearch函數,CutForSearch的到是List<String> ,所以位置屬性OffsetAttribute得我自己來寫:

 public Lucene.Net.Analysis.Token Next()
        {
           
            int length = 0;
            bool res = iter.MoveNext();
            Lucene.Net.Analysis.Token token;
            if (res)
            {
                JiebaNet.Segmenter.Token word = iter.Current;

                token = new Lucene.Net.Analysis.Token(word.Word, word.StartIndex,word.EndIndex);
                start += length;
                return token;

            }
            else
                return null;    
            
        }

自己定義了start,根據每個分詞的長度,很容易算出來每個分詞的位置。但是我忘了CutForSearch是一個細粒度模式,會有“中國模式”,“中國”,“模式”同時存在,這樣的寫法就是錯的了,如果是Cut就對了。分詞的位置信息錯誤,帶來的就是高亮的錯誤,因為高亮需要知道分詞的正確的起始和結束位置。具體的錯誤就是:

 at System.String.Substring(Int32 startIndex, Int32 length)
   at Lucene.Net.Search.VectorHighlight.BaseFragmentsBuilder.MakeFragment(StringBuilder buffer, Int32[] index, Field[] values, WeightedFragInfo fragInfo, String[] preTags, String[] postTags, IEncoder encoder) in C:\BuildAgent\work\b1b63ca15b99dddb\src\Lucene.Net.Highlighter\VectorHighlight\BaseFragmentsBuilder.cs:line 195
   at Lucene.Net.Search.VectorHighlight.BaseFragmentsBuilder.CreateFragments(IndexReader reader, Int32 docId, String fieldName, FieldFragList fieldFragList, Int32 maxNumFragments, String[] preTags, String[] postTags, IEncoder encoder) in C:\BuildAgent\work\b1b63ca15b99dddb\src\Lucene.Net.Highlighter\VectorHighlight\BaseFragmentsBuilder.cs:line 146
   at Lucene.Net.Search.VectorHighlight.BaseFragmentsBuilder.CreateFragments(IndexReader reader, Int32 docId, String fieldName, FieldFragList fieldFragList, Int32 maxNumFragments) in C:\BuildAgent\work\b1b63ca15b99dddb\src\Lucene.Net.Highlighter\VectorHighlight\BaseFragmentsBuilder.cs:line 99

當你使用Lucene的時候出現這樣的錯誤,大多數都是你的分詞位置屬性出錯。

後來才發現JIEba分詞提供了 Tokenize()函數,專門提供了分詞以及分詞的位置信息,我很欣慰的用了Tokenize()函數,結果還是報錯,一樣的報錯,當我嘗試著加上CorrectOffset()函數的時候:

 offsetAtt.SetOffset(CorrectOffset(word.StartOffset),CorrectOffset(word.EndOffset));

雖然不報錯了,但是高亮的效果總是有偏差,總而言之換了Tokenize函數,使用CorrectOffset函數,都無法使分詞的位置信息變準確。於是查看JIEba分詞的源碼。

Tokenize函數:

 public IEnumerable<Token> Tokenize(string text, TokenizerMode mode = TokenizerMode.Default, bool hmm = true)
        {
            var result = new List<Token>();

            var start = 0;
            if (mode == TokenizerMode.Default)
            {
                foreach (var w in Cut(text, hmm: hmm))
                {
                    var width = w.Length;
                    result.Add(new Token(w, start, start + width));
                    start += width;
                }
            }
            else
            {
                foreach (var w in Cut(text, hmm: hmm))
                {
                    var width = w.Length;
                    if (width > 2)
                    {
                        for (var i = 0; i < width - 1; i++)
                        {
                            var gram2 = w.Substring(i, 2);
                            if (WordDict.ContainsWord(gram2))
                            {
                                result.Add(new Token(gram2, start + i, start + i + 2));
                            }
                        }
                    }
                    if (width > 3)
                    {
                        for (var i = 0; i < width - 2; i++)
                        {
                            var gram3 = w.Substring(i, 3);
                            if (WordDict.ContainsWord(gram3))
                            {
                                result.Add(new Token(gram3, start + i, start + i + 3));
                            }
                        }
                    }

                    result.Add(new Token(w, start, start + width));
                    start += width;
                }
            }

            return result;
        }

Cut函數:

 public IEnumerable<string> Cut(string text, bool cutAll = false, bool hmm = true)
        {
            var reHan = RegexChineseDefault;
            var reSkip = RegexSkipDefault;
            Func<string, IEnumerable<string>> cutMethod = null;

            if (cutAll)
            {
                reHan = RegexChineseCutAll;
                reSkip = RegexSkipCutAll;
            }

            if (cutAll)
            {
                cutMethod = CutAll;
            }
            else if (hmm)
            {
                cutMethod = CutDag;
            }
            else
            {
                cutMethod = CutDagWithoutHmm;
            }

            return CutIt(text, cutMethod, reHan, reSkip, cutAll);
        }

終於找到了關鍵的函數:CutIt

 internal IEnumerable<string> CutIt(string text, Func<string, IEnumerable<string>> cutMethod,
                                           Regex reHan, Regex reSkip, bool cutAll)
        {
            var result = new List<string>();
            var blocks = reHan.Split(text);
            foreach (var blk in blocks)
            {
                if (string.IsNullOrWhiteSpace(blk))
                {
                    continue;
                }

                if (reHan.IsMatch(blk))
                {
                    foreach (var word in cutMethod(blk))
                    {
                        result.Add(word);
                    }
                }
                else
                {
                    var tmp = reSkip.Split(blk);
                    foreach (var x in tmp)
                    {
                        if (reSkip.IsMatch(x))
                        {
                            result.Add(x);
                        }
                        else if (!cutAll)
                        {
                            foreach (var ch in x)
                            {
                                result.Add(ch.ToString());
                            }
                        }
                        else
                        {
                            result.Add(x);
                        }
                    }
                }
            }

            return result;
        }

在CutIt函數中JieBa分詞都把空格省去,這樣在Tokenize函數中使用start=0 start+=word.Length 顯示不能得到正確的原始文本中的位置。

  if (string.IsNullOrWhiteSpace(blk))
                {
                    continue;
                }

JIEba分詞也沒有考慮到會使用Lucene的高亮,越是只能自己改寫了CutIt函數和Tokenize函數:

在CutIt函數中,返回的值不在是一個string,而是一個包含string,startPosition的類,這樣在Tokenize中就很準確的得到每個分詞的位置屬性了。

 internal IEnumerable<WordInfo> CutIt2(string text, Func<string, IEnumerable<string>> cutMethod,
                                           Regex reHan, Regex reSkip, bool cutAll)
        {
            //Console.WriteLine("*********************************我開始分詞了*******************");
            var result = new List<WordInfo>();
            var blocks = reHan.Split(text);
            var start = 0;
            foreach(var blk in blocks)
            {
                //Console.WriteLine("?????????????當前的串:"+blk);
                if(string.IsNullOrWhiteSpace(blk))
                {
                    start += blk.Length;
                    continue;
                }
                if(reHan.IsMatch(blk))
                {
                    
                    foreach(var word in cutMethod(blk))
                    {
                        //Console.WriteLine("?????blk 分詞:" + word + "????????初始位置:" + start);
                        result.Add(new WordInfo(word,start));
                        start += word.Length;
                    }
                }
                else
                {
                    var tmp = reSkip.Split(blk);
                    foreach(var x in tmp)
                    {
                        if(reSkip.IsMatch(x))
                        {
                            //Console.WriteLine("????? x  reSkip 分詞:" + x + "????????初始位置:" + start);
                            result.Add(new WordInfo(x,start));
                            start += x.Length;
                        }
                        else if(!cutAll)
                        {
                            foreach(var ch in x)
                            {
                                //Console.WriteLine("?????ch  分詞:" + ch + "????????初始位置:" + start);
                                result.Add(new WordInfo(ch.ToString(),start));
                                start += ch.ToString().Length;
                            }
                        }
                        else{
                            //Console.WriteLine("?????x  分詞:" + x + "????????初始位置:" + start);
                            result.Add(new WordInfo(x,start));
                            start += x.Length;
                            
                        }
                    }
                }
            }

            return result;
        }



 public IEnumerable<Token> Tokenize(string text, TokenizerMode mode = TokenizerMode.Default, bool hmm = true)
        {
            var result = new List<Token>();

            if (mode == TokenizerMode.Default)
            {
                foreach (var w in Cut2(text, hmm: hmm))
                {
                    var width = w.value.Length;
                    result.Add(new Token(w.value, w.position, w.position + width));

                }
            }
            else
            {
                var xx = Cut2(text, hmm: hmm);
                foreach (var w in Cut2(text, hmm: hmm))
                {
                    var width = w.value.Length;
                    if (width > 2)
                    {
                        for (var i = 0; i < width - 1; i++)
                        {
                            var gram2 = w.value.Substring(i, 2);
                            if (WordDict.ContainsWord(gram2))
                            {
                                result.Add(new Token(gram2, w.position + i, w.position + i + 2));
                            }
                        }
                    }
                    if (width > 3)
                    {
                        for (var i = 0; i < width - 2; i++)
                        {
                            var gram3 = w.value.Substring(i, 3);
                            if (WordDict.ContainsWord(gram3))
                            {
                                result.Add(new Token(gram3, w.position + i, w.position + i + 3));
                            }
                        }
                    }

                    result.Add(new Token(w.value, w.position, w.position + width));

                 }
            }

            return result;
        }



 public class WordInfo
    {
        public WordInfo(string value,int position)
        {
            this.value = value;
            this.position = position;
        }
        //分詞的內容
        public string value { get; set; }
        //分詞的初始位置
        public int position { get; set; }
    }

這樣的話,終於可以正確的進行高亮了,果然搜索效果要比PanGu分詞好很多。

4.停用詞

是用JIEba的停用詞的方法,是把停用詞的文件裏的內容讀取出來,然後在Reset()函數裏把停用詞都過濾掉:

 StreamReader rd = File.OpenText(stopUrl);
            string s = "";
            while((s=rd.ReadLine())!=null)
            {
                stopWords.Add(s);
            }

 public override void Reset()
        {
            base.Reset();

            _InputText = ReadToEnd(base.m_input);
            RemoveStopWords(segmenter.Tokenize(_InputText,mode));


            start = 0;
            iter = _WordList.GetEnumerator();

        }

        public void RemoveStopWords(System.Collections.Generic.IEnumerable<JiebaNet.Segmenter.Token> words)
        {
            _WordList.Clear();
            
            foreach(var x in words)
            {
                if(stopWords.IndexOf(x.Word)==-1)
                {
                    _WordList.Add(x);
                }
            }

        }

5.索引速度

使用JIEba分詞之後,雖然效果很好,但是寫索引的速度很慢,考慮到時細粒度分詞,相比以前一篇文章多出來很多分詞,所以索引速度慢了8倍左右,但是感覺這並不正常,前面的開源代碼測試結果中,CutForSearch很快的,應該是自己的代碼哪裏出了問題。

三,Lucene的高亮

這裏再對Lucene的高亮的總結一下,Lucene提供了兩種高亮模式,一種是普通高亮,一種是快速高亮。

1.普通高亮

普通高亮的原理,就是將搜索之後得到的文檔,使用分詞器再進行分詞,得到的TokenStream,再進行高亮:

 SimpleHTMLFormatter simpleHtmlFormatter = new SimpleHTMLFormatter("<span style=‘color:red;‘>", "</span>");

            Lucene.Net.Search.Highlight.Highlighter highlighter = new Lucene.Net.Search.Highlight.Highlighter(simpleHtmlFormatter, new QueryScorer(query));

            highlighter.TextFragmenter = new SimpleFragmenter(150);
Analyzer analyzer = new JieBaAnalyzer(TokenizerMode.Search);


            TokenStream tokenStream = analyzer.GetTokenStream("Content", new StringReader(doc.Get("Content")));
var frags = highlighter.GetBestFragments(tokenStream, doc.Get(fieldName), 200);

2.快速高亮

之所很快速,是因為高亮是直接根據索引儲存的信息進行高亮,前面已經說過我們索引需要儲存分詞的位置信息,這個就是為高亮服務的,所以速度很快,當然帶來的後果是你的索引文件會比較大,因為儲存了位置信息。

 FastVectorHighlighter fhl = new FastVectorHighlighter(false, false, simpleFragListBuilder, scoreOrderFragmentsBuilder);
            FieldQuery fieldQuery = fhl.GetFieldQuery(query,_indexReader);

          highLightSetting.MaxFragNum.GetValueOrDefault(MaxFragNumDefaultValue);
            var frags = fhl.GetBestFragments(fieldQuery, _indexReader, docid, fieldName, fragSize, maxFragNum);

快速高亮的關鍵源代碼:

   protected virtual string MakeFragment(StringBuilder buffer, int[] index, Field[] values, WeightedFragInfo fragInfo,
            string[] preTags, string[] postTags, IEncoder encoder)
        {
            StringBuilder fragment = new StringBuilder();
            int s = fragInfo.StartOffset;
            int[] modifiedStartOffset = { s };
            string src = GetFragmentSourceMSO(buffer, index, values, s, fragInfo.EndOffset, modifiedStartOffset);
            int srcIndex = 0;
            foreach (SubInfo subInfo in fragInfo.SubInfos)
            {
                foreach (Toffs to in subInfo.TermsOffsets)
                {
                    
                    fragment
                        .Append(encoder.EncodeText(src.Substring(srcIndex, (to.StartOffset - modifiedStartOffset[0]) - srcIndex)))
                        .Append(GetPreTag(preTags, subInfo.Seqnum))
                        .Append(encoder.EncodeText(src.Substring(to.StartOffset - modifiedStartOffset[0], (to.EndOffset - modifiedStartOffset[0]) - (to.StartOffset - modifiedStartOffset[0]))))
                        .Append(GetPostTag(postTags, subInfo.Seqnum));
                    srcIndex = to.EndOffset - modifiedStartOffset[0];
                }
            }
            fragment.Append(encoder.EncodeText(src.Substring(srcIndex)));
            return fragment.ToString();
        }

fragInfo儲存了所有需要高亮的關鍵字和位置信息,src則是原始文本,而之前報的錯誤正是這裏引起的錯誤,由於位置信息有誤src.Substring就會報錯。

四,結語

.net core2.0版的中文分詞確實不多,相比較之下,java,c++,的分詞工具有很多,或許可以用c++的速度快的特點,做一個單獨分詞服務,效果是不是會更好。

Lucene.net(4.8.0) 學習問題記錄五: JIEba分詞和Lucene的結合,以及對分詞器的思考