原文链接# Lucene学习总结之二:Lucene的总体架构,这里做一些摘要并收藏。
Lucene总的来说是:
- 一高效的,可扩展的,全文检索库
- 全部用Java实现,无需配置
- 仅支持纯文本的索引(Indexing)和搜索(Search)。
- 不负责由其他格式的文件抽取纯文本文件,或从网络中抓取文件的过程。
在Lucene in action中,Lucene的架构和过程如下图,
说明Lucene是有索引和搜索两个过程,包含索引创建,索引,搜索三个要点。
让我们更细一些看Lucene的各组件:
- 被索引的文档用Document对象表示。
- IndexWriter通过函数addDocument()将文档添加到索引中,实现创建索引的过程。
- Lucene的索引是应用反向索引。
- 当用户有请求时,Query代表用户的查询语句。
- IndexSearcher通过函数search()搜索Lucene Index。
- IndexSearcher计算term weight和score并且将结果返回给用户。
- 返回给用户的文档集合用TopDocsCollector表示。
那么如何应用这些组件呢?
让我们再详细对Lucene API的调用实现索引和搜索过程。
- 索引过程如下:
- 创建一个IndexWriter用来写索引文件,它有几个参数,#{INDEX_DIR}就是索引文件所存放的位置,Analyzer便是用来对文档进行词法分析和语言处理的。
- 创建一个Document代表我们要索引的文档。
- 将不同的Field加入到文档中。我们知道,一篇文档有多种信息,如题目,作者,修改时间,内容等。不同类型的信息用不同的Field来表示,在本例子中,一共有两类信息进行了索引,一个是文件路径,一个是文件内容。其中FieldReader的#{SRC_FILE}就表示要索引的源文件。
- IndexWriter调用函数addDocument()将索引写到索引文件夹中。
- 搜索过程如下:
- IndexReader将磁盘上的索引信息读到内存,#{INDEX_DIR}就是索引文件存放的位置。
- 创建IndexSearcher准备惊醒搜索。
- 创建Analyzer用来对查询语句进行词法分析和语言处理。
- 创建QueryParser用来对查询语句进行语法分析。
- QueryParser调用parser()进行语法分析,形成查询语法树,放到Query中。
- IndexSearcher调用search()对查询语法树Query进行搜索,得到结果TopScoreDocCollector。
以上便是Lucene API函数的见到调用。
然而当进入Lucene源代码后,发现Lucene有很多包,关系错综复杂。
通过下图,我们不难发现,Lucene的各个源码模块,都是对普通索引和搜索过程的一种实现。
此图是上一节介绍的全文检索的流程对应的Lucene实现的包结构。
- Lucene的analysis模块主要负责词法分析及语言处理而形成term。
- Lucene的index模块主要负责索引的创建,里面有IndexWriter。
- Lucene的store模块主要负责索引的读写。
- Lucene的QueryParser模块主要负责语法的分析。
- Lucene的search模块主要负责对索引的搜索。
- Lucene的similarity模块主要负责相关性打分的实现。
以下是一个实现,基于Lucene 7.3.0:
https://github.com/lanzehao/lucene-dome
- Indexer.java
/**
* 索引文件
*/
public class Indexer {
// 写索引实例
private IndexWriter writer;
public Indexer(String indexDir) throws IOException {
// 得到索引所在目录的路径
Directory directory = FSDirectory.open(Paths.get(indexDir));
// 标准分词器
Analyzer analyzer = new StandardAnalyzer();
// 保存用户创建IndexWriter的所有配置
IndexWriterConfig iwConfig = new IndexWriterConfig(analyzer);
// 实例化IndexWriter
writer = new IndexWriter(directory,iwConfig);
}
/**
* 关闭写索引
*/
public void close() throws IOException {
writer.close();
}
public int index(String dataDir) throws IOException {
File[] files = new File(dataDir).listFiles();
for (File file : files) {
// 索引指定文件
indexFile(file);
}
return writer.numDocs();
}
/**
* 索引单个文件
* @param f
*/
private void indexFile(File f) throws IOException {
// 输出索引文件的路径
System.out.println("索引文件: " + f.getCanonicalPath());
// 获取文档,文档里再设置每个字段
Document document = getDocument(f);
// 开始写入,就是把文档写进索引文件中
writer.addDocument(document);
}
/**
* 获取文档,文档里再设置每个字段
*
* @param f
* @return document
*/
private Document getDocument(File f) throws IOException {
Document doc = new Document();
//把设置好的索引加到Document里,以便在确定被索引文档
doc.add(new TextField("contents", new FileReader(f)));
//Field.Store.YES:把文件名存索引文件里,为NO就说明不需要加到索引文件里去
doc.add(new TextField("fileName", f.getName(), Field.Store.YES));
//把完整路径存在索引文件里
doc.add(new TextField("fullPath", f.getCanonicalPath(), Field.Store.YES));
return doc;
}
}
- Searcher.java
/**
* 根据索引搜索
*/
public class Searcher {
public static void search(String indexDir, String q) throws IOException, ParseException {
// 得到读取索引文件的路径
Directory dir = FSDirectory.open(Paths.get(indexDir));
// 通过Dir得到路径下所有文件
IndexReader reader = DirectoryReader.open(dir);
// 建立索引查询器
IndexSearcher searcher = new IndexSearcher(reader);
// 实例化分析器
Analyzer analyzer = new StandardAnalyzer();
/********建立查询解析器********/
// 第一个参数是要查询的字段; 第二个参数市分析器Analyzer
QueryParser parser = new QueryParser("contents", analyzer);
// 根据传进来的q查找
Query query = parser.parse(q);
// 计算索引开始时间
long start = System.currentTimeMillis();
/********开始查询********/
// 第一个参数是通过传过来的参数来查找得到的query; 第二个参数是要查询出的行数
TopDocs hits = searcher.search(query,10);
// 计算索引结束时间
long end = System.currentTimeMillis();
System.out.println("匹配 "+ q + ",查询到 " + hits.totalHits + " 个记录, 用时:" + (end - start));
// 遍历hits.scoreDocs,得到scoreDoc
// scoreDoc:得分文档,即得到的文档 scoreDocs:代表topDocs这个文档数组
for (ScoreDoc scoreDoc : hits.scoreDocs) {
Document doc = searcher.doc(scoreDoc.doc);
System.out.println(doc.get("fullPath"));
}
//关闭reader
reader.close();
}
}
- Runnable.java
public class Runnable {
// 索引的存放路径
static final String indexDir = "D:\\IdeaProjects\\lucene-dome\\src\\main\\resources\\dataindex";
// 被索引数据的路径
static final String dataDir = "D:\\IdeaProjects\\lucene-dome\\src\\main\\resources\\data";
// 搜索内容
static final String query = "api NOT index";
public static void main(String[] args) {
// index();
search();
}
private static void index(){
Indexer indexer = null;
int numIndexed = 0;
//startTime
long start = System.currentTimeMillis();
try{
indexer = new Indexer(indexDir);
numIndexed = indexer.index(dataDir);
}catch(Exception e){
e.printStackTrace();
}finally{
try{
indexer.close();
}catch (Exception e){
e.printStackTrace();
}
}
//endTime
long end = System.currentTimeMillis();
System.out.println("索引: "+ numIndexed + " 个文件, 用时: "+ (end-start));
}
private static void search(){
try{
Searcher.search(indexDir, query);
}catch (Exception e){
e.printStackTrace();
}
}
}
了解了Lucene的整个结构,我们便可以开始Lucene的源码之旅了。