繁体   English   中英

Lucene 如果存在太多文档/类似文档,则索引查询找不到文档

[英]Lucene Index Query does not find document if too many documents/similar documents present

如果我这样创建文档:

{
    Document document = new Document();
    document.add(new TextField("id", "10384-10735", Field.Store.YES));
    submitDocument(document);
}
{
    Document document = new Document();
    document.add(new TextField("id", "10735", Field.Store.YES));
    submitDocument(document);
}

for (int i = 20000; i < 80000; i += 123) {
    Document otherDoc1 = new Document();
    otherDoc1.add(new TextField("id", String.valueOf(i), Field.Store.YES));
    submitDocument(otherDoc1);

    Document otherDoc2 = new Document();
    otherDoc2.add(new TextField("id", i + "-" + (i + 67), Field.Store.YES));
    submitDocument(otherDoc2);
}

意义:

  • 一个 ID 为10384-10735
  • id 为10735的一个(这是上一个文档 ID 的最后一部分)
  • 以及几乎任何 ID 的 975 个其他文件

然后使用以下方法编写它们:

final IndexWriterConfig luceneWriterConfig = new IndexWriterConfig(new StandardAnalyzer());
luceneWriterConfig.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND);

final IndexWriter luceneDocumentWriter = new IndexWriter(luceneDirectory, luceneWriterConfig);

for (Map.Entry<String, Document> indexDocument : indexDocuments.entrySet()) {
    final Term term = new Term(Index.UNIQUE_LUCENE_DOCUMENT_ID, indexDocument.getKey());
    indexDocument.getValue().add(new TextField(Index.UNIQUE_LUCENE_DOCUMENT_ID, indexDocument.getKey(), Field.Store.YES));

    luceneDocumentWriter.updateDocument(term, indexDocument.getValue());
}

luceneDocumentWriter.close();

现在索引已经写好了,我想执行一个查询,搜索 ID 10384-10735的文档。

我将通过两种方式执行此操作,使用 TermQuery 和带有 StandardAnalyzer 的 QueryParser:

System.out.println("term query:   " + index.findDocuments(new TermQuery(new Term("id", "10384-10735"))));

final QueryParser parser = new QueryParser(Index.UNIQUE_LUCENE_DOCUMENT_ID, new StandardAnalyzer());
System.out.println("query parser: " + index.findDocuments(parser.parse("id:\"10384 10735\"")));

在这两种情况下,我都希望文档出现。 但是,如果我运行查询,这就是结果:

term query:   []
query parser: []

这似乎很奇怪。 我进一步试验了一下,发现如果我减少文档数量删除条目10735 ,查询解析器查询现在可以成功找到文档:

term query:   []
query parser: [Document<stored,indexed,tokenized<id:10384-10735> stored,indexed,tokenized<uldid:10384-10735>>]

这意味着这有效:

{
    Document document = new Document();
    document.add(new TextField("id", "10384-10735", Field.Store.YES));
    submitDocument(document);
}

for (int i = 20000; i < 80000; i += 123) {
    Document otherDoc1 = new Document();
    otherDoc1.add(new TextField("id", String.valueOf(i), Field.Store.YES));
    submitDocument(otherDoc1);

    Document otherDoc2 = new Document();
    otherDoc2.add(new TextField("id", i + "-" + (i + 67), Field.Store.YES));
    submitDocument(otherDoc2);
}

这有效(490 份文件)

{
    Document document = new Document();
    document.add(new TextField("id", "10384-10735", Field.Store.YES));
    submitDocument(document);
}
{
    Document document = new Document();
    document.add(new TextField("id", "10735", Field.Store.YES));
    submitDocument(document);
}

for (int i = 20000; i < 50000; i += 123) {
    Document otherDoc1 = new Document();
    otherDoc1.add(new TextField("id", String.valueOf(i), Field.Store.YES));
    submitDocument(otherDoc1);

    Document otherDoc2 = new Document();
    otherDoc2.add(new TextField("id", i + "-" + (i + 67), Field.Store.YES));
    submitDocument(otherDoc2);
}

有人知道这是什么原因吗? 我真的需要索引来始终如一地找到文档。 我可以使用 QueryParser 而不是 TermQuery。

我使用 9.3.0 lucene-core 和 lucene-queryparser。

提前谢谢你的帮助。

编辑 1 :这是 findDocuments() 中的代码:

