Lucene源码系列(三十四):分词器的框架设计

概述

今天我们来介绍下Lucene中分词器这个组件,本文的目的不是为了介绍具体的分词算法实现,而是介绍Lucene中分词模块的框架设计,从而在我们的业务场景中,知道如何基于Lucene分词框架自定义满足具体业务需要的分词器。

首先,我们理一下一个分词器需要做什么,当我们为其提供一段文本输入,期待它给我们返回哪些信息:

  1. 分词的最小单位:token

  2. token的position信息

  3. token的offset信息

  4. 。。。

一般我们只关注前3个比较重要的信息。那不管是什么信息,肯定需要在进行分词处理的时候获取并且记录的。那Lucene中具体是怎么实现的呢?

Lucene中的分词框架有三个组件配合完成:

  • Analyzer:Analyzer是入口,负责创建TokenStream,并做一些可复用的控制。
  • Attribute:token的相关信息是使用Attribute来记录的,不同的TokenStream在实现时就已经注册了自己关注的属性,TokenStream每得到一个token,就会同步所有属性的信息,业务通过相关的Attribute获取信息。
  • TokenStream:它是所有分词器的顶级抽象类,从名字可以看出是一个token的流,它其实就是真正执行分词逻辑的组件,对文本输入进行分词处理,得到token相关信息。

Analyzer是入口,通过Analyzer可以创建TokenStream,从TokenStream提供了对输入分词的方法。分词的过程中需要记录比如token,position,offset等信息,每种信息对应一个Attribute,Attribute是通过AttributeSource来管理的,TokenStream继承了AttributeSource,所以TokenStream可以直接管理Attribute,在分词的过程中,把相关信息存储在每个Attribute中,分完一个词,业务就可以通过Attribute获取相关的信息。

接下来,我们根据源码详细分析下这三个组件实现及其工作方式。

本文源码版本:9.8.0

Analyzer

Analyzer是入口,负责创建TokenStream,并做一些可复用的控制。

TokenStreamComponents

java 复制代码
public static final class TokenStreamComponents {
  // 分词器的输入
  protected final Consumer<Reader> source;
  
  // 分词器  
  protected final TokenStream sink;

  // 针对输入是字符串的缓存
  transient ReusableStringReader reusableStringReader;

  // source: 数据源
  // result: 分词器  
  public TokenStreamComponents(final Consumer<Reader> source, final TokenStream result) {
    this.source = source;
    this.sink = result;
  }

  // tokenizer是最原始的分词器
  // result是tokenizer通过装饰器处理的
  public TokenStreamComponents(final Tokenizer tokenizer, final TokenStream result) {
    this(tokenizer::setReader, result);
  }

  public TokenStreamComponents(final Tokenizer tokenizer) {
    this(tokenizer::setReader, tokenizer);
  }

  // 更新reader
  private void setReader(final Reader reader) {
    source.accept(reader);
  }

  // 获取 TokenStream
  public TokenStream getTokenStream() {
    return sink;
  }

  // 获取Reader
  public Consumer<Reader> getSource() {
    return source;
  }
}

ReuseStrategy

ReuseStrategy决定了业务通过Analyzer获取指定字段的TokenStreamComponents的复用策略。

java 复制代码
public abstract static class ReuseStrategy {
  protected ReuseStrategy() {}

  // 获取fieldName字段的可复用的TokenStreamComponents
  public abstract TokenStreamComponents getReusableComponents(
      Analyzer analyzer, String fieldName);

  // 为fieldName字段设置一个可复用的TokenStreamComponents
  public abstract void setReusableComponents(
      Analyzer analyzer, String fieldName, TokenStreamComponents components);

  // storeValue就是所有字段的公用的可复用的TokenStreamComponents的存储的地方
  protected final Object getStoredValue(Analyzer analyzer) {
    if (analyzer.storedValue == null) {
      throw new AlreadyClosedException("this Analyzer is closed");
    }
    return analyzer.storedValue.get();
  }

  // storeValue就是所有字段的公用的可复用的TokenStreamComponents的存储的地方
  protected final void setStoredValue(Analyzer analyzer, Object storedValue) {
    if (analyzer.storedValue == null) {
      throw new AlreadyClosedException("this Analyzer is closed");
    }
    analyzer.storedValue.set(storedValue);
  }
}

Lucene中自带有两种实现,一种是所有的字段的可复用的TokenStreamComponents都一样,另一种是每个字段一个可复用的TokenStreamComponents。

