zoukankan      html  css  js  c++  java
  • ]NET Core Lucene.net和PanGu分词实现全文检索

    Lucene.net和PanGu分词实现全文检索

    Lucene.net(4.8.0) 学习问题记录五: JIEba分词和Lucene的结合,以及对分词器的思考

     

    前言:目前自己在做使用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的同学。

    目录

    一,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分词有新词录入的功能,我一直打开这个功能的开关:

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

     然而并没有改善。后来我使用了JIEba分词测试分词效果,发现JIEba分词使用搜索引擎模式,和PanGu分词打开多元分词功能开关时的分词效果如下:

    1
    2
    3
    4
    5
    测试样例:小明硕士毕业于中国科学院计算所,后在日本京都大学深造
     
    结巴分词(搜索引擎模式):小明/ 硕士/ 毕业/ 于/ 中国/ 科学/ 学院/ 科学院/ 中国科学院/ 计算/ 计算所/ ,/ 后/ 在/ 日本/ 京都/ 大学/ 日本京都大学/ 深造
     
    盘古分词(开启多元分词开关): 小  明  硕士  毕业  于  中国科学院  计算所  后  在  日本  京都  大学  深造

     显然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:BuildAgentwork1b63ca15b99dddbsrcLucene.Net.HighlighterVectorHighlightBaseFragmentsBuilder.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:BuildAgentwork1b63ca15b99dddbsrcLucene.Net.HighlighterVectorHighlightBaseFragmentsBuilder.cs:line 146
       at Lucene.Net.Search.VectorHighlight.BaseFragmentsBuilder.CreateFragments(IndexReader reader, Int32 docId, String fieldName, FieldFragList fieldFragList, Int32 maxNumFragments) in C:BuildAgentwork1b63ca15b99dddbsrcLucene.Net.HighlighterVectorHighlightBaseFragmentsBuilder.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++的速度快的特点,做一个单独分词服务,效果是不是会更好。

  • 相关阅读:
    模拟赛总结
    2018.04.06学习总结
    2018.04.06学习总结
    Java实现 LeetCode 672 灯泡开关 Ⅱ(数学思路问题)
    Java实现 LeetCode 671 二叉树中第二小的节点(遍历树)
    Java实现 LeetCode 671 二叉树中第二小的节点(遍历树)
    Java实现 LeetCode 671 二叉树中第二小的节点(遍历树)
    Java实现 LeetCode 670 最大交换(暴力)
    Java实现 LeetCode 670 最大交换(暴力)
    Java实现 LeetCode 670 最大交换(暴力)
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/8439869.html
Copyright © 2011-2022 走看看