• 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/SilentCC/Lucene.Net.Analysis.PanGu

     JIEba分词(可以直接使用的)

    https://github.com/SilentCC/JIEba-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分词有新词录入的功能,我一直打开这个功能的开关:

     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: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++的速度快的特点,做一个单独分词服务,效果是不是会更好。

  • 相关阅读:
    数据结构(1)
    数据库知识(2)
    Leetcode每日一题(1)
    数据库知识(1)
    Redis之MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist………………
    mstsc远程连接本地的虚拟机步骤
    Spring Scurity入门--遇到的坑-01
    idea环境连接Oracle数据库步骤
    虚拟机oracle: ORA-12514,TNS:listener does not currently know of SID given in connect descriptor错误解决
    多模块Maven工程 install时 出现Compilation failure: Compilation failure: …………ProductServiceImpl.java:[3,26] 程序包com.ssm.dao不存在 的错误解决办法
  • 原文地址:https://www.cnblogs.com/dacc123/p/8431369.html
Copyright © 2020-2023  润新知