大家好,又见面了,我是你们的朋友全栈君。
首先介绍下在lucene中attributeSource的类层次:
org.apache.lucene.util.AttributeSource
· org.apache.lucene.analysis.TokenStream (implementsjava.io.Closeable)
· org.apache.lucene.analysis.NumericTokenStream
· org.apache.lucene.analysis.TeeSinkTokenFilter.SinkTokenStream
· org.apache.lucene.analysis.TokenFilter
· org.apache.lucene.analysis.ASCIIFoldingFilter
· org.apache.lucene.analysis.CachingTokenFilter
· org.apache.lucene.analysis.ISOLatin1AccentFilter
· org.apache.lucene.analysis.LengthFilter
· org.apache.lucene.analysis.LowerCaseFilter
· org.apache.lucene.analysis.PorterStemFilter
· org.apache.lucene.analysis.StopFilter
· org.apache.lucene.analysis.TeeSinkTokenFilter
· org.apache.lucene.analysis.Tokenizer
· org.apache.lucene.analysis.CharTokenizer
· org.apache.lucene.analysis.LetterTokenizer
· org.apache.lucene.analysis.LowerCaseTokenizer
· org.apache.lucene.analysis.WhitespaceTokenizer
· org.apache.lucene.analysis.KeywordTokenizer
从上可以看出 AttributeSource是作为TokenStream 的超类,
1.首先我们来看AttributeSource的实现:
1.1 成员属性:
private finalMap<Class<? extends Attribute>, AttributeImpl> attributes;
private final Map<Class<? extends AttributeImpl>, AttributeImpl>attributeImpls;
privateAttributeFactory factory;
private staticfinal WeakHashMap<Class<? extendsAttributeImpl>,LinkedList<WeakReference<Class<? extendsAttribute>>>> knownImplClasses;
1.1.1 Attribute:
仅仅是一个空的接口,实现该接口的接口有FlagsAttribute, OffsetAttribute, PayloadAttribute,PositionIncrementAttribute, TermAttribute, TypeAttribute,从他们的命名上可以看出其与lucene项的属性有关联,其实上述接口的确与项的属性有一一对应的关系,这些接口定义了对项的特定属性的规范操作,但仅仅定义了操作的行为,具体的属性的数据却在在AttributeImpl的派生类中定义。
1.1.2AttributeImpl:
类定义如是:public abstract class AttributeImpl implements Cloneable, Serializable,Attribute
具体的继承该类的派生类有
FlagsAttributeImpl,OffsetAttributeImpl, PayloadAttributeImpl, PositionIncrementAttributeImpl,TermAttributeImpl, Token, TypeAttributeImpl
看FlagsAttributeImpl具体的类定义:public class FlagsAttributeImpl extends AttributeImpl implementsFlagsAttribute, Cloneable, Serializable
可见其实现了FlagsAttribute接口,另外看FlagsAttributeImpl的源代码可知,其内还有关于项的标志属性的数据信息,因此我们可以推测:
AttributeImpl是属性的数据信息以及和该属性关联的操作的类,该属性关联的操作放在了Attribute接口中定义,实现是在AttributeImpl中。
1.1.3 综上,我们有结论如下,Token有若干的属性,对每个属性其表达由Attribute和AttributeImpl完成,Attribute定义对该属性的操作,AttributeImpl实现该接口并包含具体的属性数据。
1.1.4
private finalMap<Class<? extends Attribute>, AttributeImpl> attributes;
private final Map<Class<? extends AttributeImpl>, AttributeImpl>attributeImpls;
上述两个成员保存了两种映射关系,AttributeImpl实例对应实现的所有Attribute接口,都可以映射到该AttributeImpl实例,这是第一个映射;第二个映射是AttributeImpl实例对应实现的AttributeImpl抽象类对该AttributeImpl实例的映射。
设计这两个映射关系的目的是在该AttributeSource实例中对每个Attribute和AttributeImpl保证只有一个AttributeImpl实例,换句话说,当用具体Attribute或者具体AttributeImpl获取其对象实例时,不会每次都新建实例,而是首次时建立,其后只返回以前建立的。下面来剖析实现方法;
public voidaddAttributeImpl(final AttributeImpl att) {
/*
从绿色部分可以看出如果之前已经有att类型的实例被保存在映射中,那么添加该类型实例将失败;
//*/
final Class<? extends AttributeImpl>clazz = att.getClass();
if (attributeImpls.containsKey(clazz)) return;
/*
深红色部分是获取该att实现的所有Attribute接口,并保存在foundInterfaces中;
//*/
LinkedList<WeakReference<Class<?extends Attribute>>> foundInterfaces;
synchronized(knownImplClasses) {
foundInterfaces = knownImplClasses.get(clazz);
if (foundInterfaces == null) {
// we have a strong reference to theclass instance holding all interfaces in the list (parameter “att”),
// so all WeakReferences are neverevicted by GC
knownImplClasses.put(clazz,foundInterfaces = new LinkedList<WeakReference<Class<? extendsAttribute>>>());
// find all interfaces that thisattribute instance implements
// and that extend the Attributeinterface
Class<?> actClazz = clazz;
do {
for (Class<?>curInterface : actClazz.getInterfaces()) {
if(curInterface != Attribute.class &&Attribute.class.isAssignableFrom(curInterface)) {
foundInterfaces.add(new WeakReference<Class<? extendsAttribute>>(curInterface.asSubclass(Attribute.class)));
}
}
actClazz =actClazz.getSuperclass();
} while (actClazz != null);
}
}
/*
海军蓝部分是将该att实现的每个Attribute接口与att之间的映射关系添加到Map中;
//*/
// add all interfacesof this AttributeImpl to the maps
for (WeakReference<Class<? extends Attribute>>curInterfaceRef : foundInterfaces) {
final Class<? extends Attribute>curInterface = curInterfaceRef.get();
assert (curInterface != null) :
“We have a strong reference onthe class holding the interfaces, so they should never get evicted”;
// Attribute is a superclass of this interface
if (!attributes.containsKey(curInterface)) {
// invalidate state to forcerecomputation in captureState()
this.currentState = null;
attributes.put(curInterface, att);
attributeImpls.put(clazz, att);
}
}
}
public <Aextends Attribute> A addAttribute(Class<A> attClass) {
AttributeImpl attImpl = attributes.get(attClass);
if (attImpl == null) {
if (!(attClass.isInterface() && Attribute.class.isAssignableFrom(attClass))){
throw new IllegalArgumentException(
“addAttribute()only accepts an interface that extends Attribute, but ” +
attClass.getName() +” does not fulfil this contract.”
);
}
addAttributeImpl(attImpl= this.factory.createAttributeInstance(attClass));
}
return attClass.cast(attImpl);
}
AttributeSource中有一个内部类AttributeFactory类,其中维护了Attribute.Class和AttributeImpl.Class的
对应关系,但是纵观AttributeFactory的实现却并未发现有专用的添加删除操作来维护这种关系,其实其维护只通过一个函数
publicAttributeImpl createAttributeInstance(Class<? extends Attribute>attClass),当AttributeFactory中的对应关系中有Attribute.Class对应的AttributeImpl.Class映射时,则上述函数直接用AttributeImpl.Class创建AttributeImpl的类实例,并返回,如果不存在这样的映射关系,那么AttributeFactory将使用下述的方法添加这种映射并创建类实例返回。
attClassImplMap.put(attClass,
new WeakReference<Class<? extends AttributeImpl>>(
clazz = Class.forName(attClass.getName() + “Impl”, true,attClass.getClassLoader())
.asSubclass(AttributeImpl.class)
);
1.1.5
public <Aextends Attribute> A getAttribute(Class<A> attClass):返回在Map中注册的attClass对应的AttributeImpl实例;
public StatecaptureState():返回当前时刻已注册的所有AttributeImpl实例。
2. Lucene中AttributeSource作为TokenStream父类的原因的
2.1 TokenStream的作用是从给入的文本中不断解析出Token,具体的做法是TokenStream有方法incrementToken,每次调用将产生待分析文本的下一个Token,其实incrementToken做的事情就是填充我所关心的若干属性,通过这些属性来反馈分析结果,因此自然而然的一种想法是TokenStream的派生类中有若干的属性成员,每次调用incrementToken都首先清除上一次的属性信息,然后进行分析并填充属性,这样做无可厚非,但是请考虑TokenStream流的嵌套,也就是说嵌套的内层流获取的属性将作为外层流的分析的输入,如果使用上述方法实现TokenStream,则必然嵌套流的每层流都将有自己的属性实例,而层次之间可能会出现同样的属性,也就是说同样的属性实例在流层次中可能会有多个,这样是没有必要的,也就是说对相同的属性在流层次中只有一个实例就可以满足分析的需求了。
2.2 基于2.1读者可能会说嵌套时当外层流与内层流有相同的属性时,可以将外层流的该属性赋内层流的属性引用,这样就可以避免2.1的情况。错误的原因在于,我们在嵌套时,嵌套流的层次关系用户根据自己的需求组合而成的,也就是说外层流往往无法知道自己的内层流会是谁,“将外层流的该属性赋内层流的属性引用”的前提是外层流清楚内层流是谁,因此这样的方法不可行。
2.3 其实上述的获取内层TokenStream中有哪些AttributeImpl的子类实例的方法只需要通过java的反射机制就可以解决,可是为什么lucene还要用AttributeSource这么复杂的一个构建来实现呢?究其原因在于效率的考虑!
当我们将TokenStream所关心的属性抽象的由AttributeSource来管理时,我们在进行流的嵌套时,根据对AttributeSource的分析可知,外层流定义自己关心的属性,并不需要在构造函数中实例化该属性,而是从AttributeSource中获取,如果存在的话,则直接返回实例,否则新建,这样在流嵌套式外层流和内存流共享AttributeSource,也就是说当外层流和内层流都关心某个属性时,内层流首先初始化,此时他将会将该属性注册到AttributeSource中,这样在外层流初始化时将向AttributeSource获取该属性,从而可以保证在流层次中若干层流都关心的属性只有一份实例。
为什么说不用反射实现是基于效率的考虑,这是因为如果使用AttributeSource实现只有在首次注册属性时才会使用反射机制,以后都是直接获取,而如果纯使用反射机制保证流嵌套层次都关心属性实例的唯一性,那么假设有n层嵌套,那么将有n-1次对反射机制的使用,显然AttributeSource的实现方式将有更高的效率。
3. IK中的处理过程
在ik中进行初始化的时候,这些需要的属性已经加入到了分词器中了,下面便是IK初始化的时候调用的
/**
* Lucene 4.0 Tokenizer适配器类构造函数
* @param in
* @param useSmart
*/
public IKTokenizer(Reader in , boolean useSmart){
super(in);
offsetAtt = addAttribute(OffsetAttribute.class);
termAtt = addAttribute(CharTermAttribute.class);
typeAtt = addAttribute(TypeAttribute.class);
_IKImplement = new IKSegmenter(input , useSmart);
}
而在执行下面的代码的时候,已经不会再生成对象,而是直接从attributes中获取相应的对象然后返回。
//获取词元位置属性
OffsetAttribute offset = ts.addAttribute(OffsetAttribute.class);
//获取词元文本属性
CharTermAttribute term = ts.addAttribute(CharTermAttribute.class);
//获取词元文本属性
TypeAttribute type = ts.addAttribute(TypeAttribute.class);
而在执行下面的代码的时候
//迭代获取分词结果
while (ts.incrementToken()) {
System.out.println(offset.startOffset()+ ” – “+ offset.endOffset() + ” : “ + term.toString() + ” | “ + type.type());
}
调用的方法incrementToken是在IKTokenizer.java中的,其代码如下(为了更清新的展示属性处理,我们将IKTokenizer初始化也显示下):
/**
* Lucene 4.0 Tokenizer适配器类构造函数
* @param in
* @param useSmart
*/
public IKTokenizer(Reader in , boolean useSmart){
super(in);
offsetAtt = addAttribute(OffsetAttribute.class);
termAtt = addAttribute(CharTermAttribute.class);
typeAtt = addAttribute(TypeAttribute.class);
_IKImplement = new IKSegmenter(input , useSmart);
}
/* (non-Javadoc)
* @seeorg.apache.lucene.analysis.TokenStream#incrementToken()
*/
@Override
public boolean incrementToken() throws IOException {
//清除所有的词元属性
clearAttributes();
Lexeme nextLexeme = _IKImplement.next();
if(nextLexeme != null){
//将Lexeme转成Attributes
//设置词元文本
termAtt.append(nextLexeme.getLexemeText());
//设置词元长度
termAtt.setLength(nextLexeme.getLength());
//设置词元位移
offsetAtt.setOffset(nextLexeme.getBeginPosition(),nextLexeme.getEndPosition());
//记录分词的最后位置
endPosition = nextLexeme.getEndPosition();
//记录词元分类
typeAtt.setType(nextLexeme.getLexemeTypeString());
//返会true告知还有下个词元
return true;
}
//返会false告知词元输出完毕
return false;
}
看到开始我们初始化IKTokenizer的时候调用的这个初始化函数中的变量offsetAtt、termAtt、typeAtt这些都只初始化了一次放在attributes中了,在进行incrementToken()方法处理的时候对这些属性赋值的时候也不会重新初始化。
总结:
TokenStream的作用是从给入的文本中不断解析出Token,具体的做法是TokenStream有方法incrementToken,每次调用 将产生待分析文本的下一个Token,其实incrementToken做的事情就是填充用户所关心的若干属性,通过这些属性来反馈分析结果,因此自然而然 的一种想法是TokenStream的派生类中有若干的属性成员,每次调用incrementToken都首先清除上一次的属性信息,然后进行分析并填充 属性,这样做无可厚非,但是请考虑TokenStream流的嵌套,也就是说嵌套的内层流获取的属性将作为外层流的分析的输入,如果使用上述方法实现 TokenStream,则必然嵌套流的每层流都将有自己的属性实例,而层次之间可能会出现同样的属性,也就是说同样的属性实例在流层次中可能会有多个, 这样是没有必要的,也就是说对相同的属性在流层次中只有一个实例就可以满足分析的需求了。
当将TokenStream所关心的属性抽象的由AttributeSource来管理时,我们在进行流的嵌套时,根据对 AttributeSource的分析可知,外层流定义自己关心的属性,并不需要在构造函数中实例化该属性,而是从AttributeSource中获取,如果存在的话,则直接返回实例,否则新建,这样在流嵌套式外层流和内存流共享AttributeSource,也就是说当外层流和内层流都关心某个属 性时,内层流首先初始化,此时他将会将该属性注册到AttributeSource中,这样在外层流初始化时将向AttributeSource获取该属 性,从而可以保证在流层次中若干层流都关心的属性只有一份实例。
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/163102.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...