GLOBAL_REUSE_STRATEGY

java 复制代码
// 所有的字段共享一个可复用的TokenStreamComponents
// storeValue就是TokenStreamComponents
public static final ReuseStrategy GLOBAL_REUSE_STRATEGY =
    new ReuseStrategy() {

      @Override
      public TokenStreamComponents getReusableComponents(Analyzer analyzer, String fieldName) {
        return (TokenStreamComponents) getStoredValue(analyzer);
      }

      @Override
      public void setReusableComponents(
          Analyzer analyzer, String fieldName, TokenStreamComponents components) {
        setStoredValue(analyzer, components);
      }
    };

PER_FIELD_REUSE_STRATEGY

java 复制代码
// 每个字段一个可复用的TokenStreamComponents
// storeValue的结构就是个map,key是字段名,value是可复用的TokenStreamComponents
public static final ReuseStrategy PER_FIELD_REUSE_STRATEGY =
    new ReuseStrategy() {

      @SuppressWarnings("unchecked")
      @Override
      public TokenStreamComponents getReusableComponents(Analyzer analyzer, String fieldName) {
        Map<String, TokenStreamComponents> componentsPerField =
            (Map<String, TokenStreamComponents>) getStoredValue(analyzer);
        return componentsPerField != null ? componentsPerField.get(fieldName) : null;
      }

      @SuppressWarnings("unchecked")
      @Override
      public void setReusableComponents(
          Analyzer analyzer, String fieldName, TokenStreamComponents components) {
        Map<String, TokenStreamComponents> componentsPerField =
            (Map<String, TokenStreamComponents>) getStoredValue(analyzer);
        if (componentsPerField == null) {
          componentsPerField = new HashMap<>();
          setStoredValue(analyzer, componentsPerField);
        }
        componentsPerField.put(fieldName, components);
      }
    };

Analyzer

Analyzer中使用模板模式,固定了字段输入是Reader或者字符串的处理方式,把TokenStreamComponents的具体创建作为抽象类由子类去实现,不同的业务逻辑自己创建不同的TokenStreamComponents,其实重点就是创建TokenStreamComponents中的TokenStream。

java 复制代码
public abstract class Analyzer implements Closeable {
  // 复用策略
  private final ReuseStrategy reuseStrategy;

  // 可复用的  TokenStreamComponents 
  CloseableThreadLocal<Object> storedValue = new CloseableThreadLocal<>();

  protected Analyzer() {
    this(GLOBAL_REUSE_STRATEGY);
  }

  protected Analyzer(ReuseStrategy reuseStrategy) {
    this.reuseStrategy = reuseStrategy;
  }

  // 创建 TokenStreamComponents
  protected abstract TokenStreamComponents createComponents(String fieldName);

  // 可以给原始分词器通过装饰器加一些比如统一小写之类的标准化处理
  protected TokenStream normalize(String fieldName, TokenStream in) {
    return in;
  }


  public final TokenStream tokenStream(final String fieldName, final Reader reader) {
    // 查询是否有可复用的 TokenStreamComponents
    TokenStreamComponents components = reuseStrategy.getReusableComponents(this, fieldName);
    // 处理下reader  
    final Reader r = initReader(fieldName, reader);
    if (components == null) { // 如果不存在可复用的TokenStreamComponents就创建一个,并设置为可复用的
      components = createComponents(fieldName);
      reuseStrategy.setReusableComponents(this, fieldName, components);
    }
    // 需要更新下输入源  
    components.setReader(r);
    return components.getTokenStream();
  }


  public final TokenStream tokenStream(final String fieldName, final String text) {
    // 查询是否有可复用的 TokenStreamComponents  
    TokenStreamComponents components = reuseStrategy.getReusableComponents(this, fieldName);
    // 字符串类型的输入源,使用可复用的方式存储输入源
    final ReusableStringReader strReader =
        (components == null || components.reusableStringReader == null)
            ? new ReusableStringReader()
            : components.reusableStringReader;
    strReader.setValue(text);
    final Reader r = initReader(fieldName, strReader);
    if (components == null) {
      components = createComponents(fieldName);
      reuseStrategy.setReusableComponents(this, fieldName, components);
    }

    components.setReader(r);
    components.reusableStringReader = strReader;
    return components.getTokenStream();
  }


