前言:目前自己在做使用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的同學。
一,Analyzer 中的TokenStream
1.TokenSteam的產生
在這篇博文中,其實已經介紹了TokenStream 是怎么產生的:
http://www.cnblogs.com/dacc123/p/8035438.html
在Analyzer 中,同一個線程上的所有Analyzer實例都是共用一個TokenStream,而實現如此都是因為Analyzer類中 storedValue 是全局共用的,獲取TokenStream的方法是由reuseStrategy 類提供的,TokenStream 繼承自AttributeSource
那么TokenStream的作用什么呢?
2.TokenSteam的使用
TokenStream 實際上是由一系列Token(分詞)組合起來的序列,這里僅僅介紹如何通過TokenStream獲得分詞的信息。TokenStream的工作流程:
1. 創建TokenStream
2.TokenStream.Reset()
3.TokenStream.IncrementToken()
4.TokenStream.End();
5.TokenStream.Dispose() //Lucene 4.8.0中已經取消了Close(),只有Dispose()
在執行:
_indexWriter.AddDocument(doc)
之后,IndexWriter則會調用初始化時創建的Analyzer,也即IndewWriterConfig()中的Analyzer參數。這里以PanGu分詞為例子。
調用分詞器,首先會執行CreateComponents()函數,創建一個TokenStreamComponents,這也是為什么所有自定義,或者外部的分詞器如果繼承Analyzer,必須要覆寫CreateComponents()函數:
protected override TokenStreamComponents CreateComponents(string fieldName, TextReader reader) { var result = new PanGuTokenizer(reader, _originalResult, _options, _parameters); var finalStream = (TokenStream)new LowerCaseFilter(LVERSION.LUCENE_48, result); finalStream.AddAttribute<ICharTermAttribute>(); finalStream.AddAttribute<IOffsetAttribute>(); return new TokenStreamComponents(result, finalStream); }
可以看到在這個CreateComponents函數中,我們可以初始化創建自己想要的Tokenizer和TokenStream。TokenStreamComponents是Lucene4.0中才有的,一個TokenStreamComponents是由Tokenizer和TokenStream組成。
在初始化完TokenStream 之后我們可以添加屬性Attribute 到TokenStream中:
finalStream.AddAttribute<ICharTermAttribute>();
finalStream.AddAttribute<IOffsetAttribute>();
2.1 AttributeSource的介紹
上面說到TokenStream 繼承自AttributeSource , finalStream.AddAttribute<ICharTermAttribute> 真是調用了父類AttributeSource的方法AddAttribute<T>() ,所以AttributeSoucre是用來給TokenStream添加一系列屬性的,這是Lucene4.8.0中AttributeSource中AddAttribute的源碼:
public T AddAttribute<T>() where T : IAttribute { var attClass = typeof(T); if (!attributes.ContainsKey(attClass)) { if (!(attClass.GetTypeInfo().IsInterface && typeof(IAttribute).IsAssignableFrom(attClass))) { throw new ArgumentException("AddAttribute() only accepts an interface that extends IAttribute, but " + attClass.FullName + " does not fulfil this contract."); } //正真添加Attribute的函數,而創造Attribute實例則是通過AttributeSource中的
//private readonly AttributeFactory factory; AddAttributeImpl(this.factory.CreateAttributeInstance<T>()); } T returnAttr; try { returnAttr = (T)(IAttribute)attributes[attClass].Value; } #pragma warning disable 168 catch (KeyNotFoundException knf) #pragma warning restore 168 { return default(T); } return returnAttr; }
2.2 Attribute介紹
上面介紹了AttributeSource 給TokenStream添加屬性Attribute ,其實Attribute就是你需要獲得的分詞的屬性。
比如:上面寫到的 ICharTermAttribute 繼承自CharTermAttribute 表示的是分詞內容;
IOffsetAttribute 繼承自 OffsetAttribute 表示的是分詞起始位置和結束位置;
類似的還有 IFlasAttribute , IKeywordAttribute,IPayloadAttribute,IPositionIncrementAttribute,IPositionLengthAttribute,ITermToBytesRefAttribute,ITypeAttribute
我們再看Token(分詞)類的源碼:
public class Token : CharTermAttribute, ITypeAttribute, IPositionIncrementAttribute, IFlagsAttribute, IOffsetAttribute, IPayloadAttribute, IPositionLengthAttribute
其實Token(分詞),是繼承這些Attribute,也就是說分詞是由這些屬性組成的,所以就可以理解為什么在TokenStream中添加Attributes。
再回到之前,再初始化TokenStream 和添加完屬性之后,必須執行TokenStream的Reset(),才可繼續執行TokenStream.IncrementToken().
Reset()函數實際上在TokenStream創建和使用之后進行重置,因為我們之前說過,在Analyzer中所有實例是共用一個TokenStream的所以在TokenStream被使用過一次后,需要Reset() 以清除上次使用的信息,重新給下一個需要分詞的text使用。
而IncrementToken實際的作用則是在遍歷TokenStream 中的Token,類似於一個迭代器。
public sealed override bool IncrementToken() { ClearAttributes(); Token word = Next(); if (word != null) { var buffer = word.ToString(); termAtt.SetEmpty().Append(buffer); offsetAtt.SetOffset(word.StartOffset, word.EndOffset); typeAtt.Type = word.Type; return true; } End(); this.Dispose(); return false; }
直到返回的false ,表示分詞已經遍歷完了,這個時候調用End() 和Dispose() 來注銷這個TokenStream。在這個過程中,TokenStream是可以被使用多次的,比如我寫入索引的時候,加入兩個Field :
new Field("title","xxxx") new Field("content","xxxxx")
對這個兩個域進行分詞,TokenStream創建之后,會先對title進行分詞,遍歷。然后執行Reset(),再對content進行分詞,遍歷。直到所有要分詞的域都遍歷過了。才會執行End()和Dispose()函數進行銷毀。
二,問題:搜索不到內容
在遷移的過程中,突然出現了搜索不到內容的bug,經過調試,發現寫索引的時候,對文本的分詞都是正確。這里要提一點,分詞(Token) 和 Term的區別 ,term是最小的搜索的單位,就是每個詞語,比如“我是搞IT的”,那么,經過分詞 “我”,“是”,“搞”,“IT” 這些都是term,而這些分詞的具體信息,比如起始位置信息,都包含在Token當中,在Lucene2.9中之后,已經不推薦用Token(分詞),而直接用Attribute表示這些term的屬性
后來發現寫索引的時候正常,但是在搜索的時候,獲取搜索關鍵詞是,利用自己寫的TokenStream獲取分詞信息出了錯。
tokenStream.Reset(); //ItermAttribute在Lucene4.8.0中已經替換為CharTermAttribute while (tokenStream.IncrementToken()) { var termAttr = tokenStream.GetAttribute<ICharTermAttribute>(); var str = new string(termAttr.Buffer, 0, termAttr.Buffer.Length); var positionAttr = tokenStream.GetAttribute<IOffsetAttribute>(); var start = positionAttr.StartOffset; var end = positionAttr.EndOffset; yield return new Token() { EndPosition = end, StartPosition = start, Term = str }; }
termAttr.Buffer 是字節數組,而termAttr.Buffer.Length 是字節數組的長度,是固定。而termAttr.Length 是字節數組中實際元素的長度,是不一樣的。我那樣寫會導致得到term字節信息是 [69,5b,23,/0,/0,/0,/0,/0,/0,/0] 因為長度填錯了,所以后面自動填充/0,這樣自然搜索不到,改成termAttr.Length就可以了。
這里在提一下在Lcuene.net 4.0中新增了BytesRef 類,表示term的字節信息,以后會介紹道