final TopDocs topDocs = getIndexSearcher().search(query, Integer.MAX_VALUE);

final List<Document> documents = new ArrayList<>((int) topDocs.totalHits.value);
for (int i = 0; i < topDocs.totalHits.value; i++) {
    documents.add(getIndexSearcher().doc(topDocs.scoreDocs[i].doc));
}

return documents;

编辑2 :这是一个工作示例: https://pastebin.com/Ft0r8pN5

出于某种原因,文档过多的问题并没有发生在这个问题中,我将对此进行研究。 我仍然将其留作示例。 这是我的 output:

[similar id: true, many documents: true]
Indexing [3092] documents
term query:   []
query parser: []

[similar id: true, many documents: false]
Indexing [654] documents
term query:   []
query parser: []

[similar id: false, many documents: true]
Indexing [3091] documents
term query:   []
query parser: [Document<stored,indexed,tokenized<id:10384-10735> stored,indexed,tokenized<uldid:10384-10735>>]

[similar id: false, many documents: false]
Indexing [653] documents
term query:   []
query parser: [Document<stored,indexed,tokenized<id:10384-10735> stored,indexed,tokenized<uldid:10384-10735>>]

如您所见,如果将 ID 为10735的文档添加到文档中,则无法再找到该文档。

乍一看,一个可能的解决方案是:
带有作为第一个参数传递的术语的updateDocument()方法当前用于构建索引。 当将null作为术语传递或使用addDocument()方法时,查询成功返回了正确的值。 解决方案必须与Term相关。

luceneDocumentWriter.addDocument(indexDocument.getFields());
// or
luceneDocumentWriter.updateDocument(null, indexDocument);

再玩一点:存储相关文档的术语的键不能再次用作文档内的字段键,否则文档将变得不可搜索:

final Term term = new Term("uldid", indexDocument.get("id"));

// would work, different key from term...
indexDocument.add(new TextField("uldid2", indexDocument.get("id"), Field.Store.YES));

// would not work...
indexDocument.add(new TextField("uldid", indexDocument.get("id"), Field.Store.YES));

// ...when adding to index using term
luceneDocumentWriter.updateDocument(term, indexDocument);

避免这种情况的另一种方法是使用文档中相同字段的不同值(在本例中为uldid ),这也与在索引中搜索的 ID 不同:

final Term term = new Term("uldid", indexDocument.get("id").hashCode() + "");
// or
indexDocument.add(new TextField("uldid", indexDocument.get("id").hashCode() + "", Field.Store.YES));

这似乎很奇怪。 我真的没有最终的解决方案或原因,但从现在开始我将使用第二个选项,使用 hash 的任何键,我想将文档存储为Term

概括

问题是由 (a) 处理您的文件的顺序引起的; (b) updateDocument首先删除然后在索引中插入数据的事实。

当您使用writer.updateDocument(term, document)时,Lucene 执行原子删除然后添加:

通过首先删除包含术语的文档然后添加新文档来更新文档。

在您的情况下,处理文档的顺序取决于如何从您的 Java Map检索它们 - 这取决于 Z1D78DC8ED51214E518ZB5114FE24490AE 如何对条目进行哈希处理。

正如您在回答中指出的那样,您已经有办法通过使用 Java object 哈希作为updateDocument术语来避免这种情况。 (只要您没有遇到任何 hash 碰撞。)

该答案试图解释您所看到的结果背后的“原因”。


基本演示

这是您的代码的高度简化版本。

考虑以下两个 Lucene 文档:

final Document documentA = new Document();
documentA.add(new TextField(FIELD_NAME, "10735", Field.Store.YES));
final Term termA = new Term(FIELD_NAME, "10735");
writer.updateDocument(termA, documentA);
            
final Document documentB = new Document();
documentB.add(new TextField(FIELD_NAME, "10384-10735", Field.Store.YES));
final Term termB = new Term(FIELD_NAME, "10384-10735");
writer.updateDocument(termB, documentB);

文件A然后是文件B:

Lucene 在添加documentA时没有什么可删除的。 添加文档后,索引包含以下内容:

field id
  term 10735
    doc 0
      freq 1
      pos 0

所以,我们只有一个令牌10735

对于documentB ,索引中没有包含术语10384-10735的文档 - 因此在将documentB添加到索引之前不会删除任何内容。

我们最终得到以下最终索引数据:

