由于工作中需要做同义词,今天看了看solr的实现以及源码,记个笔记。我看的solr的版本是5.5.3.
在solr的schema.xml中(5.x的版本是managed-schema文件)已经有实例了,截图如下:
关键就是配置的SynonyFilterFactory,我们看看他的源码:
SynonymFilterFactory类时继承自TokenFilterFactory类,后者为所有的TokenFilter工厂的抽象类,上图中的LowerCaseFilterFactory也是继承自这个类。TokenFilterFactory这个抽象类最关键的是create(TokenStream)方法,即根据tokenizer的操作继续添加操作,这个倒是很容易理解,在TokenizerFactory中也有个create方法,不过是没有参数的,因为此时还没有生成tokenStream。
明白了TokenFilterFactory之后,再看一下SynonymFilterFactory类的结构,直接看他的构造方法吧:
public SynonymFilterFactory(Mapargs) {//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); } Listfiles = 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中修改起来特别麻烦,如何能更加方便的动态修改词典呢,这个留在下一个博客中,只要稍微做些修改,就能实现动态的添加词典,动态的做同义词。