目录
1. Lucene概述
1.1 什么是Lucene
1.2 Lucene的原理
2. Lucene的使用
2.1 准备
2.2 生成索引
2.3 全文检索
2.4 多Field检索
2.5 中文分词器
2.6 停用词
2.7 是否索引,是否储存
Lucene是一个全文搜索框架,而不是应用产品。因此它并不像baidu或者google那样拿来就能用,它只是提供了一些工具让你能实现这些产品。
Lucene的发明者Doug Cutting也同样是Hadoop的创造者。
Lucene能做什么?
要回答这个问题,先要了解Lucene的本质。
实际上Lucene的功能很单一,说到底,就是你给它若干个字符串,然后它为你提供一个全文搜索服务,告诉你你要搜索的关键词出现在哪里。
知道了这个本质,你就可以发挥想象做任何符合这个条件的事情了。
你可以把站内新闻都索引了,做个资料库;
你可以把一个数据库表的若干个字段索引起来,那就不用再担心因为"%like%"而锁表了;
你也可以写个自己的搜索引擎。
Lucene效率如何?
下面给出一些N年前的测试数据,如果你觉得可以接受,那么可以选择。
测试一:250万记录,300M左右文本,生成索引380M左右,800线程下平均处理时间300ms。
测试二:37000记录,索引数据库中的两个varchar字段,索引文件2.6M,800线程下平均处理时间1.5ms。
Lucene为什么这么快?
倒排索引
压缩算法
二元搜索
倒排索引
先要了解一下数据库的like搜索为什么那么慢:
执行like搜索时,数据库要遍历每条数据,并在每次遍历的过程中匹配关键词。
也就是说,慢的原因是在记录中搜索关键词,所以可以把这个过程反过来,以关键词搜索记录。
倒排索引是将每个文档(数据库中的记录)中的词汇提前取出,建立“词汇-文档索引”。
以上表中数据为例,将其中所有词汇取出,并建立索引:
Lucene的工作方式
Lucene提供的服务实际包含两部分:一入一出。
所谓入是写入,即将你提供的源(本质是字符串)写入索引或者将其从索引中删除;
所谓出是读出,即向用户提供全文搜索服务,让用户可以通过关键词定位源。
写入流程:
源字符串首先经过analyzer处理,包括:分词(分成一个个单词)、去除停用词(stopword,可选)。
将源中需要的信息加入Document的各个Field中,并把需要索引的Field索引起来,把需要存储的Field存储起来。
将索引写入存储器,存储器可以是内存或磁盘。
读出流程:
用户提供搜索关键词,经过analyzer处理。
对处理后的关键词搜索索引找出对应的Document。
用户根据需要从找到的Document中提取需要的Field。
引入依赖
org.apache.lucene
lucene-highlighter
9.5.0
org.apache.lucene
lucene-queryparser
9.5.0
org.apache.lucene
lucene-analyzers-common
8.11.2
commons-io
commons-io
2.11.0
准备基础数据
知乎热榜 - 知乎
创建两个目录,例如 D:/data
和 D:/index
在data目录中添加一些数据,可以保存几个网页
private static final String INDEX_DIR = "/Users/whitecamellia/Desktop/lucene/index";
private static final String DATA_DIR = "/Users/whitecamellia/Desktop/lucene/data";
@Test
public void createIndex() throws Exception {
// 获取存放索引的目录
Directory directory = FSDirectory.open(Paths.get(INDEX_DIR));
// 创建IndexWriter的默认配置
IndexWriterConfig indexWriterConfig = new IndexWriterConfig();
// 创建IndexWriter
IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig);
// 获取存放原始数据的目录
File dataDir = new File(DATA_DIR);
// 遍历目录中的文件
for (File data : dataDir.listFiles()) {
if (data.isFile()) {
// 创建文档
Document document = new Document();
String title = data.getName();
// 向title Field域中加入文件名
document.add(new TextField("title", title, Field.Store.YES));
String content = FileUtils.readFileToString(data, "utf-8");
// 向content Field域中加入文件内容
document.add(new TextField("content", content, Field.Store.YES));
indexWriter.addDocument(document);
}
}
indexWriter.close();
}
@Test
public void search() throws Exception {
String keyword = "何平";
// 获取索引目录
Directory directory = FSDirectory.open(Paths.get(INDEX_DIR));
// 创建标准分词器
Analyzer analyzer = new StandardAnalyzer();
// 通过索引创建出IndexSearcher
DirectoryReader reader = DirectoryReader.open(directory);
IndexSearcher indexSearcher = new IndexSearcher(reader);
// 指定搜索域
QueryParser queryParser = new QueryParser("title", analyzer);
// QueryParser queryParser = new QueryParser("content", analyzer);
// 处理搜索关键词
Query query = queryParser.parse(keyword);
// 搜索整个索引,并获取前5条
TopDocs topDocs = indexSearcher.search(query, 5);
System.out.println("共搜索出 " + topDocs.totalHits + " 条数据");
System.out.println("#################################################################");
// 遍历搜索结果
for (ScoreDoc doc : topDocs.scoreDocs) {
// 通过文档的索引获取文档
Document document = indexSearcher.doc(doc.doc);
// 获取文档指定域的内容
// System.out.println(document.get("title"));
System.out.println(document.getField("title").stringValue());
// 获取该文档的score,决定了结果顺序,代表与关键词的相关性(相关性计算所占百分比)
System.out.println("搜索评分:" + doc.score);
}
}
以上只是在单个Field(title)中搜索,也可以在多个Field中搜索。
只需将
QueryParser queryParser = new QueryParser("title", analyzer);
替换为
QueryParser queryParser = new MultiFieldQueryParser(new String[]{"title", "content"}, analyzer);
Lucene默认使用StandardAnalyzer
进行分词,该分词器逻辑较为简单,尤其是对于中文,只是单纯对每个字进行拆分,没办法使用。
Lucene也提供了中文分词器,需引入依赖:org.apache.lucene:lucene-analyzers-smartcn
,然后将上面程序中的分词器修改成SmartChineseAnalyzer
,注意,在生成索引时也要指定分词器,不然还是默认的StandardAnalyzer
:
// search 创建中文分词器
Analyzer analyzer = new SmartChineseAnalyzer();
// createIndex
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);
org.apache.lucene
lucene-analyzers-smartcn
8.11.2
文档中有很多词对于检索来说是没有实际意义的,比如“我”、“是”、“的”、“了”……
这就需要在生成索引和检索时排除掉这些,Lucene默认有一个停用词库,不过里面只包含了几十个标点符号。
我们也可以在创建分词器时自己指定停用词:
List STOP_WORD_LIST = Arrays.asList(new String[]{",", "。", ",", ".", "?", "我", "的", "了"});
CharArraySet set = new CharArraySet(STOP_WORD_LIST, true);
Analyzer analyzer = new SmartChineseAnalyzer(set);
也可以去网上找一个通用的停用词库。
中文停用词表
// 创建文档
Document document = new Document();
String title = data.getName();
// 是否索引==》是否要对title的数据进行分词,生成索引表
// TextField("title") 是索引
// new StringField("author")
// 是否储存 生成索引表的时候要不要在我们的索引库中保存原数据
// 保存就可以获取到对应的内容 document.getField("content").stringValue()
// Field.Store.No
// 通常,对于标题,我们是存储的,但是对于内容,我们并不需要,因为内容的数据太了,
// 内容的数据,我们可以去查表获取
document.add(new TextField("title", title, Field.Store.YES));
String content = FileUtils.readFileToString(data, "utf-8");
// 向content Field域中加入文件内容
document.add(new TextField("content", content, Field.Store.NO));
indexWriter.addDocument(document);