Lucene:关键字检索
Lucene是一个基于java开源的全文检索工具包,项目地址是http://lucene.apache.org/。难得可贵的是它支持中文检索。下面我们来学习下一个简单的Lucene的入门范例。
基本概念
Index
。 在Lucene中index是一个集合, 这个集合中会包含多个全文关键字以待检索。本例中我们的index是一个文件夹,我们会把待检索内容进行分词后存入这个文件夹中。Document
。文档指代的是一篇待进行检索的全文。对一篇文档(或者待检索内容)进行关键字分词后就存入文档中。使用到的类
org.apache.lucene.store.Directory
。 这个类是Lucen中表示索引存储位置的类,它可以是一个org.apache.lucene.store.FSDirectory
表示将索引存储到文件系统中。也可是org.apache.lucene.store.RAMDirectory
将索引存储到内存中。org.apache.lucene.analysis.Analyzer
是词法分析器,用来进行语言分词,在创建索引和检索的时候需要使用同样的分析器来进行语句分析。为了支持中文检索,我们将会使用到org.apache.lucene.analysis.cjk.CJKAnalyzer
,这个分析器同样支持日文的检索。org.apache.lucene.index.IndexReader
。顾名思义这个类是用来读取Index的类,我们在进行检索的时候需要让检索器知道如何去读取Index,那么就需要构建一个IndexReader。我们将会使用的Reader将是普通的org.apache.lucene.index.DirectoryReader
。org.apache.lucene.search.IndexSearcher
是用来进行Index检索的主类。org.apache.lucene.index.IndexWriter
是构建index的时候使用到的类,用来将index输入到指定的位置(文件系统或者内存)。构建writer的时候还需要构建org.apache.lucene.index.IndexWriterConfig
来对writer进行配置。org.apache.lucene.document.Document
。如前段说说,Document是一篇待检索内容。Document中可以存储多个内容用来表示一篇完整的待检索内容(文本)。Document必须要存储在Index中,可以在存储的时候选择是否要存储原内容或者只存储用于检索的关键字。org.apache.lucene.document.StoredField
。用来存储一段需要保存到Index中的内容,这个内容在后续检索的时候可以直接获取原文(不会进行分词或者index转换)。一般存储一些很小的数据内容,并且需要在检索过程中使用到的内容,比如排序之类的关键字。org.apache.lucene.document.TextField
。用来存储一大段文本的分词结果,通常用于不需要存储原文的大段文本内容。org.apache.lucene.document.StringField
。这个用来表示会存进index中但不会进行分词的内容,通常用来存储关键字,或者一些不会变化的文本,可以用来作为Document的id来进行Document运行时的更新。org.apache.lucene.search.ScoreDoc
。用来表示检索过程中hit的Document,其中会有Document的顺序标识(用来取出Document)和检索的时候匹配度(个人理解,仅供参考)。
下面我们开始学习代码
需要使用到的包
下载Lucene后会发现了有好多包,那么基础的包在以下几个目录中,这些包是一定要引入的。
- analysis\common\这个包里面包含了我们需要的分析器,如果需要其他类型的分析器,可以尝试去analysis目录下寻找。
- core\这个目录下的包是Lucene的运行核心代码
- queries\和queryparser\目录下的包是用来执行检索查询功能的核心包
文本分析并且存入Index中
首先我们需要指定Index的存储位置,本例中我们使用文件系统来存储Index,那么需要构建一个FSDirectory:
Path indexPath = Paths.get(indexPathString);
Directory indexDir = FSDirectory.open(indexPath);
其次我们需要构建一个IndexWriter进行Index的存储,在构建的时候我们可以指定index的存储方式CREATE_OR_APPEND,表明我们期望在index存在的情况下只进行update而不是创建新的index:
IndexWriterConfig iwc = new IndexWriterConfig(analyzer);
iwc.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND);
IndexWriter writer = new IndexWriter(indexDir, iwc);
获得了Writer之后我们就可以开始构建Document,然后使用Writer将Document写入文件中。一个Document对应一个待检索内容。所以完整的构建index的代码如下:
public void index(String id, String context, String title){
//构建存储index的目录
Path indexPath = Paths.get(indexPathString);
Directory indexDir = FSDirectory.open(indexPath);
Analyzer analyzer = new CJKAnalyzer();
//构建Writer
IndexWriterConfig iwc = new IndexWriterConfig(analyzer);
iwc.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND);
IndexWriter writer = new IndexWriter(indexDir, iwc);
//构建的待检索文本的Document
Document doc = new Document();
//存储为TextField的格式(分词)并且不存储原文本的内容(减少存储空间的占用)
doc.add(new
TextField("context", context, Field.Store.NO));
doc.add(new TextField("title", title, Field.Store.NO));
//添加index创建的时间,可以在后续进行检索查询的时候作为排序依据。当然你可以自己决定是否存储。
doc.add(new StoredField("created", System.currentTimeMillis()));
//存储id标识(可以是文本id或者数据库中记录id,以方便后续查询的时候可以通过这个id找回原文本内容)
doc.add(new StoredField("id", id));
}
索引查询
要根据关键字进行前段构建的索引的查询,我们需要构建IndexSearcher。并且我们需要明白Lucene中查询是返回TopN的形式,并且需要指定是通过哪一个字段进行检索(比如前段中的contex还是title)。下面的是范例代码:
public List<String> query(String keyWord){
List<String> ids = new Arrayist<String>();
//必须使用和构建index的时候使用的Analyzer一样的类
Analyzer analyzer = new CJKAnalyzer();
//我们需要构建一个QueryPaser来生出查询使用的Query对象
//构建的时候需要指定我们查询要针对的是哪一个字段,比如本例中指定对contex字段进行查询
//这个contex是在构建Document的是存入Document中的。
QueryParser parser = new QueryParser("context", analyzer);
Query query = parser.parse(key);
//构建一个Reader
IndexReader indexReader = DirectoryReader.open(indexDir);
//根据Reader构建Searcher
IndexSearcher searcher = new IndexSearcher(indexReader);
//查询Top100个记录
ScoreDoc[] docs = searcher.search(query, 100).scoreDocs;
for (ScoreDoc doc : docs) {
//根据记录中的docid来获取Document的具体对象。
Document document = searcher.doc(doc.doc);
//需要注意的是必须是在构建Index的时候指定Store为true的filed才可以在这里取出 ids.add(document.get("id"));
}
retirn ids;
}
查询排序
如果要对查询结果进行排序,那么我们需要在进行查询的时候传入一个org.apache.lucene.search.Sort对象,比如需要使用”created”进行排序,那么在进行Searcher之前需要构建一个Sort对象:
Sort sort = new Sort(new SortField("create", SortField.Type.LONG));
//查询Top100个记录
ScoreDoc[] docs = searcher.search(query, 100, sort).scoreDocs;
分页查询
计数
既然要分页,那么我们需要知道关键字hit到的记录总数:
public int count(String key, String field){
QueryParser parser = new QueryParser("context", analyzer);
Query query = parser.parse(key);
//构建一个Reader
IndexReader indexReader = DirectoryReader.open(indexDir);
//根据Reader构建Searcher
IndexSearcher searcher = new IndexSearcher(indexReader);
return searcher.count(query);
}
查询指定页数的记录
Lucene中并没有提供一个直观的分页功能,但是它有提供了searchAfter功能,我们可以使用这个功能来进行分页查询。为了实现searchAfter,我们需要知道上一次(上一页)查询的最后一个ScoreDoc。我们可以在每次查询的时候都根据页数和每页显示的数量来找到最后一个ScoreDoce。当然这个方法有缺点就是如果只是查询后面几页的记录的话,我们还是需要查询出前面所有的ScoreDoc。这会导致内存占用的浪费。
public ScoreDoc getLastScoreDoc(int page, int rows, Query query, Sort sort) throws IOException {
if (page == 1) return null;//如果是第一页就返回空
int num = rows * (page - 1);//获取上一页的最后数量
TopDocs tds = searcher.search(query, num, sort);
return tds.scoreDocs[num - 1];
}
当获得了前一页的最后一个ScoreDoc之后我们就可以使用searchAfer的方法查询当前页面的ScoreDoc了,这里我们需要注意的是如果使用了排序,那么在获取上一页最后一个ScoreDoc和获取当前页面的SocreDoc的时候都需要使用一样的Sort。
//page: 要显示的页数
//rows: 每页显示多少条记录
//...省略构建Searcher的过程
Sort sort = new Sort(new SortField("create", SortField.Type.LONG));
ScoreDoc last = getLastScoreDoc(page, rows, query, sort);
ScoreDoc[] docs = searcher.searchAfter(last, query, rows, sort).scoreDocs;
//...后续处理