field id
  term 10384
    doc 1
      freq 1
      pos 0
  term 10735
    doc 0
      freq 1
      pos 0
    doc 1
      freq 1
      pos 1

当我们搜索10384时,正如预期的那样,我们得到了一个结果。

文件B 然后是文件A:

如果我们交换 2 个文档的处理顺序,我们会在documentB被索引后看到以下内容:

field id
  term 10384
    doc 0
      freq 1
      pos 0
  term 10735
    doc 0
      freq 1
      pos 1

documentA被索引时,Lucene 发现索引中的doc 0 (上图)确实包含documentA使用的术语10735 因此,在添加documentA之前,会从索引中删除所有doc 0条目。

我们最终得到以下索引数据(基本上是一个新的doc 0 ,在原始doc 0被删除之后):

field id
  term 10735
    doc 0
      freq 1
      pos 0

现在,当我们搜索10384时,我们得到零命中——这不是我们所期望的。


更复杂的演示

通过使用 Java Map来收集要索引的文档,在您的问题场景中事情变得更加复杂。 由于 map 执行散列,这会导致 Lucene 文档的索引顺序与创建它们的顺序不同。

这是您的代码的另一个简化版本,但这次它使用 map:

public class MyIndexBuilder {

    private static final String INDEX_PATH = "index";
    private static final String FIELD_NAME = "id";

    private static final Map<String, Document> indexDocuments = new HashMap<>();

    public static void buildIndex() throws IOException, FileNotFoundException, ParseException {
        final Directory dir = FSDirectory.open(Paths.get(INDEX_PATH));

        Analyzer analyzer = new StandardAnalyzer();

        IndexWriterConfig iwc = new IndexWriterConfig(analyzer);
        iwc.setOpenMode(OpenMode.CREATE);
        //iwc.setCodec(new SimpleTextCodec());

        try ( IndexWriter writer = new IndexWriter(dir, iwc)) {
            
            String suffix = "10429";
            
            Document document1 = new Document();
            document1.add(new TextField("id", "10001-" + suffix, Field.Store.YES));
            indexDocuments.put("10001-" + suffix, document1);
            
            Document document2 = new Document();
            document2.add(new TextField("id", suffix, Field.Store.YES));
            indexDocuments.put(suffix, document2);
            
            //int max = 10193; // OK
            int max = 10192; // not OK
            
            for (int i = 10003; i <= max; i += 1) {
                Document otherDoc1 = new Document();
                otherDoc1.add(new TextField(FIELD_NAME, String.valueOf(i), Field.Store.YES));
                indexDocuments.put(String.valueOf(i), otherDoc1);
            }

            System.out.println("Total docs: " + indexDocuments.size());
            for (Map.Entry<String, Document> indexDocument : indexDocuments.entrySet()) {
                if (indexDocument.getKey().contains(suffix)) {
                    // show the order in which the document1 and document2 are indexed:
                    System.out.println(indexDocument.getKey());
                }
                final Term term = new Term(FIELD_NAME, indexDocument.getKey());
                writer.updateDocument(term, indexDocument.getValue());
            }
            
        }
    }

}

除了我们感兴趣的两个文档之外,我还在索引中添加了 191 个额外的(完全不相关的)文档。

当我处理 map 时,我看到以下 output:

Total docs: 193
10429
10001-10429

因此, document2document1之前被索引 - 我们对10001的搜索会找到一个命中。

但是,如果我处理更少的这些“额外”文档(190 个而不是 191 个):

int max = 10192; // not OK

...然后我得到这个 output:

Total docs: 192
10001-10429
10429

您可以看到document1document2的处理顺序已经颠倒了 - 现在对10001的相同搜索发现零命中。

一个看似不相关的更改(处理少一个文档)导致从 map 的检索顺序发生更改,从而导致索引数据不同。

(当我注意到索引数据显然相同时,我在问题中的一个评论中是不正确的。它不一样。当我第一次查看索引数据时,我错过了。)


推荐

考虑在您的 Lucene 文档中添加一个新字段,用于存储每个文档的唯一标识符。

您可以将其doc_id并将其创建为StringField ,而不是TextField

这将确保该字段的内容永远不会被标准分析器处理并作为单个(可能是唯一的)令牌存储在索引中。 StringField已编入索引但未标记化。

然后,您可以在构建要在updateDocument()方法中使用的术语时使用此字段。 您可以使用现有的id字段进行搜索。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM