Lucene 7.3自定义Tokenizer报错:TokenStream契约违反问题求助
首先,这个异常的核心是你没有遵守Lucene TokenStream的生命周期规则——Lucene要求TokenStream必须严格遵循reset() → 多次调用incrementToken()直到返回false → close()的流程,而且子类必须正确调用父类的reset()和close()方法。结合你的代码,我帮你拆解问题并给出修复方案:
一、修复SentenceTokenizer的核心问题
1. 构造方法不符合新版Lucene规范
新版Lucene的Tokenizer不再允许随意通过setReader设置输入,正确的做法是通过父类构造器传入Reader:
// 修改构造方法,直接传入Reader和检测器 public SentenceTokenizer(Reader reader, SentenceDetector detector) { super(reader); // 父类构造器初始化输入流 this.sentenceDetector = detector; }
如果是Solr环境需要无参构造,你可以保留但要确保在setReader后完成初始化,不过更推荐明确传入Reader的方式,避免状态混乱。
2. reset()方法错误销毁核心实例
你的reset()把sentenceDetector置为null,导致第二次调用reset()后分词器彻底失效。正确的reset应该重置分词状态,而不是销毁检测器:
@Override public void reset() throws IOException { super.reset(); // 必须调用父类reset,重置输入流的状态 // 只重置分词相关的临时状态 this.sentenceSpans = null; this.tokenOffset = 0; this.inputSentence = null; }
3. 配合reset状态完善fillSentences逻辑
你的fillSentences()一次性读取全量文本用于分句是合理的,只要确保每次reset()后sentenceSpans会被置为null,incrementToken()就会重新调用fillSentences()读取新的输入流——这部分逻辑你已经具备,配合上面的reset修复即可正常工作。
二、修复NameFilter的问题
1. 不要在TokenFilter中持有独立的Tokenizer实例
NameFilter作为TokenFilter,它的上游已经是一个TokenStream(也就是你的SentenceTokenizer),不需要自己再持有SimpleTokenizer.INSTANCE。你的fillSpans()方法错误地用自己的Tokenizer重新分词,应该直接处理上游传来的句子token。
2. 补充reset()方法,遵守契约
你没给全NameFilter的代码,但必须给它添加正确的reset()实现,重置所有状态并调用父类方法:
@Override public void reset() throws IOException { super.reset(); // 重置所有自定义状态 this.spans = null; this.spanOffsets = 0; this.tokenQueue.clear(); this.text = null; this.baseOffset = 0; this.foundNames = null; this.tokenTypes = null; }
3. 完善incrementToken()的逻辑
从你给出的代码片段看,incrementToken()在处理tokenQueue时逻辑不完整,要确保:
- 当队列空时,调用
fillSpans()从上游获取新的句子 - 处理完所有span后,正确返回false
- 不要重复调用
input.incrementToken()而不保存状态
三、通用规则提醒
所有自定义的TokenStream(Tokenizer/TokenFilter)必须遵守:
- 必须在
reset()中调用super.reset(),在close()中调用super.close() - 不要在构造方法中读取输入流,所有输入操作应该放在
reset()或incrementToken()中 - 确保TokenStream的状态是可重置的——Lucene会复用TokenStream实例,所以每次reset后必须回到初始状态
内容的提问来源于stack exchange,提问作者Bibek Shakya

