博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
【Lucene】Apache Lucene全文检索引擎架构之入门实战
阅读量:4293 次
发布时间:2019-05-27

本文共 7228 字,大约阅读时间需要 24 分钟。

Lucene是一套用于全文检索和搜寻的开源程式库,由Apache软件基金会支持和提供。Lucene提供了一个简单却强大的应用程式接口,能够做全文索引和搜寻。在Java开发环境里Lucene是一个成熟的免费开源工具。就其本身而言,Lucene是当前以及最近几年最受欢迎的免费Java信息检索程序库。——《百度百科》

  这篇博文主要从两个方面出发,首先介绍一下Lucene中的全文搜索原理,其次通过程序示例来展现如何使用Lucene。关于全文搜索原理部分我上网搜索了一下,也看了好几篇文章,最后在写这篇文章的时候部分参考了其中两篇(地址我放在文章的末尾),感谢原文作者。

1. 全文检索

  何为全文检索?举个例子,比如现在要在一个文件中查找某个字符串,最直接的想法就是从头开始检索,查到了就OK,这种对于小数据量的文件来说,很实用,但是对于大数据量的文件来说,就有点呵呵了。或者说找包含某个字符串的文件,也是这样,如果在一个拥有几十个G的硬盘中找那效率可想而知,是很低的。

  文件中的数据是属于非结构化数据,也就是说它是没有什么结构可言的,要解决上面提到的效率问题,首先我们得即将非结构化数据中的一部分信息提取出来,重新组织,使其变得有一定结构,然后对此有一定结构的数据进行搜索,从而达到搜索相对较快的目的。这就叫全文搜索。即先建立索引,再对索引进行搜索的过程。
  那么lucene中是如何建立索引的呢?假设现在有两个文档,内容如下:

文章1的内容为:Tom lives in Guangzhou, I live in Guangzhou too.

文章2的内容为:He once lived in Shanghai.

  首先第一步是将文档传给分词组件(Tokenizer),分词组件会将文档分成一个个单词,并去除标点符号和停词。所谓的停词指的是没有特别意义的词,比如英文中的a,the,too等。经过分词后,得到词元(Token) 。如下:

文章1经过分词后的结果:[Tom] [lives] [Guangzhou] [I] [live] [Guangzhou]

文章2经过分词后的结果:[He] [lives] [Shanghai]

  然后将词元传给语言处理组件(Linguistic Processor),对于英语,语言处理组件一般会将字母变为小写,将单词缩减为词根形式,如”lives”到”live”等,将单词转变为词根形式,如”drove”到”drive”等。然后得到词(Term)。如下:

文章1经过处理后的结果:[tom] [live] [guangzhou] [i] [live] [guangzhou]

文章2经过处理后的结果:[he] [live] [shanghai]

  最后将得到的词传给索引组件(Indexer),索引组件经过处理,得到下面的索引结构:

关键词 文章号[出现频率] 出现位置
guangzhou 1[2] 3,6
he 2[1] 1
i 1[1] 4
live 1[2],2[1] 2,5,2
shanghai 2[1] 3
tom 1[1] 1

  以上就是lucene索引结构中最核心的部分。它的关键字是按字符顺序排列的,因此lucene可以用二元搜索算法快速定位关键词。实现时lucene将上面三列分别作为词典文件(Term Dictionary)、频率文件(frequencies)和位置文件(positions)保存。其中词典文件不仅保存有每个关键词,还保留了指向频率文件和位置文件的指针,通过指针可以找到该关键字的频率信息和位置信息。

  搜索的过程是先对词典二元查找、找到该词,通过指向频率文件的指针读出所有文章号,然后返回结果,然后就可以在具体的文章中根据出现位置找到该词了。所以lucene在第一次建立索引的时候可能会比较慢,但是以后就不需要每次都建立索引了,就快了。当然了,这是针对英文的检索,针对中文的规则会有不同,后面我再看看相关资料。

2. 示例代码

  根据上文的分析,全文检索有两个步骤,先建立索引,再检索。所以为了测试这个过程,我写了两个java类,一个是测试建立索引的,另一个是测试检索的。首先建立个maven工程,pom.xml如下:

4.0.0
demo.lucene
Lucene01
0.0.1-SNAPSHOT
org.apache.lucene
lucene-core
5.3.1
org.apache.lucene
lucene-queryparser
5.3.1
org.apache.lucene
lucene-analyzers-common
5.3.1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

  在写程序之前,首先得去弄一些文件,我随便找了一些英文的文档(中文的后面再研究),放到了D:\lucene\data\目录中,如下:

文档
文档里面都是密密麻麻的英文,我就不截图了。
接下来开始写建立索引的java程序:

/** * 建立索引的类 * @author Ni Shengwu * */public class Indexer {
private IndexWriter writer; //写索引实例 //构造方法,实例化IndexWriter public Indexer(String indexDir) throws Exception { Directory dir = FSDirectory.open(Paths.get(indexDir)); Analyzer analyzer = new StandardAnalyzer(); //标准分词器,会自动去掉空格啊,is a the等单词 IndexWriterConfig config = new IndexWriterConfig(analyzer); //将标准分词器配到写索引的配置中 writer = new IndexWriter(dir, config); //实例化写索引对象 } //关闭写索引 public void close() throws Exception { writer.close(); } //索引指定目录下的所有文件 public int indexAll(String dataDir) throws Exception { File[] files = new File(dataDir).listFiles(); //获取该路径下的所有文件 for(File file : files) { indexFile(file); //调用下面的indexFile方法,对每个文件进行索引 } return writer.numDocs(); //返回索引的文件数 } //索引指定的文件 private void indexFile(File file) throws Exception { System.out.println("索引文件的路径:" + file.getCanonicalPath()); Document doc = getDocument(file); //获取该文件的document writer.addDocument(doc); //调用下面的getDocument方法,将doc添加到索引中 } //获取文档,文档里再设置每个字段,就类似于数据库中的一行记录 private Document getDocument(File file) throws Exception{ Document doc = new Document(); //添加字段 doc.add(new TextField("contents", new FileReader(file))); //添加内容 doc.add(new TextField("fileName", file.getName(), Field.Store.YES)); //添加文件名,并把这个字段存到索引文件里 doc.add(new TextField("fullPath", file.getCanonicalPath(), Field.Store.YES)); //添加文件路径 return doc; } public static void main(String[] args) { String indexDir = "D:\\lucene"; //将索引保存到的路径 String dataDir = "D:\\lucene\\data"; //需要索引的文件数据存放的目录 Indexer indexer = null; int indexedNum = 0; long startTime = System.currentTimeMillis(); //记录索引开始时间 try { indexer = new Indexer(indexDir); indexedNum = indexer.indexAll(dataDir); } catch (Exception e) { e.printStackTrace(); } finally { try { indexer.close(); } catch (Exception e) { e.printStackTrace(); } } long endTime = System.currentTimeMillis(); //记录索引结束时间 System.out.println("索引耗时" + (endTime-startTime) + "毫秒"); System.out.println("共索引了" + indexedNum + "个文件"); }}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

  我是按照建立索引的过程来写的程序,在注释中已经解释的很清楚了,这里就不再赘述了。然后运行一下main方法看一下结果,如下:

索引结果
  共索引了7个文件,耗时649毫秒,还是蛮快的,而且索引文件的路径也是对的,然后可以看一下D:\lucene\会生成一些文件,这些就是生成的索引。
索引
  现在有了索引了,我们可以检索想要查询的字符了,我随便打开了一个文件,在里面找了个比较丑的字符串“generate-maven-artifacts”来作为检索的对象。在检索之前先看一下检索的java代码:

public class Searcher {
public static void search(String indexDir, String q) throws Exception { Directory dir = FSDirectory.open(Paths.get(indexDir)); //获取要查询的路径,也就是索引所在的位置 IndexReader reader = DirectoryReader.open(dir); IndexSearcher searcher = new IndexSearcher(reader); Analyzer analyzer = new StandardAnalyzer(); //标准分词器,会自动去掉空格啊,is a the等单词 QueryParser parser = new QueryParser("contents", analyzer); //查询解析器 Query query = parser.parse(q); //通过解析要查询的String,获取查询对象 long startTime = System.currentTimeMillis(); //记录索引开始时间 TopDocs docs = searcher.search(query, 10);//开始查询,查询前10条数据,将记录保存在docs中 long endTime = System.currentTimeMillis(); //记录索引结束时间 System.out.println("匹配" + q + "共耗时" + (endTime-startTime) + "毫秒"); System.out.println("查询到" + docs.totalHits + "条记录"); for(ScoreDoc scoreDoc : docs.scoreDocs) { //取出每条查询结果 Document doc = searcher.doc(scoreDoc.doc); //scoreDoc.doc相当于docID,根据这个docID来获取文档 System.out.println(doc.get("fullPath")); //fullPath是刚刚建立索引的时候我们定义的一个字段 } reader.close(); } public static void main(String[] args) { String indexDir = "D:\\lucene"; String q = "generate-maven-artifacts"; //查询这个字符串 try { search(indexDir, q); } catch (Exception e) { e.printStackTrace(); } }}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

运行一下main方法,看一下结果:

检索
  Lucene已经正确的帮我们检索到了,然后我把中间的“-”去掉,它也能帮我们检索到,但是我把前面的字符都去掉,只留下“rtifacts”就检索不到了,这也能说明Lucene中建立索引是以单词来划分的,但是这个问题是可以解决的,我会在后续的文章中写到。

部分参考自:


—–乐于分享,共同进步!

—–我的博客主页:

你可能感兴趣的文章
Spring 全家桶注解一览
查看>>
JDK1.8-Stream API使用
查看>>
cant connect to local MySQL server through socket /tmp/mysql.sock (2)
查看>>
vue中的状态管理 vuex store
查看>>
Maven之阿里云镜像仓库配置
查看>>
Maven:mirror和repository 区别
查看>>
微服务网关 Spring Cloud Gateway
查看>>
SpringCloud Feign的使用方式(一)
查看>>
SpringCloud Feign的使用方式(二)
查看>>
关于Vue-cli+ElementUI项目 打包时排除Vue和ElementUI
查看>>
Vue 路由懒加载根据根路由合并chunk块
查看>>
vue中 不更新视图 四种解决方法
查看>>
MySQL 查看执行计划
查看>>
OpenGL ES 3.0(四)图元、VBO、VAO
查看>>
OpenGL ES 3.0(五)纹理
查看>>
OpenGL ES 3.0(八)实现带水印的相机预览功能
查看>>
OpenGL ES 3.0(九)实现美颜相机功能
查看>>
FFmpeg 的介绍与使用
查看>>
Android 虚拟机简单介绍——ART、Dalvik、启动流程分析
查看>>
原理性地理解 Java 泛型中的 extends、super 及 Kotlin 的协变、逆变
查看>>