  // 可以对普通的输入源做一些处理
  protected Reader initReader(String fieldName, Reader reader) {
    return reader;
  }

  // 只对字符串的输入源做一些处理
  protected Reader initReaderForNormalization(String fieldName, Reader reader) {
    return reader;
  }
}

Attribute

在分词过程中,我们希望得到token的相关信息,每种信息都可以使用一种Attribute来记录,在分词的过程中,把关注的属性记录下来。当然,前提是分词器要支持这些属性的获取,至于分词器能提供哪些属性的获取,在分词器的实现时就已经决定了。

Attribute在lucene中是一个标记接口,我们直接看它的抽象类:

java 复制代码
public abstract class AttributeImpl implements Cloneable, Attribute {
  // 重置属性的值为默认值
  public abstract void clear();

  public void end() {
    clear();
  }

  // 字符串形式展示属性的信息,根据是否拼接属性类名称,格式有所区别
  public final String reflectAsString(final boolean prependAttClass) {
    final StringBuilder buffer = new StringBuilder();
    reflectWith(
        (attClass, key, value) -> {
          if (buffer.length() > 0) {
            buffer.append(',');
          }
          if (prependAttClass) {
            buffer.append(attClass.getName()).append('#');
          }
          buffer.append(key).append('=').append((value == null) ? "null" : value);
        });
    return buffer.toString();
  }

  // 子类实现了几种属性,就可以使用reflector把所有的属性信息都格式化
  public abstract void reflectWith(AttributeReflector reflector);
}

属性的创建:AttributeFactory

所有的属性的实现类都必须通过AttributeFactory来创建的,之所以严格要求属性必须是通过属性工厂来创建的,是为了统一入口方便做一些合法性的校验。

java 复制代码
public abstract class AttributeFactory {

  // 通过属性的接口来创建属性的实现类
  public abstract AttributeImpl createAttributeInstance(Class<? extends Attribute> attClass)
      throws UndeclaredThrowableException;

    
  private static final MethodHandles.Lookup lookup = MethodHandles.publicLookup();
  private static final MethodType NO_ARG_CTOR = MethodType.methodType(void.class);
  private static final MethodType NO_ARG_RETURNING_ATTRIBUTEIMPL =
      MethodType.methodType(AttributeImpl.class);    
  // 获取属性实现类的无参构造器
  static final MethodHandle findAttributeImplCtor(Class<? extends AttributeImpl> clazz) {
    try {
      return lookup.findConstructor(clazz, NO_ARG_CTOR).asType(NO_ARG_RETURNING_ATTRIBUTEIMPL);
    } catch (NoSuchMethodException | IllegalAccessException e) {
      throw new IllegalArgumentException(
          "Cannot lookup accessible no-arg constructor for: " + clazz.getName(), e);
    }
  }

  // 创建属性实现类  
  public abstract AttributeImpl createAttributeInstance(Class<? extends Attribute> attClass)
      throws UndeclaredThrowableException;

  // 默认的属性创建工厂
  public static final AttributeFactory DEFAULT_ATTRIBUTE_FACTORY = new DefaultAttributeFactory();

  // delegate: 备选的属性工厂
  // clazz: 要创建的属性
  public static <A extends AttributeImpl> AttributeFactory getStaticImplementation(
      AttributeFactory delegate, Class<A> clazz) {
    final MethodHandle constr = findAttributeImplCtor(clazz);
    return new StaticImplementationAttributeFactory<A>(delegate, clazz) {
      @Override
      @SuppressWarnings("unchecked")
      protected A createInstance() {
        try {
          final AttributeImpl impl = (AttributeImpl) constr.invokeExact();
          return (A) impl;
        } catch (Error | RuntimeException e) {
          throw e;
        } catch (Throwable e) {
          throw new UndeclaredThrowableException(e);
        }
      }
    };
  }
}

目前Lucene中有两种属性工厂的实现。

DefaultAttributeFactory

DefaultAttributeFactory是默认的属性工厂,DefaultAttributeFactory要求的属性满足的规则如下:

  • 属性接口必须继承Attribute接口
  • 属性的实现类必须是接口名+Impl
  • 属性的实现类必须存在public无参构造器
  • 属性的实现类必须继承AttributeImpl

可以看到DefaultAttributeFactory适用于某个Attribute和AttributeImpl一一对应的情况,当前版本lucene中实现只有一种不是,这种例外使用的就是另外一种工厂来创建,其他的属性都是按照这种规则来实现的:

java 复制代码
private static final class DefaultAttributeFactory extends AttributeFactory {
  // 属性实现类的无参构造器  
  private final ClassValue<MethodHandle> constructors =
      new ClassValue<MethodHandle>() {
        @Override
        protected MethodHandle computeValue(Class<?> attClass) {
          return findAttributeImplCtor(findImplClass(attClass.asSubclass(Attribute.class)));
        }
      };

  DefaultAttributeFactory() {}

  // 通过public的无参构造器创建属性的实现类
  public AttributeImpl createAttributeInstance(Class<? extends Attribute> attClass) {
    try {
      return (AttributeImpl) constructors.get(attClass).invokeExact();
    } catch (Error | RuntimeException e) {
      throw e;
    } catch (Throwable e) {
      throw new UndeclaredThrowableException(e);
    }
  }

  // 获取attClass的实现类,直接按照要求的规则,attClass全类名后面拼上 Impl  
  private Class<? extends AttributeImpl> findImplClass(Class<? extends Attribute> attClass) {
    try {
      return Class.forName(attClass.getName() + "Impl", true, attClass.getClassLoader())
          .asSubclass(AttributeImpl.class);
    } catch (ClassNotFoundException cnfe) {
      throw new IllegalArgumentException(
          "Cannot find implementing class for: " + attClass.getName(), cnfe);
    }
  }
}

StaticImplementationAttributeFactory

StaticImplementationAttributeFactory是针对属性实现类实现了多种的属性接口,目前lucene中的实现就一个:org.apache.lucene.analysis.tokenattributes.PackedTokenAttributeImpl,我们可以看下:

java 复制代码
public class PackedTokenAttributeImpl extends CharTermAttributeImpl
    implements TypeAttribute,
        PositionIncrementAttribute,
        PositionLengthAttribute,
        OffsetAttribute,
        TermFrequencyAttribute

这种情况下,就直接通过实现类的无参构造器创建。

java 复制代码
public abstract static class StaticImplementationAttributeFactory<A extends AttributeImpl>
    extends AttributeFactory {
  // 备选的工厂  
  private final AttributeFactory delegate;
  // 要创建的属性实现类  
  private final Class<A> clazz;

  protected StaticImplementationAttributeFactory(AttributeFactory delegate, Class<A> clazz) {
    this.delegate = delegate;
    this.clazz = clazz;
  }

  // 如果clazz继承attClass或者是attClass本身,则使用createInstance()来创建,否则使用备选的工厂创建
  public final AttributeImpl createAttributeInstance(Class<? extends Attribute> attClass) {
    return attClass.isAssignableFrom(clazz)
        ? createInstance()
        : delegate.createAttributeInstance(attClass);
  }

  protected abstract A createInstance();
}

属性的注册:AttributeSource

AttributeSource是用来管理所有的属性的,分词过程中关注的属性都是注册到AttributeSource中的。

AttributeSource我觉得最大的作用就是可以集中批量操作属性,比如清空重置等。源码中的实现是把所有属性串成一个链表,相关的批量操作都是遍历这个链表。这里的逻辑我是比较疑惑的,直接遍历集合不就行了,为什么要特意串成链表来遍历,目前还没想清楚。

还有一点需要注意的是,如果某个属性实现类实现了多种属性接口,则在attributes会存储每种接口的信息。

java 复制代码
public class AttributeSource {

  // 属性实现类链表中的节点
  public static final class State implements Cloneable {
    AttributeImpl attribute;
    State next;

    @Override
    public State clone() {
      State clone = new State();
      clone.attribute = attribute.clone();

      if (next != null) {
        clone.next = next.clone();
      }

      return clone;
    }
  }

  // 存储所有的属性接口及其实现类实例
  private final Map<Class<? extends Attribute>, AttributeImpl> attributes;
  // 存储所有的属性实现类及其实现类实例  
  private final Map<Class<? extends AttributeImpl>, AttributeImpl> attributeImpls;
  private final State[] currentState;

  private final AttributeFactory factory;

  public AttributeSource() {
    this(AttributeFactory.DEFAULT_ATTRIBUTE_FACTORY);
  }

  public AttributeSource(AttributeSource input) {
    Objects.requireNonNull(input, "input AttributeSource must not be null");
    this.attributes = input.attributes;
    this.attributeImpls = input.attributeImpls;
    this.currentState = input.currentState;
    this.factory = input.factory;
  }

