有人能帮助我解决Lucene中的多词同义词问题吗?

3z6pesqy  于 2022-11-07  发布在  Lucene
关注(0)|答案(1)|浏览(288)

简单的同义词(wordA = wordB)就可以了。当同义词是一个短语(wordA = wordB**word C)时,则匹配是命中或不命中的。
我有一个简单的测试用例(它是作为一个Ant项目交付的)来说明这个问题。这个测试用例使用的文件和我今天发布的另一个问题相同,但是我在这里给予了相同的描述。
材料
您可以在此处下载测试用例:mydemo.with.libs.zip (5MB)
该归档文件包括我的测试使用的Lucene9.2库;如果您喜欢不包含JAR文件的副本,可以从以下位置下载:mydemo.zip (9KB)
您可以通过将归档文件解压缩到一个空目录并运行Ant命令**ant stsearch**来运行测试用例
输入
为文档编制索引时,将使用以下同义词列表(根据需要进行排列):

note,notes,notice,notification
subtree,sub tree,sub-tree

我有三份文件,每一份都只有一句话,这三句话是:

These release notes describe a document sub tree in a simple way.
This release note describes a document subtree in a simple way.
This release notice describes a document sub-tree in a simple way.

问题

我认为以下任何搜索都应与所有三个文档匹配:

subtree
sub tree
sub-tree
"document subtree"
"document sub tree"
"document sub-tree"

虽然对subtreesub-tree的搜索正确匹配,但对sub tree的搜索仅匹配单个文档(字面上包含sub tree作为两个单词的文档)。
短语搜索不正确:“文档子树”和“文档子树”各匹配一个,而“文档子树”匹配两个。
如果我在短语搜索中添加一个邻近修饰符,如下所示:

"document subtree"~1
"document sub tree"~1
"document sub-tree"~1

第一个和第三个现在匹配所有三个记录,但是“document subtree”~1仍然只匹配一个文档。
把一个两个单词的短语作为一个单词的同义词来配对是行不通的。
下面是我的分析器,包括同义词Map构建器:

public class MyAnalyzer extends Analyzer {
   public MyAnalyzer(String synlist) {
      this.synlist = synlist;
   }

   @Override
   protected TokenStreamComponents createComponents(String fieldName) {
      WhitespaceTokenizer src = new WhitespaceTokenizer();
      TokenStream result = new LowerCaseFilter(src);
      if (synlist != null) {
         result = new SynonymGraphFilter(result, getSynonyms(synlist), Boolean.TRUE);
         result = new FlattenGraphFilter(result);
      }
      return new TokenStreamComponents(src, result);
   }

   private static SynonymMap getSynonyms(String synlist) {
      boolean dedup = Boolean.TRUE;
      SynonymMap synMap = null;
      SynonymMap.Builder builder = new SynonymMap.Builder(dedup);
      int cnt = 0;

      try {
         BufferedReader br = new BufferedReader(new FileReader(synlist));
         String line;
         try {
            while ((line = br.readLine()) != null) {
               processLine(builder,line);
               cnt++;
            }
         } catch (IOException e) {
            System.err.println(" caught " + e.getClass() + " while reading synonym list,\n with message " + e.getMessage());
         }
         System.out.println("Synonym load processed " + cnt + " lines");
         br.close();
      } catch (Exception e) {
         System.err.println(" caught " + e.getClass() + " while loading synonym map,\n with message " + e.getMessage());
      }
      if (cnt > 0) {
         try {
            synMap = builder.build();
         } catch (IOException e) {
            System.err.println(e);
         }
      }
      return synMap;
   }

   private static void processLine(SynonymMap.Builder builder, String line) {
      boolean keepOrig = Boolean.TRUE;
      String terms[] = line.split(",");
      if (terms.length < 2) {
         System.err.println("Synonym input must have at least two terms on a line: " + line);
      } else {
         String word = terms[0];
         String[] synonymsOfWord = Arrays.copyOfRange(terms, 1, terms.length);
         addSyns(builder, word, synonymsOfWord, keepOrig);
      }
   }

   private static void addSyns(SynonymMap.Builder builder, String word, String[] syns, boolean keepOrig) {
      CharsRefBuilder synset = new CharsRefBuilder();
      SynonymMap.Builder.join(syns, synset);
      CharsRef wordp = SynonymMap.Builder.join(word.split("\\s+"), new CharsRefBuilder());
      builder.add(wordp, synset.get(), keepOrig);
   }

   private String synlist;
}

