【问题标题】:Date range query using Lucene spatial search / DateRangePrefixTree?使用 Lucene 空间搜索/DateRangePrefixTree 进行日期范围查询?
【发布时间】:2017-06-03 12:22:13
【问题描述】:

我使用的是 Lucene 6.3,但我无法弄清楚以下非常基本的搜索查询有什么问题。它只是向每个文档添加一个日期范围,然后尝试在更大的范围内搜索应该找到两个文档。怎么了?

有内联 cmets 应该使示例非常自我解释。如果有任何不清楚的地方,请告诉我。

请注意,我的主要要求是能够与其他字段查询一起执行日期范围查询,例如

text:interesting date:[2014 TO NOW]

这是在看了Lucene spatial deep dive video的介绍之后,介绍了DateRangePrefixTree和策略所基于的框架。

咆哮:感觉如果我在这里犯了任何错误,我应该得到一些验证错误,无论是在查询上还是在写作上,考虑到我的例子是多么简单。

import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.*;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.*;
import org.apache.lucene.spatial.prefix.NumberRangePrefixTreeStrategy;
import org.apache.lucene.spatial.prefix.PrefixTreeStrategy;
import org.apache.lucene.spatial.prefix.tree.DateRangePrefixTree;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.RAMDirectory;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.util.Calendar;
import java.util.Date;


public class TestLuceneDatePrefix {

  /*
  All these names should be lower case as field names are case sensitive in Lucene.
   */
  private static final String NAME = "name";
  public static final String TIME = "time";


  private Directory directory;
  private StandardAnalyzer analyzer;
  private ScoreDoc lastDocOnPage;
  private IndexWriterConfig indexWriterConfig;

  @Before
  public void setup() {
    analyzer = new StandardAnalyzer();
    directory = new RAMDirectory();
    indexWriterConfig = new IndexWriterConfig(analyzer);
  }


  @Test
  public void testAddDocumentAndSearchByDate() throws IOException {

    IndexWriter w = new IndexWriter(directory, new IndexWriterConfig(analyzer));

    // Responsible for creating the prefix string / geohash / token to identify the date.
    // aka Create post codes
    DateRangePrefixTree prefixTree = new DateRangePrefixTree(DateRangePrefixTree.JAVA_UTIL_TIME_COMPAT_CAL);

    // Strategy indexing the token.
    // aka transform post codes into tokens that make them efficient to search.
    PrefixTreeStrategy strategy = new NumberRangePrefixTreeStrategy(prefixTree, TIME);


    createDocument(w, "Bill", new Date(2017,1,1), prefixTree, strategy);
    createDocument(w, "Ted", new Date(2018,1,1), prefixTree, strategy);

    w.close();

    // Written the document, now try query them

    DirectoryReader reader;
    try {
      QueryParser queryParser = new QueryParser(NAME, analyzer);
      System.out.println(queryParser.getLocale());

      // Surely searching only on year for the easiest case should work?
      Query q = queryParser.parse("time:[1972 TO 4018]");

      // The following query returns 1 result, so Lucene is set up.
      // Query q = queryParser.parse("name:Ted");
      reader = DirectoryReader.open(directory);
      IndexSearcher searcher = new IndexSearcher(reader);

      TotalHitCountCollector totalHitCountCollector = new TotalHitCountCollector();

      int hitsPerPage = 10;
      searcher.search(q, hitsPerPage);

      TopDocs docs = searcher.search(q, hitsPerPage);
      ScoreDoc[] hits = docs.scoreDocs;

      // Hit count is zero and no document printed!!

      // Putting a dependency on mockito would make this code harder to paste and run.
      System.out.println("Hit count : "+hits.length);
      for (int i = 0; i < hits.length; ++i) {
        System.out.println(searcher.doc(hits[i].doc));
      }
      reader.close();
    }
    catch (ParseException e) {
      e.printStackTrace();
    }
  }


  private void createDocument(IndexWriter w, String name, Date fromDate, DateRangePrefixTree prefixTree, PrefixTreeStrategy strategy) throws IOException {
    Document doc = new Document();

    // Store a text/stored field for the name. This helps indicate that Lucene is orking.
    doc.add(new TextField(NAME, name, Field.Store.YES));

    //offset toDate
    Calendar cal = Calendar.getInstance();
    cal.setTime( fromDate );
    cal.add( Calendar.DATE, 1 );
    Date toDate = cal.getTime();

    // This lets the prefix tree create whatever tokens it needs
    // perhaps index year, date, second etc separately, hence multiple potential tokens.
    for (IndexableField field : strategy.createIndexableFields(prefixTree.toRangeShape(
        prefixTree.toUnitShape(fromDate), prefixTree.toUnitShape(toDate)))) {
      // Debugging the tokens produced is difficult as I can't intuitively look at them and know if they are valid.
      doc.add(field);
    }
    w.addDocument(doc);
  }
}