  public AttributeSource(AttributeFactory factory) {
    this.attributes = new LinkedHashMap<>();
    this.attributeImpls = new LinkedHashMap<>();
    this.currentState = new State[1];
    this.factory = Objects.requireNonNull(factory, "AttributeFactory must not be null");
  }

  public final AttributeFactory getAttributeFactory() {
    return this.factory;
  }

  public final Iterator<Class<? extends Attribute>> getAttributeClassesIterator() {
    return Collections.unmodifiableSet(attributes.keySet()).iterator();
  }


  public final Iterator<AttributeImpl> getAttributeImplsIterator() {
    final State initState = getCurrentState();
    if (initState != null) {
      return new Iterator<AttributeImpl>() {
        private State state = initState;

        @Override
        public void remove() {
          throw new UnsupportedOperationException();
        }

        @Override
        public AttributeImpl next() {
          if (state == null) throw new NoSuchElementException();
          final AttributeImpl att = state.attribute;
          state = state.next;
          return att;
        }

        @Override
        public boolean hasNext() {
          return state != null;
        }
      };
    } else {
      return Collections.<AttributeImpl>emptySet().iterator();
    }
  }

  // 获取实现类的所有接口
  private static final ClassValue<Class<? extends Attribute>[]> implInterfaces =
      new ClassValue<Class<? extends Attribute>[]>() {
        @Override
        protected Class<? extends Attribute>[] computeValue(Class<?> clazz) {
          final Set<Class<? extends Attribute>> intfSet = new LinkedHashSet<>();
          // 获取整个继承体系中实现的所有接口
          do {
            for (Class<?> curInterface : clazz.getInterfaces()) {
              if (curInterface != Attribute.class
                  && Attribute.class.isAssignableFrom(curInterface)) {
                intfSet.add(curInterface.asSubclass(Attribute.class));
              }
            }
            // 再查父类实现的接口  
            clazz = clazz.getSuperclass();
          } while (clazz != null);
          final Class<? extends Attribute>[] a = intfSet.toArray(new Class[intfSet.size()]);
          return a;
        }
      };

  static Class<? extends Attribute>[] getAttributeInterfaces(
      final Class<? extends AttributeImpl> clazz) {
    return implInterfaces.get(clazz);
  }

  // 新增属性的实现类
  public final void addAttributeImpl(final AttributeImpl att) {
    final Class<? extends AttributeImpl> clazz = att.getClass();
    // 拒绝重复添加  
    if (attributeImpls.containsKey(clazz)) return;

    // 遍历属性类的所有接口
    for (final Class<? extends Attribute> curInterface : getAttributeInterfaces(clazz)) {
      // 是个新接口的话才新增
      if (!attributes.containsKey(curInterface)) {
        // 数据有更新则需要把currentState开始的链表无效化,这样下次再获取链表就重新构建
        this.currentState[0] = null;
        attributes.put(curInterface, att);
        attributeImpls.put(clazz, att);
      }
    }
  }

  // 新增属性
  public final <T extends Attribute> T addAttribute(Class<T> 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);
  }

  // 获取实现类的链表  
  private State getCurrentState() {
    State s = currentState[0];
    // 如果已经是一条链了,直接返回  
    if (s != null || !hasAttributes()) {
      return s;
    }
    // 创建头结点  
    State c = s = currentState[0] = new State();
    // 遍历所有的属性实现类,串成链表  
    final Iterator<AttributeImpl> it = attributeImpls.values().iterator();
    c.attribute = it.next();
    while (it.hasNext()) {
      c.next = new State();
      c = c.next;
      c.attribute = it.next();
    }
    return s;
  }
}

TokenStream

TokenStream是真正执行分词逻辑实现的,可以看到它继承了AttributeSource,所以可以通过TokenStream注册和获取相关的属性。

java 复制代码
public abstract class TokenStream extends AttributeSource implements Closeable {

  // PackedTokenAttributeImpl属性必须要用StaticImplementationAttributeFactory工厂来创建
  public static final AttributeFactory DEFAULT_TOKEN_ATTRIBUTE_FACTORY =
      AttributeFactory.getStaticImplementation(
          AttributeFactory.DEFAULT_ATTRIBUTE_FACTORY, PackedTokenAttributeImpl.class);

  protected TokenStream() {
    super(DEFAULT_TOKEN_ATTRIBUTE_FACTORY);
    assert assertFinal();
  }