我怀疑我必须对synonymsOfWord数组做一些额外的操作,但是我所做的一切都没有效果。
请注意,分析器会在建立索引时包含同义字,而不是在执行查询时。

fjnneemd

fjnneemd1#

我不知道这是否是最好的解决办法,但这是一个解决办法。
它基本上是一种与answer to this related question非常相似的方法,但是在处理同义词方面进行了增强,其中一些同义词包含多个单词:

"subtree", "sub tree", "sub-tree"

在这种情况下,同义词构建器需要使用SynonymMap.html #WORD_SEPARATOR:

  • 对于多字支持,您必须用此分隔符分隔单词 *

它只是一个包含空终止符\u0000char
因此,你可以做一些快速和肮脏的如下:

String[] synonyms = {"sub tree", "sub-tree", "subtree"};
int len = synonyms.length;
String sep = Character.toString(SynonymMap.WORD_SEPARATOR);
String[] luceneSyns = new String[len];
for (int i = 0; i < len; i++) {
    luceneSyns[i] = synonyms[i].replaceAll(" ", sep).replaceAll("-", sep);
}

现在,luceneSyns成为我们使用的数组:

// build a synonym map where every word or phrase in the list is a synonym
// of every other word or phrase in the list:
SynonymMap.Builder synMapBuilder = new SynonymMap.Builder(dedup);
for (String word : luceneSyns) {
    for (String synonym : luceneSyns) {
        if (!synonym.equals(word)) {
            //System.out.println(word + " > " + synonym);
            synMapBuilder.add(new CharsRef(word), new CharsRef(synonym), includeOrig);
        }
    }
}

这很管用。
问题中列出的所有查询都将找到所有三个文档。
上面的方法并不漂亮--它假设您只需要将空格和破折号作为两个字符来处理,这两个字符需要用空终止符来替换。
另一种更健壮的方法可能是使用SynonymMap.Parser,它有一个parse()方法,用于将提供的同义词文本转换为短语同义词所需的文本。
这是一个抽象类,我不知道如何正确地实现analyze()方法--但这是我所得到的结果:
首先,我创建了MySynonymParser类:

import java.io.IOException;
import java.io.Reader;
import java.text.ParseException;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.synonym.SynonymMap;
import org.apache.lucene.util.CharsRef;
import org.apache.lucene.util.CharsRefBuilder;

public class MySynonymParser extends SynonymMap.Parser {

    private final boolean dedup;
    private final Analyzer analyzer;

    public MySynonymParser(boolean dedup, Analyzer analyzer) {
        this.dedup = dedup;
        this.analyzer = analyzer;
    }

    @Override
    public CharsRef analyze​(String text, CharsRefBuilder reuse) throws IOException {
        // implementation here
        return null;
    } 

    @Override
    public void parse(Reader reader) throws IOException, ParseException {
        throw new UnsupportedOperationException("Not supported yet."); 
    }

}

如前所述,所需的analyze​()方法缺少其实现。

  • 我假设该方法必须捕获所提供的输入字符串的分析输出,然后用空终止符替换任何空格-并以CharsRef的形式返回新字符串。

但假设它被正确实现,那么我假设它将被如下使用:

Analyzer analyzer = new Analyzer() {
    @Override
    protected Analyzer.TokenStreamComponents createComponents(String fieldName) {
        Tokenizer source = new StandardTokenizer();
        TokenStream tokenStream = source;
        tokenStream = new LowerCaseFilter(tokenStream);
        tokenStream = new ASCIIFoldingFilter(tokenStream);
        return new Analyzer.TokenStreamComponents(source, tokenStream);
    }
};
MySynonymParser mySynonymParser = new MySynonymParser(dedup, analyzer);
CharsRefBuilder charsRefBuilder = new CharsRefBuilder();
mySynonymParser.analyze(sep, charsRefBuilder);
// build a synonym map where every word in the list is a synonym
// of every other word in the list:
SynonymMap.Builder synMapBuilder2 = new SynonymMap.Builder(dedup);
for (String word : luceneSyns) {
    for (String synonym : luceneSyns) {
        if (!synonym.equals(word)) {
            synMapBuilder2.add(mySynonymParser.analyze(word, charsRefBuilder), 
                    mySynonymParser.analyze(synonym, charsRefBuilder), includeOrig);
        }
    }
}

在上面的代码中,我们必须创建一个分析器传递给MySynonymParser,这个分析器和we actually use for indexing一样,但是没有同义词过滤器。
然后我们分析每个wordsynonym,它们用空终止符替换所有空格。

相关问题