目的:
最近项目中有用到lucene,第一次使用些开源工具记录下从中遇到的开发问题。
版本:
3.6
需求:
在创建索引时一个主字段可能对应多条索引记录,其它字段数据不同。如a(1,2,b(3,4))索引就要建成(1,2,3) (1,2,4)这样,查询时根据b中的条件可能查询出多条同一样的记录,所以要根据a的主字段去重。
问题:
lucene在执行query与filter时,是先执行filter把范围缩小后再执行query。我们去重用的是filter过滤器,先去重后再查询就己丢失了b中的部分数据,导致某些b的条件查询不出a
上述执行顺序可见 FilteredQuery类的getFilteredScorer方法:
static Scorer getFilteredScorer(final IndexReader indexReader, final Similarity similarity, final Weight weight, final Weight wrapperWeight, final Filter filter) throws IOException { assert filter != null; final DocIdSet filterDocIdSet = filter.getDocIdSet(indexReader); if (filterDocIdSet == null) { // this means the filter does not accept any documents. return null; } final DocIdSetIterator filterIter = filterDocIdSet.iterator(); if (filterIter == null) { // this means the filter does not accept any documents. return null; } // we are gonna advance() this scorer, so we set // inorder=true/toplevel=false final Scorer scorer = weight.scorer(indexReader, true, false); return (scorer == null) ? null : new Scorer(similarity, wrapperWeight) { private int scorerDoc = -1, filterDoc = -1; // optimization: we are topScorer and collect directly using // short-circuited algo @Override public void score(Collector collector) throws IOException { int filterDoc = filterIter.nextDoc(); int scorerDoc = scorer.advance(filterDoc); // the normalization trick already applies the boost of this // query, // so we can use the wrapped scorer directly: collector.setScorer(scorer); for (;;) { if (scorerDoc == filterDoc) { // Check if scorer has exhausted, only before // collecting. if (scorerDoc == DocIdSetIterator.NO_MORE_DOCS) { break; } // 这里作去重处理 collector.collect(scorerDoc); filterDoc = filterIter.nextDoc(); scorerDoc = scorer.advance(filterDoc); } else if (scorerDoc > filterDoc) { filterDoc = filterIter.advance(scorerDoc); } else { scorerDoc = scorer.advance(filterDoc); } } } private int advanceToNextCommonDoc() throws IOException { for (;;) { if (scorerDoc < filterDoc) { scorerDoc = scorer.advance(filterDoc); } else if (scorerDoc == filterDoc) { return scorerDoc; } else { filterDoc = filterIter.advance(scorerDoc); } } } @Override public int nextDoc() throws IOException { filterDoc = filterIter.nextDoc(); return advanceToNextCommonDoc(); } @Override public int advance(int target) throws IOException { if (target > filterDoc) { filterDoc = filterIter.advance(target); } return advanceToNextCommonDoc(); } @Override public int docID() { return scorerDoc; } @Override public float score() throws IOException { return scorer.score(); } }; }
解决:
由于从lucene源码中可以看到,查询那里没提供参数来改变filter与query的执行顺序。所以只好改动源码,在最后查询出的结果处自己去做数据的去重过滤。
涉及改动类:
IndexSearcher.java, HitQueue.java, FilteredQuery.java
说明:
这里HitQueue.java要重写是因为IndexSearcher.java被自定义,可源码中的HitQueue没被定义成public继而引用不了。这里只改动三个类是取巧了,以牺牲 性能为代价下面将会有说明,其实在本需求中要改动的远远不止这三个类。
IndexSearcher改动:
在查询时无论使用search方法还是searchAfter,最后源码跟踪都是执行以下代码 :
@Override public void search(Weight weight, Filter filter, Collector collector) throws IOException { // TODO: should we make this // threaded...? the Collector could be sync'd? // always use single thread: for (int i = 0; i < subReaders.length; i++) { // search each subreader collector.setNextReader(subReaders[i], docBase + docStarts[i]); final Scorer scorer = (filter == null) ? weight.scorer( subReaders[i], !collector.acceptsDocsOutOfOrder(), true) : FilteredQuery.getFilteredScorer(subReaders[i], getSimilarity(), weight, weight, filter); if (scorer != null) { scorer.score(collector); } //break; } }
所以我们要对此处进行改动,当然这只针对IndexSearcher中executor为空即没传入ExecutorService的 情况下(可自己跟踪源码查看)。先将关键处改动后的代码贴出:
@Override public void search(Weight weight, Filter filter, Collector collector) throws IOException { Map<String, String> idVersionKey = new HashMap<String, String>(); // TODO: should we make this // threaded...? the Collector could be sync'd? // always use single thread: for (int i = 0; i < subReaders.length; i++) { // search each subreader collector.setNextReader(subReaders[i], docBase + docStarts[i]); final Scorer scorer = (filter == null) ? weight.scorer( subReaders[i], !collector.acceptsDocsOutOfOrder(), true) : MyFilteredQuery.getFilteredScorer(subReaders[i], getSimilarity(), weight, weight, filter, this, idVersionKey); if (scorer != null) { scorer.score(collector); } // break; } }
FilteredQuery改动:
上面代码中己能看到FilteredQuery的改动,其方法为getFilteredScorer。可先将此方法复制一份,改动形参(只所以不去除老方法是因为此处还有其本能中的一个类部类在调用)。getFilteredScorer末改动源码在问题部份己贴出,现贴改动后的方法:
static Scorer getFilteredScorer(final IndexReader indexReader, final Similarity similarity, final Weight weight, final Weight wrapperWeight, final Filter filter, final MyIndexSearcher myIndexSearch, final Map<String, String> idVersionKey) throws IOException { assert filter != null; final DocIdSet filterDocIdSet = filter.getDocIdSet(indexReader); if (filterDocIdSet == null) { // this means the filter does not accept any documents. return null; } final DocIdSetIterator filterIter = filterDocIdSet.iterator(); if (filterIter == null) { // this means the filter does not accept any documents. return null; } // we are gonna advance() this scorer, so we set // inorder=true/toplevel=false final Scorer scorer = weight.scorer(indexReader, true, false); return (scorer == null) ? null : new Scorer(similarity, wrapperWeight) { private int scorerDoc = -1, filterDoc = -1; // optimization: we are topScorer and collect directly using // short-circuited algo @Override public void score(Collector collector) throws IOException { int filterDoc = filterIter.nextDoc(); int scorerDoc = scorer.advance(filterDoc); String key = null; // the normalization trick already applies the boost of this // query, // so we can use the wrapped scorer directly: collector.setScorer(scorer); for (;;) { if (scorerDoc == filterDoc) { // Check if scorer has exhausted, only before // collecting. if (scorerDoc == DocIdSetIterator.NO_MORE_DOCS) { break; } // 过滤去重 Document doc = myIndexSearch.doc(scorerDoc); key = doc.get("要去重的索引字段"); if (idVersionKey.get(key) == null) { collector.collect(scorerDoc); idVersionKey.put(key, "exist"); } filterDoc = filterIter.nextDoc(); scorerDoc = scorer.advance(filterDoc); } else if (scorerDoc > filterDoc) { filterDoc = filterIter.advance(scorerDoc); } else { scorerDoc = scorer.advance(filterDoc); } } } private int advanceToNextCommonDoc() throws IOException { for (;;) { if (scorerDoc < filterDoc) { scorerDoc = scorer.advance(filterDoc); } else if (scorerDoc == filterDoc) { return scorerDoc; } else { filterDoc = filterIter.advance(scorerDoc); } } } @Override public int nextDoc() throws IOException { filterDoc = filterIter.nextDoc(); return advanceToNextCommonDoc(); } @Override public int advance(int target) throws IOException { if (target > filterDoc) { filterDoc = filterIter.advance(target); } return advanceToNextCommonDoc(); } @Override public int docID() { return scorerDoc; } @Override public float score() throws IOException { return scorer.score(); } }; }
从代码
final DocIdSet filterDocIdSet = filter.getDocIdSet(indexReader); if (filterDocIdSet == null) { // this means the filter does not accept any documents. return null; }
片段中可看出,些处当过滤器为空时下面代码即不会再走,上面所说的“取巧”就是指这里。我们为了保证代码流程走到改动的代码中,所以要给我们的查询定义一个什么都不做的自定义过滤器。
当然,如果你们需求就是先过滤后查询,这里自定义过滤器就派上用场了,这里可以多自段过滤。
new Filter() { @Override public DocIdSet getDocIdSet(IndexReader reader) throws IOException { TermDocs termDocs = reader.termDocs(new Term(LuceneKey.MARK_ID, LuceneKey.MARK_VALUE)); final FixedBitSet bitSet = new FixedBitSet(reader.maxDoc()); final int[] docs = new int[32]; final int[] freqs = new int[32]; try { do { while (true) { final int count = termDocs.read(docs, freqs); if (count != 0) { for (int i = 0; i < count; i++) { bitSet.set(docs[i]); } } else { break; } } } while (termDocs.next()); } finally { termDocs.close(); } return bitSet; } }
这里顺便也贴出多字段自定义过滤器
new Filter() { @Override public DocIdSet getDocIdSet(IndexReader reader) throws IOException { TermDocs termDocs = reader.termDocs(new Term("这里只要是全部索引都有的字段就行,用于全部索引查询")); final IndexSearcher indexSearch = new IndexSearcher(reader); final FixedBitSet bitSet = new FixedBitSet(reader.maxDoc()); Map<String, String> idVersionKey = new HashMap<String, String>(); final int[] docs = new int[32]; final int[] freqs = new int[32]; String key = null; try { do { while (true) { final int count = termDocs.read(docs, freqs); if (count != 0) { for (int i = 0; i < count; i++) { Document doc = indexSearch.doc(docs[i]); key = doc.get("去重字段1") + doc.get("去重字段2"); bitSet.set(docs[i]); if (idVersionKey.get(key) == null) { bitSet.set(docs[i]); idVersionKey.put(key, "exist"); } } } else { break; } } } while (termDocs.next()); } finally { try { termDocs.close(); } finally { indexSearch.close(); } } return bitSet; } }
注:
Map<String, String> idVersionKey要从IndexSearcher类的search方法中传入到FilteredQuery类的getFilteredScorer中,是因为subReaders.length可能大于1,这里可能出现有次循环导致过滤结果不准确。出现这种现像的原因是,当你重新添加索引后而又末调用 writer.forceMerge(1),就会出现多个“虚拟子目录”。
for (int i = 0; i < subReaders.length; i++) { // search each subreader collector.setNextReader(subReaders[i], docBase + docStarts[i]); final Scorer scorer = (filter == null) ? weight.scorer( subReaders[i], !collector.acceptsDocsOutOfOrder(), true) : MyFilteredQuery.getFilteredScorer(subReaders[i], getSimilarity(), weight, weight, filter, this,idVersionKey); if (scorer != null) { scorer.score(collector); } //break; }
相关推荐
在大型数据集的搜索场景中,分页是必不可少的功能,它帮助用户逐步浏览大量的搜索结果。Lucene没有内置的分页机制,但可以通过结合Searcher的`docIds`和`scoreDocs`属性来实现。通常,我们先执行一个查询,获取到`...
这使得搜索性能得到了显著提升,因为过滤操作可以在编译查询时就完成,而不是在匹配文档时进行。例如,我们可能希望只返回特定日期范围内的文档,或者只包含某些关键词的文档,这时Filter就能派上用场。 Filter的...
用到的工具 jsoup+spring+struct+DButil+mysql+lucene 可以配置采集网站的图片,包含分组统计,相同数据合并功能,主要是给群内成员来个demo,让大家有个学习的demo 小试牛刀、临时写的,莫吐槽 需要用到mysql...
为了方便开发,许多基于Lucene的工具和框架如Solr、Elasticsearch提供了更高级的分页功能。例如,Solr支持`start`和`rows`参数来实现分页,Elasticsearch则有`from`和`size`参数,同时提供了更丰富的分页策略,如`...
在本文中,我们将深入探讨如何使用Lucene查询工具类和`IndexSearcher`进行分页查询,这在处理大量数据时尤其有用。Lucene是一个强大的全文搜索引擎库,它提供了高效、可扩展的文本检索功能。在Java开发环境中,...
在大型数据集上进行搜索时,一次性返回所有结果并不实际,因此分页搜索显得尤为重要。在 Lucene 中,可以通过 `TopDocs` 类来实现这一功能。`TopDocs` 包含了查询结果的排序信息和总数,可以用来计算出每一页的数据...
在 .NET 平台上,Lucene.net 提供了与原生 Lucene 相同的强大功能,并且完全兼容 .NET Framework 和 .NET Core。 1. **文本分析(Text Analysis)** 文本分析是 Lucene 的关键部分,用于将输入的字符串转换为可...
标题中的“使用Lucene4.7实现搜索功能,分页+高亮”表明我们要讨论的是如何利用Apache Lucene 4.7版本来构建一个具备搜索、分页和高亮显示功能的系统。Lucene是一个高性能、全文本搜索引擎库,它提供了强大的文本...
在lucene搜索分页过程中,可以有两种方式 一种是将搜索结果集直接放到session中,但是假如结果集非常大,同时又存在大并发访问的时候,很可能造成服务器的内存不足,而使服务器宕机 还有一种是每次都重新进行搜索,这样...
在IT行业中,Lucene是一个非常重要的全文搜索引擎库,尤其在处理大数据量的文本搜索时,它的性能和灵活性得到了广泛认可。本篇文章将带你迈出使用Lucene的第一步,重点关注如何实现分页检索,这对于构建高效、用户...
### Lucene 使用正则表达式 #### 知识点概览 1. **Lucene简介** 2. **正则表达式(regex)在...通过以上内容,我们可以看到如何在Lucene中使用正则表达式进行高级搜索,这对于处理大量数据时进行精确查询是非常有用的。
在构建一个简单的新闻网站搜索引擎时,SSH(Spring、Struts2和Hibernate)是一个常见的Java Web开发框架组合,而Lucene是Apache开源组织提供的一款强大的全文搜索引擎库。本项目结合了这些技术,实现了分页、排序和...
这篇博客文章“对内存中Lucene查询的集合进行分页”探讨的是如何在处理大量数据时,有效地对Lucene查询结果进行分页显示,以提高用户体验并减轻服务器负担。 首先,理解Lucene的基本工作原理至关重要。Lucene通过...
当用户输入查询字符串时,IKAnalyzer会对其进行分词处理,生成对应的查询关键字,然后Lucene会使用这些关键字在倒排索引中进行查找,返回匹配的文档。通过这种方式,即使面对复杂的中文查询,也能快速找到相关结果。...
在IT领域,搜索引擎技术是不可或缺的一部分,而Apache Lucene是一个高性能、全文本搜索库,它为开发者提供了构建自定义搜索引擎应用程序所需的所有工具。本篇我们将深入探讨如何在Lucene中实现模糊搜索,以及相关的...
本文将对Lucene部分常用代码进行详细解释,包括多字段搜索、多条件搜索、过滤等。 多字段搜索 在Lucene中,我们可以使用MultifieldQueryParser来指定多个搜索字段。MultifieldQueryParser可以将多个字段合并到一个...
在本系统中,我们利用SpringMVC的模型绑定功能,结合Lucene和MyBatis的查询能力,实现了基于关键词的文档搜索结果的分页显示。用户可以通过指定页码和每页条目数量来浏览搜索结果,系统会动态地计算总页数并加载相应...
7. **QueryWrapperFilter**:将Lucene的Query对象转换为过滤器,允许根据查询条件进行过滤。 以`FieldCacheRangeFilter`为例,我们可以通过以下方式创建一个范围过滤器: ```java FieldCacheRangeFilter<Integer> ...
本文将深入探讨如何在Lucene中实现高亮显示搜索结果和高效的分页功能,帮助开发者更好地理解和运用这个强大的工具。 一、Lucene简介 Lucene的核心功能是提供文本的索引和搜索,其内部实现了高效的倒排索引结构,...
在Lucene.Net中,查询语句会被解析成Query对象,然后与索引进行匹配。匹配过程中,可以使用评分函数(如TF-IDF)来评估相关性,按相关性高低返回结果。 **七、Web搜索引擎开发流程** 1. **数据采集**:使用网络...