更新:

  • 与 StandardAnalyzer 相比,我认为答案可能是使用 SimpleAnalyzer,但这似乎也不起作用。

  • 我对能够解析用户日期范围的要求似乎确实是 catered by SOLR,所以我希望这基于 Lucene 功能。

【问题讨论】:

  • 我想也许答案是使用 SimpleAnalyzer 与 StandardAnalyzer 相比,但这似乎也不起作用。

标签: java date lucene spatial


【解决方案1】:

首先 QueryParser 可以解析日期并默认生成一个 TermRangeQuery。请参阅生成 TermRangeQuery 的默认解析器的以下方法。

org.apache.lucene.queryparser.classic.QueryParserBase#getRangeQuery(java.lang.String, java.lang.String, java.lang.String, boolean, boolean)

这假设您将在 lucene 数据库中存储日期作为字符串,这有点低效,但可以直接使用,前提是使用 SimpleAnalyzer 或等效工具。

或者,您可以将日期存储为 LongPoint,根据我上面的问题,这对于日期场景最有效,其中日期是时间点,每个字段存储一个日期。

Calendar fromDate = ...
doc.add(new LongPoint(FIELDNAME, fromDate.getTimeInMillis()));

但是这里就像为 DatePrefixTree 建议的那样,这需要编写硬编码查询。

Query pointRangeQueryHardCoded = LongPoint.newRangeQuery(FIELDNAME, fromDate.getTimeInMillis(), toDate.getTimeInMillis());

如果以下方法被生成 LongPoint 范围查询的版本覆盖,即使在这里也可以重用 QueryParser。

org.apache.lucene.queryparser.classic.QueryParserBase#newRangeQuery(java.lang.String, java.lang.String, java.lang.String, boolean, boolean)

这也可以用于 datePrefix 树版本,但这种方案只有在以下情况下才值得:

  • 您想通过一些不寻常的标记进行搜索(我相信它可以适应星期一)。
  • 每个文档字段有多个日期。
  • 您正在存储需要查询的日期范围。

调整查询解析器以使用方便的术语来捕获所有相关场景,我认为这对于最后一种情况来说是相当多的工作。

另外请小心不要将 Date(YEAR, MONTH, DAY) 与 GregorianCalendar(YEAR, MONTH, DAY) 混用,因为参数不相等会导致问题。

请参阅java.util.Date#Date(int, int, int),了解参数的不同之处以及不推荐使用此构造函数的原因。根据问题中的代码,这让我感到困惑。

再次感谢 femtoRgon 指出空间搜索的机制,但最终这不是我要走的路。

【讨论】:

    【解决方案2】:

    QueryParser 不会用于搜索空间字段,分析器也不会产生任何影响。分析器旨在标记和转换 文本。因此,它们不被空间领域使用。同样,QueryParser 主要针对文本搜索,不支持空间查询。

    您需要使用空间查询进行查询。特别是,AbstractPrefixTreeQuery 的子类将很有用。

    例如,如果我想查询其时间字段的范围包含 2003 - 2005 年的文档,我可以创建如下查询:

    Shape queryShape = prefixTree.toRangeShape(
        prefixTree.toUnitShape(new GregorianCalendar(2003,1,1)), 
        prefixTree.toUnitShape(new GregorianCalendar(2005,12,31)));
    
    Query q = new ContainsPrefixTreeQuery(
              queryShape,
              "time",
              prefixTree,
              10,
              false
      );
    

    因此这将匹配已被索引的文档,例如,范围为 2000-01-01 到 2006-01-01。

    或者反之,匹配所有范围完全查询范围内的文档:

    Shape queryShape = prefixTree.toRangeShape(
        prefixTree.toUnitShape(new GregorianCalendar(1990,1,1)), 
        prefixTree.toUnitShape(new GregorianCalendar(2020,12,31)));
    
    Query q = new WithinPrefixTreeQuery(
              queryShape,
              "time",
              prefixTree,
              10,
              -1,
              -1
      );
    

    关于参数的注意事项:我不太了解这些查询的某些参数,尤其是 detailLevel 和 prefixGridScanLevel。还没有找到任何关于它们如何工作的文档。这些值似乎适用于我的基本测试,但我不知道最佳选择是什么。

    【讨论】:

    • 感谢您的详细回答,这表明我期望 Lucene 会在查询解析方面完成繁重的工作是不正确的。我假设这是因为这似乎在 SOLR 中可用(添加了链接),我认为它会在 Lucene 层中编码。进一步咆哮:分析器 API 令人困惑,因为它被添加到共享索引编写器,而不是适用的英文文本字段。在我将其标记为正确之前,我需要测试您的解决方案并说服自己不存在替代方案。再次感谢。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-10-31
    • 2011-06-20
    相关资源
    最近更新 更多