以来自http://blog.csdn.net/itbasketplayer/article/details/40742117的NumberEnglishFilter为例子做剖析。NumberEnglishFilter主要是为了在索引时切分数字+字母,比如"2014show","2014"、"show"作为term,这样检索2014或者show都能检索出来。
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.lucene.analysis.TokenFilter;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.apache.lucene.analysis.tokenattributes.OffsetAttribute;
import org.apache.lucene.util.Version;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class NumberEnglishFilter extends TokenFilter {
private static final Logger logger = LoggerFactory.getLogger(NumberEnglishFilter.class);
private char[] curTermBuffer;
/* char[]总长度 */
private int curTermLength;
/* 当前切割处,+1表示下一次切割处 */
private int curGramSize;
/* number和english位置 */
private List < Integer > positions;
/* 相对位置,即在整个输入词的位置 */
private int tokStart;
/* positions所在的位置 */
private int position;
private final CharTermAttribute termAtt = addAttribute(CharTermAttribute.class);
/* 表示开始位置,结束位置 */
private final OffsetAttribute offsetAtt = addAttribute(OffsetAttribute.class);
/* private final TypeAttribute typeAtt = addAttribute(TypeAttribute.class); */
public NumberEnglishFilter(Version matchVersion, TokenStream input) {
super(input);
}@Override public boolean incrementToken() throws IOException {
while (true) {
/* 表示上一个token结束,进入下一个token */
if (curTermBuffer == null) {
if (!input.incrementToken()) {
return false;
} else {
curTermBuffer = termAtt.buffer().clone();
curTermLength = termAtt.length();
/* 放置position list */
positions = getPositions(curTermBuffer, curTermLength);
tokStart = offsetAtt.startOffset();
curGramSize = positions.get(0);
position = 0;
}
}
if (curGramSize < curTermLength && curTermLength > 1 && positions.size() > 2) {
try {
position++;
offsetAtt.setOffset(tokStart + curGramSize, tokStart + positions.get(position));
termAtt.copyBuffer(curTermBuffer, curGramSize, positions.get(position) - curGramSize);
curGramSize = positions.get(position);
return true;
} catch(Exception e) {
logger.error(position + "\t" + new String(curTermBuffer));
clearAttributes();
offsetAtt.setOffset(tokStart + 0, tokStart + curTermLength);
termAtt.copyBuffer(curTermBuffer, 0, curTermLength);
curTermBuffer = null;
return true;
}
} else {
clearAttributes();
offsetAtt.setOffset(tokStart + 0, tokStart + curTermLength);
termAtt.copyBuffer(curTermBuffer, 0, curTermLength);
curTermBuffer = null;
return true;
}
}
}
/* 记录下number和english位置 */
public List < Integer > getPositions(char[] term, int length) {
List < Integer > list = new ArrayList < Integer > ();
list.add(0);
for (int i = 0; i < length;) {
if (Character.isDigit(term[i])) {
while (++i < length && Character.isDigit(term[i]));
list.add(i);
continue;
} else if (i < length && Character.isLetter(term[i])) {
while (++i < length && Character.isLetter(term[i]));
list.add(i);
continue;
} else {
list.add(++i);
}
}
if (!Character.isDigit(term[length - 1]) && !Character.isLetter(term[length - 1])) list.add(length);
return list;
}
}
代码中,curTermBuffer用来存储上层传下来的每一个token(token初始存在termAtt中),当curTermBuffer为空时,说明上一个token已经处理完毕,因此需要通过input.incrementToken()从Token Stream里面获取一个新的token,存在curTermBuffer中,然后处理该token(curTermBuffer)得到token中数字和字母之间的切割点位置,存在positions数组中。接下来调用incrementToken()函数,根据positions数组,将token中的一个数字串或者一个字母串切割出来,存到termAtt中,然后返回true告诉上层继续调用incrementToken()函数;新一次调用中,代码检测到curTermBuffer非空,则直接根据positions数组,再次将token中的一个数字串或者一个字母串切割出来存到termAtt中,返回true让上层继续调用incrementToken()函数。一直重复这个过程直到curTermBuffer切割完毕,将最后一个数字串或者字母串存到termAtt中,然后将curTermBuffer置为空、返回true,好让下一次调用时代码去处理新的token(之前的每次调用都在处理上一个token)。
注意:
1.input.incrementToken()返回0表示Token Stream里面已经没有token,因此重写的incrementToken()函数直接就返回false告诉上层整个Token Stream处理完毕;返回true则表示当前还有token在处理,incrementToken()函数需要继续被上层调用。
2.每一次调用incrementToken()函数处理的是curTermBuffer,不等于每次处理的是新的token(处理完一个token可能需要多次函数的调用),处理的是否是新的token要看curTermBuffer有没有在该次调用中去获取新的token。
3.每一次调用incrementToken()函数,如果不返回false,则都会从当前token(存在curTermBuffer中)中处理出一个结果存在termAtt中、然后返回true,上层代码通过调用incrementToken()函数可以知道需不需要继续调用、并从termAtt中接收上一次调用的结果。可以说termAtt在solr tokenfilter处理一个token的过程中起到承上启下的作用:首先,需要处理的token先传到termAtt中,然后暂存到curTermBuffer;之后,原始的token内容就一直存在curTermBuffer中直到该该token处理完毕,而termAtt在该token处理的过程中起到输出结果的作用,每一次从token中处理出一个结果,就存在termAtt中,然后由上层接收(根据具体需求,一个token可能需要多次调用处理出多个结果,比如上述代码,一个token就需要处理出多个数字串、字母串,处理过程中每一次得到一个结果都存到termAtt中;注意,一次调用只允许你从token中处理出一个你想要的结果,如果你想从一个token中获得一组结果,那需要多次调用)。每一个token的处理过程中,termAtt中都依次扮演上述两个角色。
4.在Solr Tokenizer开发接口不熟悉的情况下,采用Solr自带的tokenizer——solr.KeywordTokenizerFactory(该tokenizer不会对solr doc的字段做任何处理,会将字段内容当作一个token传给后续的TokenFilter来处理),结合TokenFilter开发可以将tokenizer的功能也在tokenfilter里面完成(不过这种做法应该不大好)。