  protected TokenStream(AttributeSource input) {
    super(input);
    assert assertFinal();
  }


  protected TokenStream(AttributeFactory factory) {
    super(factory);
    assert assertFinal();
  }

  // TokenStream的实现类必须是final的,或者 incrementToken 方法必须是final的
  private boolean assertFinal() {
    try {
      final Class<?> clazz = getClass();
      if (!clazz.desiredAssertionStatus()) return true;
      assert clazz.isAnonymousClass()
              || (clazz.getModifiers() & (Modifier.FINAL | Modifier.PRIVATE)) != 0
              || Modifier.isFinal(clazz.getMethod("incrementToken").getModifiers())
          : "TokenStream implementation classes or at least their incrementToken() implementation must be final";
      return true;
    } catch (
        @SuppressWarnings("unused")
        NoSuchMethodException nsme) {
      return false;
    }
  }

  // 这个方法调用,就是处理了一个token,并且把token的相关信息更新到注册的所有属性中
  public abstract boolean incrementToken() throws IOException;

  public void end() throws IOException {
    endAttributes(); 
  }

  public void reset() throws IOException {}

  @Override
  public void close() throws IOException {}
}

一个简单例子:WhitespaceAnalyzer

我们通过Lucene中一个简单的空格分词器来串联一下整个分词框架的实现逻辑。

使用demo

java 复制代码
public class WhitespaceAnalyzerDemo {
    public static void main(String[] args) throws IOException {
        Analyzer analyzer = new WhitespaceAnalyzer();

        TokenStream tokenStream = analyzer.tokenStream("filed", "one two three");
        tokenStream.reset();
        CharTermAttribute termAttr = tokenStream.getAttribute(CharTermAttribute.class);
        OffsetAttribute offsetAttr = tokenStream.getAttribute(OffsetAttribute.class);
        while(tokenStream.incrementToken()) {
            System.out.printf("token=%s startOffset=%d endOffset=%d\n", termAttr.toString(), offsetAttr.startOffset(), offsetAttr.endOffset());
        }
    }
}

输出:

ini 复制代码
token=one startOffset=0 endOffset=3
token=two startOffset=4 endOffset=7
token=three startOffset=8 endOffset=13

源码分析

WhitespaceAnalyzer是入口,负责创建TokenStream,空格分词的TokenStream实现类是WhitespaceTokenizer。

java 复制代码
public final class WhitespaceAnalyzer extends Analyzer {
  // 限制token的最大长度
  private final int maxTokenLength;

  public WhitespaceAnalyzer() {
    this(WhitespaceTokenizer.DEFAULT_MAX_WORD_LEN);
  }

  public WhitespaceAnalyzer(int maxTokenLength) {
    this.maxTokenLength = maxTokenLength;
  }

  // WhitespaceAnalyzer 创建的是 WhitespaceTokenizer
  protected TokenStreamComponents createComponents(final String fieldName) {
    return new TokenStreamComponents(new WhitespaceTokenizer(maxTokenLength));
  }
}

WhitespaceTokenizer继承了CharTokenizer,它是一个字符一个字符的处理输入来进行分词的,isTokenChar方法就是判断token的边界,判断当前字符是否属于正在处理的token。逻辑比较简单,就是判断当前字符是否是空格,所以WhitespaceTokenizer就是以空格来切分token。

java 复制代码
public final class WhitespaceTokenizer extends CharTokenizer {
  public WhitespaceTokenizer() {}

  public WhitespaceTokenizer(AttributeFactory factory) {
    super(factory);
  }

  public WhitespaceTokenizer(int maxTokenLen) {
    super(TokenStream.DEFAULT_TOKEN_ATTRIBUTE_FACTORY, maxTokenLen);
  }

  public WhitespaceTokenizer(AttributeFactory factory, int maxTokenLen) {
    super(factory, maxTokenLen);
  }

  // 空格切分
  protected boolean isTokenChar(int c) {
    return !Character.isWhitespace(c);
  }
}

CharTokenizer中我们只关注它能够得到的属性信息CharTermAttribute和OffsetAttribute,所以我们可以通过这两个属性获取每个token及其offset信息。在incrementToken方法中,就是按字符处理输入,根据isTokenChar和最大token长度来决定一个token的边界,在处理过程中更新token的CharTermAttribute和OffsetAttribute信息。

