Apache Solr自定义LemmaTokenFilter如何获取字段完整字符串值?
你遇到的核心问题其实是Lucene TokenFilter设计上的常见限制:TokenFilterFactory是全局初始化的,它的create方法没办法直接拿到当前处理字段的完整文本——因为每个字段的TokenStream都是独立生成的,而且流式处理的特性也意味着你没法在Factory阶段提前获取所有token来拼接文本。
我结合你的需求,给你提供两种可行的解决方案,都是围绕在Analyzer层面捕获字段原始文本来实现批量调用API的,避免逐个token调用的低效问题:
方案一:自定义Analyzer直接处理原始文本
这是最直接的方式,因为Analyzer负责将原始文本转换成TokenStream,我们可以在这里先拿到完整文本,调用API生成词形映射,再传给你的LemmaTokenFilter。
修改你的代码
你的LemmaTokenFilter和LemmaTokenFilterFactory其实不需要大改,重点是实现一个自定义Analyzer:
import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.Tokenizer; import org.apache.lucene.analysis.core.StandardTokenizer; import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.util.Map; public class LemmaAnalyzer extends Analyzer { @Override public TokenStream tokenStream(String fieldName, Reader reader) { try { // 第一步:读取字段的完整原始文本 String originalText = readFullText(reader); // 第二步:调用API批量获取词形映射 Map<String, String> lemmaMap = UdPipeService.getLemma(originalText); // 第三步:重新包装文本(因为我们已经读取了reader的内容),创建Tokenizer Tokenizer tokenizer = new StandardTokenizer(); // 替换成你实际用的Tokenizer tokenizer.setReader(new StringReader(originalText)); // 第四步:把映射传给你的LemmaTokenFilter,构建TokenStream链 return new LemmaTokenFilter(tokenizer, lemmaMap); } catch (IOException e) { throw new RuntimeException("Failed to process text for lemmatization", e); } } // 工具方法:读取Reader的全部内容 private String readFullText(Reader reader) throws IOException { StringBuilder sb = new StringBuilder(); char[] buffer = new char[1024]; int len; while ((len = reader.read(buffer)) != -1) { sb.append(buffer, 0, len); } return sb.toString(); } }
为什么这个方案可行?
tokenStream方法每次处理字段时都会被调用,直接接收当前字段的原始文本Reader,我们可以一次性读取完整内容,批量调用API生成映射。- 读取完文本后,重新包装成
StringReader传给Tokenizer,保证TokenStream能正常处理文本。 - 完美避免了逐个token调用API的低效问题,也不需要破坏TokenStream的流式状态。
方案二:自定义Attribute传递字段文本(进阶)
如果你不想完全替换Analyzer,也可以通过自定义Attribute来在TokenStream中传递字段完整文本,然后在LemmaTokenFilter的reset方法中生成映射:
步骤1:创建自定义Attribute
用来存储字段的完整文本:
import org.apache.lucene.util.Attribute; public interface FieldTextAttribute extends Attribute { void setFieldText(String text); String getFieldText(); }
实现这个Attribute:
import org.apache.lucene.util.AttributeImpl; import org.apache.lucene.util.AttributeReflector; public class FieldTextAttributeImpl extends AttributeImpl implements FieldTextAttribute { private String fieldText; @Override public void setFieldText(String text) { this.fieldText = text; } @Override public String getFieldText() { return fieldText; } @Override public void clear() { fieldText = null; } @Override public void copyTo(AttributeImpl target) { ((FieldTextAttribute) target).setFieldText(fieldText); } @Override public void reflectWith(AttributeReflector reflector) { reflector.reflect(FieldTextAttribute.class, "fieldText", fieldText); } }
步骤2:修改LemmaTokenFilter
在reset方法中获取字段文本并生成映射:
import org.apache.lucene.analysis.TokenFilter; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; import java.io.IOException; import java.util.Collections; import java.util.Map; public final class LemmaTokenFilter extends TokenFilter { private final CharTermAttribute termAtt = addAttribute(CharTermAttribute.class); private final FieldTextAttribute fieldTextAtt = addAttribute(FieldTextAttribute.class); private Map<String, String> lemmaMap; protected LemmaTokenFilter(TokenStream input) { super(input); } @Override public void reset() throws IOException { super.reset(); // 在reset阶段获取字段文本,生成词形映射 String fieldText = fieldTextAtt.getFieldText(); this.lemmaMap = (fieldText != null) ? UdPipeService.getLemma(fieldText) : Collections.emptyMap(); } @Override public boolean incrementToken() throws IOException { if (input.incrementToken()) { String term = termAtt.toString(); String lemma = lemmaMap.get(term); if (lemma != null) { termAtt.setEmpty(); termAtt.copyBuffer(lemma.toCharArray(), 0, lemma.length()); } return true; } else { return false; } } }
步骤3:在Analyzer中设置Attribute值
@Override public TokenStream tokenStream(String fieldName, Reader reader) { try { String originalText = readFullText(reader); Tokenizer tokenizer = new StandardTokenizer(); tokenizer.setReader(new StringReader(originalText)); TokenStream tokenStream = tokenizer; // 添加自定义Attribute并设置字段文本 FieldTextAttribute fieldTextAtt = tokenStream.addAttribute(FieldTextAttribute.class); fieldTextAtt.setFieldText(originalText); // 添加LemmaTokenFilter return new LemmaTokenFilter(tokenStream); } catch (IOException e) { throw new RuntimeException("Failed to process text for lemmatization", e); } }
为什么不推荐在TokenFilterFactory中获取字段值?
TokenFilterFactory是在Analyzer初始化时创建的(比如Solr配置加载阶段),它的create方法只是负责生成TokenFilter实例,此时并没有具体的字段文本传入——每个字段的TokenStream都是独立生成的,Factory本身无法感知到当前处理的是哪个字段、什么文本,这是Lucene组件设计上的固有限制。
额外优化建议
- 缓存API结果:如果多个字段有重复文本,可以缓存
lemmaMap,避免重复调用API,大幅提升效率。 - 处理大文本:如果字段文本特别大,一次性读取可能占用过多内存,可以考虑分块调用API,但要注意词形还原的上下文依赖(有些API需要完整上下文才能准确还原)。
内容的提问来源于stack exchange,提问作者Norbert Bodnar

