天天看點

Lucene.net(4.8.0) 學習問題記錄二: 分詞器Analyzer中的TokenStream和AttributeSource

前言:目前自己在做使用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的位元組資訊,以後會介紹道

繼續閱讀