java 复制代码
public abstract class CharTokenizer extends Tokenizer {
  private int offset = 0, bufferIndex = 0, dataLen = 0, finalOffset = 0;
  public static final int DEFAULT_MAX_WORD_LEN = 255;
  private static final int IO_BUFFER_SIZE = 4096;
  private final int maxTokenLen;

  // 只关注下面这两个属性  
  private final CharTermAttribute termAtt = addAttribute(CharTermAttribute.class);
  private final OffsetAttribute offsetAtt = addAttribute(OffsetAttribute.class);

  private final CharacterBuffer ioBuffer = CharacterUtils.newCharacterBuffer(IO_BUFFER_SIZE);

  // 判断当前字符是不是属于目前正在处理的token
  protected abstract boolean isTokenChar(int c);

  @Override
  public final boolean incrementToken() throws IOException {
    // 重置所有属性的信息,准备更新为新token的信息  
    clearAttributes();
    int length = 0;
    int start = -1; 
    int end = -1;
    char[] buffer = termAtt.buffer();
    while (true) {
      if (bufferIndex >= dataLen) {
        offset += dataLen;
        CharacterUtils.fill(ioBuffer, input); 
        if (ioBuffer.getLength() == 0) {
          dataLen = 0; 
          if (length > 0) {
            break;
          } else {
            finalOffset = correctOffset(offset);
            return false;
          }
        }
        dataLen = ioBuffer.getLength();
        bufferIndex = 0;
      }
      // 获取当前的字符
      final int c = Character.codePointAt(ioBuffer.getBuffer(), bufferIndex, ioBuffer.getLength());
      final int charCount = Character.charCount(c);
      bufferIndex += charCount;

      if (isTokenChar(c)) { // 如果当前字符属于当前正在处理的token
        if (length == 0) { // 是token的第一个字符,则更新offset的start和end信息
          assert start == -1;
          start = offset + bufferIndex - charCount;
          end = start;
        } else if (length >= buffer.length - 1) { // 存储token的buffer不够了
          buffer = termAtt.resizeBuffer(2 + length);
        }
        // 更新end信息  
        end += charCount;
        length += Character.toChars(c, buffer, length); 
        // 最大长度的限制
        if (length >= maxTokenLen) {
          break;
        }
      } else if (length > 0) { 
        break; 
      }
    }

    // token属性,用长度来限制有效内容  
    termAtt.setLength(length);
    // offset属性  
    offsetAtt.setOffset(correctOffset(start), finalOffset = correctOffset(end));
    return true;
  }

  @Override
  public final void end() throws IOException {
    super.end();
    offsetAtt.setOffset(finalOffset, finalOffset);
  }

  @Override
  public void reset() throws IOException {
    super.reset();
    bufferIndex = 0;
    offset = 0;
    dataLen = 0;
    finalOffset = 0;
    ioBuffer.reset(); 
  }
}

总结

本文重点是解析分词的框架,并没有对某种具体的分词算法做分析,除非业务上有需求需要参考某种具体算法,否则目前是没有精力做分词算法的分析。

相关推荐
AI人H哥会Java2 小时前
【Spring】控制反转(IoC)与依赖注入(DI)—IoC容器在系统中的位置
java·开发语言·spring boot·后端·spring
凡人的AI工具箱2 小时前
每天40分玩转Django:Django表单集
开发语言·数据库·后端·python·缓存·django
奔跑草-3 小时前
【数据库】SQL应该如何针对数据倾斜问题进行优化
数据库·后端·sql·ubuntu
Elastic 中国社区官方博客3 小时前
如何通过 Kafka 将数据导入 Elasticsearch
大数据·数据库·分布式·elasticsearch·搜索引擎·kafka·全文检索
中國移动丶移不动3 小时前
Java 并发编程:原子类(Atomic Classes)核心技术的深度解析
java·后端
nece0013 小时前
elasticsearch 杂记
大数据·elasticsearch·搜索引擎
Q_19284999064 小时前
基于Spring Boot的旅游推荐系统
spring boot·后端·旅游
愤怒的代码4 小时前
Spring Boot对访问密钥加密解密——RSA
java·spring boot·后端
美美的海顿4 小时前
springboot基于Java的校园导航微信小程序的设计与实现
java·数据库·spring boot·后端·spring·微信小程序·毕业设计
愤怒的代码4 小时前
Spring Boot中幂等性的应用
java·spring boot·后端