我使用Lucene 5.5.0进行索引。以下标准描述了我的环境:
- 索引文档由8个字段组成。它们对于语料库中的所有文档都是相同的(所有文档都有相同的"模式")
- 所有字段都是
String
或Long
字段(因此不需要文本分析)。所有这些都是由lucene储存的。字符串的最大长度为255个字符 - 该索引被视为"大部分读取",90%的请求是(并发)读取。我在应用程序级别进行锁定,所以Lucene不必担心并发读写
- 在搜索语料库时,我不要求对结果进行任何排名。检索到的文档结果的顺序可以完全是任意的
- 查询通常是布尔值、正则表达式和数值范围查询的组合
- 搜索语料库时,检索与查询匹配的所有文档是的首要任务
我实现的当前search
方法,包装Lucene的API,看起来是这样的:
public Set<Document> performLuceneSearch(Query query) {
Set<Document> documents = Sets.newHashSet();
// the reader instance is reused as often as possible, and exchanged
// when a write occurs using DirectoryReader.openIfChanged(...).
if (this.reader.numDocs() > 0) {
// note that there cannot be a limiting number on the result set.
// I absolutely need to retrieve ALL matching documents, so I have to
// make use of 'reader.numDocs()' here.
TopDocs topDocs = this.searcher.search(query, this.reader.numDocs());
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs) {
int documentId = scoreDoc.doc;
Document document = this.reader.document(documentId);
documents.add(document);
}
}
return Collections.unmodifiableSet(documents);
}
考虑到我上面概述的环境,有什么方法可以更快/更好地做到这一点吗?特别是考虑到我不需要任何排名或排序(而是结果的完整性),我觉得应该有一些捷径可以走,让事情更快。
您可以做一些事情来加快搜索速度。首先,如果你不使用评分,你应该禁用规范,这会使指数变小。由于您只使用StringField
和LongField
(例如,与带有关键字标记器的TextField
相反),因此这些字段的规范是禁用的,因此您已经获得了规范。
其次,您应该构建和包装查询,以便最大限度地减少实际分数的计算。也就是说,如果使用BooleanQuery
,请使用Occur.FILTER
而不是Occur.MUST
。两者都有相同的包含逻辑,但filter不得分。对于其他查询,请考虑将它们封装在ConstantScoreQuery
中。然而,这可能根本没有必要(解释如下)。
第三,使用自定义Collector
。默认的搜索方法适用于较小的、排序或排序的结果集,但您的用例不适合这种模式。下面是一个示例实现:
import org.apache.lucene.document.Document;
import org.apache.lucene.index.LeafReader;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.search.SimpleCollector;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
final class AllDocumentsCollector extends SimpleCollector {
private final List<Document> documents;
private LeafReader currentReader;
public AllDocumentsCollector(final int numDocs) {
this.documents = new ArrayList<>(numDocs);
}
public List<Document> getDocuments() {
return Collections.unmodifiableList(documents);
}
@Override
protected void doSetNextReader(final LeafReaderContext context) {
currentReader = context.reader();
}
@Override
public void collect(final int doc) throws IOException {
documents.add(currentReader.document(doc));
}
@Override
public boolean needsScores() {
return false;
}
}
你会这样用的。
public List<Document> performLuceneSearch(final Query query) throws IOException {
// the reader instance is reused as often as possible, and exchanged
// when a write occurs using DirectoryReader.openIfChanged(...).
final AllDocumentsCollector collector = new AllDocumentsCollector(this.reader.numDocs());
this.searcher.search(query, collector);
return collector.getDocuments();
}
收集器使用列表而不是集合。Document
不实现equals
或hashCode
,因此您不会从集合中获利,只需支付额外的平等检查费用。最后的顺序是所谓的索引顺序。第一个文档将是索引中第一个文档(如果没有自定义合并策略,则大致为插入顺序,但最终是一个不能保证稳定或可靠的任意顺序)。此外,收集器发出不需要分数的信号,这给了你与使用上面的选项2大致相同的好处,所以你可以省去一些麻烦,只需保持查询的原样。
根据您对Document
的需求,您可以通过使用DocValues而不是存储字段来获得更大的加速。只有当您只需要一个或两个字段,而不是全部字段时,这才是正确的。经验法则是,对于少数文档但许多字段,使用存储字段;对于许多文档但很少有字段,请使用DocValues。无论如何,您应该进行实验——8个字段并没有那么多,您可能会从所有字段的事件中获利。以下是如何在索引过程中使用DocValues:
import org.apache.lucene.document.Field;
import org.apache.lucene.document.LongField;
import org.apache.lucene.document.NumericDocValuesField;
import org.apache.lucene.document.SortedDocValuesField;
import org.apache.lucene.document.StringField;
import org.apache.lucene.util.BytesRef;
document.add(new StringField(fieldName, stringContent, Field.Store.NO));
document.add(new SortedDocValuesField(fieldName, new BytesRef(stringContent)));
// OR
document.add(new LongField(fieldName, longValue, Field.Store.NO));
document.add(new NumericDocValuesField(fieldName, longValue));
字段名称可以相同,如果您可以完全依赖DocValues,则可以选择不存储其他字段。收集器必须更改,例如一个字段:
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.SortedDocValues;
import org.apache.lucene.search.SimpleCollector;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
final class AllDocumentsCollector extends SimpleCollector {
private final List<String> documents;
private final String fieldName;
private SortedDocValues docValues;
public AllDocumentsCollector(final String fieldName, final int numDocs) {
this.fieldName = fieldName;
this.documents = new ArrayList<>(numDocs);
}
public List<String> getDocuments() {
return Collections.unmodifiableList(documents);
}
@Override
protected void doSetNextReader(final LeafReaderContext context) throws IOException {
docValues = context.reader().getSortedDocValues(fieldName);
}
@Override
public void collect(final int doc) throws IOException {
documents.add(docValues.get(doc).utf8ToString());
}
@Override
public boolean needsScores() {
return false;
}
}
您将分别对长字段使用getNumericDocValues
。您必须对所有必须加载的字段重复这一步骤(当然是在同一个收集器中),最重要的是:衡量何时最好从存储的字段加载完整文档,而不是使用DocValues。
最后一点:
我在应用程序级别进行锁定,所以Lucene不必担心并发读写。
IndexSearcher和IndexWriter本身已经是线程安全的。如果你只为Lucene锁定,你可以移除这些锁,并在所有线程之间共享它们。并考虑使用oal.search.SearcherManager
来重用IndexReader/Searcher。