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的同學。
目錄
- 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分詞有新詞錄入的功能,我一直打開這個功能的開關:
|
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:\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++的速度快的特點,做一個單獨分詞服務,效果是不是會更好。

