由于工作中需要做同义词,今天看了看solr的实现以及源码,记个笔记。我看的solr的版本是5.5.3.
在solr的schema.xml中(5.x的版本是managed-schema文件)已经有实例了,截图如下:
<fieldType name="text_general" class="solr.TextField" positionIncrementGap="100"> <!-- 切记,只有TextField中才可以设置type=index或者type=query的两个analyzer,我看过源码,其他的时候不起作用--> <analyzer type="index"> <tokenizer class="solr.StandardTokenizerFactory"/> <filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt" /> <!-- in this example, we will only use synonyms at query time <filter class="solr.SynonymFilterFactory" synonyms="index_synonyms.txt" ignoreCase="true" expand="false"/> --> <filter class="solr.LowerCaseFilterFactory"/> </analyzer> <analyzer type="query"> <tokenizer class="solr.StandardTokenizerFactory"/> <!--使用的分词器的工厂--> <filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt" /> <!--添加停止词过滤器工厂--> <filter class="solr.SynonymFilterFactory" synonyms="synonyms.txt" ignoreCase="true" expand="true"/> <!-- 添加同义词过滤器工厂 --> <filter class="solr.LowerCaseFilterFactory"/> <!--添加转化为小写的过滤器工厂--> </analyzer> </fieldType>
关键就是配置的SynonyFilterFactory,我们看看他的源码:
SynonymFilterFactory类时继承自TokenFilterFactory类,后者为所有的TokenFilter工厂的抽象类,上图中的LowerCaseFilterFactory也是继承自这个类。TokenFilterFactory这个抽象类最关键的是create(TokenStream)方法,即根据tokenizer的操作继续添加操作,这个倒是很容易理解,在TokenizerFactory中也有个create方法,不过是没有参数的,因为此时还没有生成tokenStream。
明白了TokenFilterFactory之后,再看一下SynonymFilterFactory类的结构,直接看他的构造方法吧:
public SynonymFilterFactory(Map<String,String> args) {//map即在配置中的参数,比如上面的synonyms,ignoreCase,expand
super(args);
ignoreCase = getBoolean(args, "ignoreCase", false);//ignoreCase表示再分词匹配的时候要不要忽略大小写
synonyms = require(args, "synonyms");//同义词词典的位置
format = get(args, "format");//解析同义词词典的时候使用的格式化对象,即怎么从同义词词典中解读同义词
expand = getBoolean(args, "expand", true);//这个也是在解读同义词词典的时候的参数,用语言不好描述,等用到再说
analyzerName = get(args, "analyzer");//这个是在词典表中读取一个字符串要进行分词,这个指定使用的分词器
tokenizerFactory = get(args, "tokenizerFactory");//这个和上面的意思一样,只不过使用的是工厂模式
if (analyzerName != null && tokenizerFactory != null) {
throw new IllegalArgumentException("Analyzer and TokenizerFactory can't be specified both: " +
analyzerName + " and " + tokenizerFactory);
}
。。。//忽略不重要的参数
}
然后我们看看对同义词词典的加载,在org.apache.lucene.analysis.synonym.SynonymFilterFactory.inform(ResourceLoader)方法中有个loadSynonyms方法,顾名思义,就是加载同义词词典表的方法
protected SynonymMap loadSynonyms(ResourceLoader loader, String cname, boolean dedup, Analyzer analyzer) throws IOException, ParseException {//第二个参数是使用的格式化对象的名字,第三个是加载表的时候要不要排除重复的,第四个是使用的分词器
CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT);
SynonymMap.Parser parser;
Class<? extends SynonymMap.Parser> clazz = loader.findClass(cname, SynonymMap.Parser.class);
try {
parser = clazz.getConstructor(boolean.class, boolean.class, Analyzer.class).newInstance(dedup, expand, analyzer);//这个是用来生成最终的SynonyMap,里面最关键的是一个FST和一个类似于HashMap的BytesRefHash.
} catch (Exception e) {
throw new RuntimeException(e);
}
List<String> files = splitFileNames(synonyms);//可以传多个同义词词典文件
for (String file : files) {
decoder.reset();
try (final Reader isr = new InputStreamReader(loader.openResource(file), decoder)) {//读取同义词词典文件
parser.parse(isr);//解析,将同义词进入fst,最关键的就是这个方法。
}
}
return parser.build();//建造一个SynonymMap
}
现在最关键的就是一个parser.parse方法了,这里的parser是SolrSynonymParser类,继承自Builder类,用于构造fst
public void parse(Reader in) throws IOException, ParseException {
LineNumberReader br = new LineNumberReader(in);//读取一行
try {
addInternal(br);//调用addInternal方法
。。。//去掉无用代码
}
private void addInternal(BufferedReader in) throws IOException {
String line = null;
while ((line = in.readLine()) != null) {
if (line.length() == 0 || line.charAt(0) == '#') {//注释的一行
continue; // ignore empty lines and comments
}
// TODO: we could process this more efficiently.
String sides[] = split(line, "=>");//根据=>分开,
if (sides.length > 1) { // 如果当前根据=>之后的个数大于1,即aa=>bb格式的同义词词典
if (sides.length != 2) {
throw new IllegalArgumentException("more than one explicit mapping specified on the same line");
}
String inputStrings[] = split(sides[0], ",");//左边的部分,用,分开,
CharsRef[] inputs = new CharsRef[inputStrings.length];
for (int i = 0; i < inputs.length; i++) {
inputs[i] = analyze(unescape(inputStrings[i]).trim(), new CharsRefBuilder());//对左边的部分使用置顶的分词器处理
}
//对右边的部分做和左边同样的操作
String outputStrings[] = split(sides[1], ",");
CharsRef[] outputs = new CharsRef[outputStrings.length];
for (int i = 0; i < outputs.length; i++) {
outputs[i] = analyze(unescape(outputStrings[i]).trim(), new CharsRefBuilder());
}
// these mappings are explicit and never preserve original
for (int i = 0; i < inputs.length; i++) {//循环左边的词,每一个词都添加所有的右边的词作为同义词,即如果配置的是a=>b,c,添加到synonymMap的是a->b,a->c,但是不会添加b->c,b->a,c->a。
for (int j = 0; j < outputs.length; j++) {
add(inputs[i], outputs[j], false);
}
}
} else {//这个是没有=>的形式,仅仅有a,b,c,这些组成同义词
String inputStrings[] = split(line, ",");
CharsRef[] inputs = new CharsRef[inputStrings.length];
for (int i = 0; i < inputs.length; i++) {
inputs[i] = analyze(unescape(inputStrings[i]).trim(), new CharsRefBuilder());
}
if (expand) {//通过if-else的对比expand的意思是要不要反方向的添加,比如a,b,c,如果不是expand,就会只记录b->a,c->a,不会记录a->b,a->c,b->c,c->b。
// all pairs
for (int i = 0; i < inputs.length; i++) {//这两个for循环,将形成所有的同义词对,比如上面的
for (int j = 0; j < inputs.length; j++) {
if (i != j) {
add(inputs[i], inputs[j], true);//add是添加到BytesRefHash的数组里面返回在数组中的位置,并记录到fst中,这样从fst中根据term获得时先获得的是在bytesRefHash的位置(可以是多个),然后再根据位置获得同义词,
}
}
}
} else {
// all subsequent inputs map to first one; we also add inputs[0] here
// so that we "effectively" (because we remove the original input and
// add back a synonym with the same text) change that token's type to
// SYNONYM (matching legacy behavior):
for (int i = 0; i < inputs.length; i++) {
add(inputs[i], inputs[0], false);//如果不是expand,那么只会将同义词映射到第一个词上。
}
}
}
}
}
至此已经搞懂了同义词的用法,不过对于fst还是没有涉及到,不过已经可以使用了 同义词了。还有问题,如何更新同义词词典呢,总不能要更新后重启吧,还有就是词典保留在solr中修改起来特别麻烦,如何能更加方便的动态修改词典呢,这个留在下一个博客中,只要稍微做些修改,就能实现动态的添加词典,动态的做同义词。