lucene结构
索引:概念上的一个表,现实体现就是一个文件目录,一个目录代表一个索引,也视作documents文档集合
文档:document,为索引中的一条数据,一个document可以拥有多个filed(域),甚至可以完全相同。
域:field,一个field即为一个字段
是否分词:tokenized
是否索引:indexed
是否存储:stored
倒排索引
所谓倒排索引, 就是从文档内容,通过分词器分出不重复关键字,以关键字作为主键查询到具有关键字的文档id列表,查询到出具有关键词的文档。因此:倒排索引是用于模糊查询大量数据得到列表结果的,而非精确查询。
常用Field类型
类型 是否分词 是否索引 是否存储
StringField N Y Y或N
TextField Y Y Y或N
FloatPoint Y Y N
DoublePoint
LongPoint
IntPoint
StoredField N N Y
NumericDocValuesField 配合其他域使用
分词器
分词: 采集到的数据会存储道document对象的Field域中,分词就是将Document中的Field的value值切分成一个一个的词。分词就是为了查询,不作为查询条件的不要分词。
过滤:标点符号去除,停用词过滤,大写转小写,词的形还原
Analyzer使用时机:索引时使用/搜索时使用(两者所用分词器必须一致)
扩展词典:
放专有名词,或者我们认为需要强制将某一些词拆成一个关键字
凡是出现在扩展词典的词,就会强制分成一个关键字
停用词典:
凡是出现在停用词点的词,都会被过滤掉(节省存储空间,提高搜索效率)
存储结构
一个目录一个索引,一个逻辑索引由多个段segment组成,多个段可以合并减少读取内存时候的IO。Lucene数据先写入buffer缓冲区,到达一定数量后写入segment段,segment内部的数据不可以被修改,但可以被删除,删除为逻辑删除。新增家的document单独在一个新生的段中,随着段的合并写入到一个段中。
词典数据结构
跳跃表 占用内存小,且可调,但是对模糊查询支持不好。3.0之前使用
排序列表Array/List 使用二分法查找,不平衡字典树 查询效率跟字符串长度有关,但只适合英文词典
哈希表 性能高,内存消耗大,几乎是原始数据的三倍
双数组字典树 适合做中文词典,内存占用小,很多分词工具均采用此种算法
Finite State Transducers (FST) 一种有限状态转移机,Lucene 4有开源实现,并大量使用
B树 磁盘索引,更新方便,但检索速度慢,多用于数据库
lucene优化
解决大量磁盘IO
indexWriterConfig.setMaxBufferedDocs(10000); // 配置写入segment前缓冲区doc最大数目
indexWriter.forceMerge(1000); // 多少个文档合并一个段 越大索引越快 搜索越慢
TermQuery更快 QueryParser更强大
相关度排序
计算出词的权重 Term(tf – 此文档中词出现次数越高越大, df — 越多文档包含,得分越低)
根据词的权重值,计算文档相关度得分
boost加权(默认加权值为1.0f)
在索引时对某个文档中的filed设置加权重,搜索匹配到时这个文档就可能排在前面
在搜索时对某个域进行加权,在进行组合域查询时,匹配到加权值高的域最后计算的相关度得分就高。
设置boost是给域(field)或者Document设置的。
注意事项
-
关键词区分大小写 OR AND TO等关键词是区分大小写的,lucene只认大写的,小写的当做普通单词。
-
读写互斥性
同一时刻只能有一个对索引的写操作,在写的同时可以进行搜索 文件锁在写索引的过程中强行退出将在tmp目录留下一个lock文件,使以后的写操作无法进行,可以将其手工删除时间格式 -
lucene只支持一种时间格式yyMMddHHmmss,所以你传一个yy-MM-dd
HH:mm:ss的时间给lucene它是不会当作时间来处理的 -
设置boost
有些时候在搜索时某个字段的权重需要大一些,例如你可能认为标题中出现关键词的文章比正文中出现关键词的文章更有价值,你可以把标题的boost设置的更大,那么搜索结果会优先显示标题中出现关键词的文章
代码演示:
/**
* 新增文档/索引
* @throws IOException
*/
@Test
public void createIndexTest() throws IOException {
// 1.采集数据
// 2.创建文档对象
Document document = new Document();
document.add(new TextField("id", "12345", Field.Store.YES));
document.add(new TextField("name", "第二份文档数据", Field.Store.YES));
document.add(new TextField("time", "2023-01-27", Field.Store.YES));
// 3.创建分词器 标准分词器对英文分词效果好 对中文是单字分词
StandardAnalyzer standardAnalyzer = new StandardAnalyzer();
IKAnalyzer smartChineseAnalyzer = new IKAnalyzer();
// 4.创建Directory目录对象 目录对象表示索引库的位置
Directory fsDirectory = FSDirectory.open(Paths.get("/opt/lucene_dir"));
// 5.创建IndexWriterConfig对象 这个对象中指定且分次使用的分词器
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(smartChineseAnalyzer);
indexWriterConfig.setMaxBufferedDocs(10000); // 配置写入segment前缓冲区doc最大数目
// 6.创建IndexWriter输出流对象 指定输出的位置和使用的config初始化对象
IndexWriter indexWriter = new IndexWriter(fsDirectory, indexWriterConfig);
indexWriter.forceMerge(1000); // 多少个文档合并一个段 越大索引越快 搜索越慢
// 7.写入文档到索引库
indexWriter.addDocument(document);
// 8.释放资源
indexWriter.close();
}
/**
* 搜索
*/
@Test
public void testSearch() throws ParseException, IOException {
// 1. 创建分词器(对搜索的关键词进行分词使用)
// 注意: 分词器需要跟创建索引时的分词器一模一样
IKAnalyzer analyzer = new IKAnalyzer();
// 2. 创建查询对象, 第一个参数: *默认*查询域, 第二个参数, 使用的分词器
QueryParser parser = new QueryParser("id", analyzer);
// 3. 设置搜索关键词 可指定查询域 如"id:123" 不指定时用默认查询域
Query query = parser.parse("123456");
// 4. 创建Directory目录, 指定索引库的位置
Directory directory = FSDirectory.open(Paths.get("D:\\dir"));
// 5. 创建输入流对象
IndexReader indexReader = DirectoryReader.open(directory);
// 6. 创建搜索对象
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
// 7. 搜索, 并返回结果
TopDocs docs = indexSearcher.search(query, 10);
System.out.println("查询到数据:" + docs.totalHits);
// 8. 获取结果集
ScoreDoc[] scoreDocs = docs.scoreDocs;
// 9. 遍历结果集
if (scoreDocs != null) {
for (ScoreDoc scoreDoc : scoreDocs) {
// 获取查询到的文档id, 该id为lucene创建文档时自动分配的id
int docId = scoreDoc.doc;
// 通过文档id,读取文档
Document doc = indexSearcher.doc(docId);
System.out.println(doc.get("name"));
System.out.println(doc.get("time"));
System.out.println(doc.get("id"));
}
}
}
/**
* 索引库修改
*/
@Test
public void updateIndexTest() throws IOException {
// 需要变更成的内容
Document document = new Document();
document.add(new TextField("id", "123456",Field.Store.YES));
document.add(new TextField("name", "修改后的名称", Field.Store.YES));
// 创建分词器
IKAnalyzer analyzer = new IKAnalyzer();
// 创建目录对象
FSDirectory directory = FSDirectory.open(Paths.get("D:\\dir"));
// 创建配置对象
IndexWriterConfig config = new IndexWriterConfig(analyzer);
// 创建输出流对象
IndexWriter indexWriter = new IndexWriter(directory, config);
// 修改, 第一个参数: 修改条件,第二个参数: 修改成的内容
indexWriter.updateDocument(new Term("id", "123456"), document);
// 关闭资源
indexWriter.close();
}
/**
* 索引库删除
*/
@Test
public void deleteTest() throws IOException {
// 创建分词器
IKAnalyzer analyzer = new IKAnalyzer();
// 创建目录对象
FSDirectory directory = FSDirectory.open(Paths.get("D:\\dir"));
// 创建配置对象
IndexWriterConfig config = new IndexWriterConfig(analyzer);
// 创建输出流对象
IndexWriter indexWriter = new IndexWriter(directory, config);
// 修改, 第一个参数: 修改条件,第二个参数: 修改成的内容
indexWriter.deleteDocuments(new Term("id", "123456"));
// 关闭资源
indexWriter.close();
}
/**
* 使用第三方分词器(IK分词)
*/
@Test
public void testIkAnalyzer() throws IOException {
// 创建分词器
Analyzer analyzer = new IKAnalyzer();
// 创建Directory对象 声明索引库的位置
Directory directory = FSDirectory.open(Paths.get("/opt/lucene_dir"));
// 创建indexWriterConfig
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);
// 6.创建IndexWriter输出流对象 指定输出的位置和使用的config初始化对象
IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig);
Document document = new Document();
document.add(new TextField("id", "12346", Field.Store.YES));
document.add(new TextField("name", "华为手机4G至尊P30", Field.Store.YES));
document.add(new IntPoint("price", 9999));
document.add(new StoredField("price", 9999));
// 7.写入文档到索引库
indexWriter.addDocument(document);
// 8.释放资源
indexWriter.close();
}
/**
* 高级查询(文本搜索)
*/
@Test
public void testTextSearch() throws IOException, ParseException {
// 1. 创建分词器(对搜索的关键词进行分词使用)
// 注意: 分词器需要跟创建索引时的分词器一模一样
IKAnalyzer analyzer = new IKAnalyzer();
// 2. 创建查询对象, 第一个参数: *默认*查询域, 第二个参数, 使用的分词器
// QueryParser parser = new QueryParser("name", analyzer);
// 3. 设置搜索关键词 可指定查询域 如"id:123" 不指定时用默认查询域
// AND 取交集 OR 取并集
// QueryParser只能对文本进行搜索
String[] fields = {"id", "name", "brand"};
Map<String, Float> boost = new HashMap<>();
// 设置权重
boost.put("brand", 10000f);
MultiFieldQueryParser parser = new MultiFieldQueryParser(fields, analyzer, boost);
Query query = parser.parse("最新 or 华为");
// 4. 创建Directory目录, 指定索引库的位置
Directory directory = FSDirectory.open(Paths.get("/opt/lucene_dir"));
// 5. 创建输入流对象
IndexReader indexReader = DirectoryReader.open(directory);
// 6. 创建搜索对象
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
// 7. 搜索, 并返回结果
TopDocs docs = indexSearcher.search(query, 10);
System.out.println("查询到数据:" + docs.totalHits);
// 8. 获取结果集
ScoreDoc[] scoreDocs = docs.scoreDocs;
// 9. 遍历结果集
if (scoreDocs != null) {
for (ScoreDoc scoreDoc : scoreDocs) {
// 获取查询到的文档id, 该id为lucene创建文档时自动分配的id
int docId = scoreDoc.doc;
// 通过文档id,读取文档
Document doc = indexSearcher.doc(docId);
System.out.println(doc.get("name"));
System.out.println(doc.get("time"));
System.out.println(doc.get("id"));
}
}
}
/**
* 高级查询(范围搜索)
*/
@Test
public void testRangeSearch() throws IOException, ParseException {
// 1. 创建分词器(对搜索的关键词进行分词使用)
// 注意: 分词器需要跟创建索引时的分词器一模一样
IKAnalyzer analyzer = new IKAnalyzer();
// 2. 创建查询对象, 第一个参数: *默认*查询域, 第二个参数, 使用的分词器
Query query = IntPoint.newRangeQuery("price", 100, 9998);
// 4. 创建Directory目录, 指定索引库的位置
Directory directory = FSDirectory.open(Paths.get("/opt/lucene_dir"));
// 5. 创建输入流对象
IndexReader indexReader = DirectoryReader.open(directory);
// 6. 创建搜索对象
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
// 7. 搜索, 并返回结果
TopDocs docs = indexSearcher.search(query, 10);
System.out.println("查询到数据:" + docs.totalHits);
// 8. 获取结果集
ScoreDoc[] scoreDocs = docs.scoreDocs;
// 9. 遍历结果集
if (scoreDocs != null) {
for (ScoreDoc scoreDoc : scoreDocs) {
// 获取查询到的文档id, 该id为lucene创建文档时自动分配的id
int docId = scoreDoc.doc;
// 通过文档id,读取文档
Document doc = indexSearcher.doc(docId);
System.out.println(doc.get("name"));
System.out.println(doc.get("time"));
System.out.println(doc.get("id"));
System.out.println(doc.get("price"));
}
}
}
/**
* 高级查询(组合搜索)
*/
@Test
public void testComplexSearch() throws IOException, ParseException {
// 1. 创建分词器(对搜索的关键词进行分词使用)
// 注意: 分词器需要跟创建索引时的分词器一模一样
IKAnalyzer analyzer = new IKAnalyzer();
// 2. 创建查询对象, 第一个参数: *默认*查询域, 第二个参数, 使用的分词器
Query query1 = IntPoint.newRangeQuery("price", 100, 9998);
QueryParser parse = new QueryParser("name", analyzer);
Query query2 = parse.parse("华为");
// 3. 组合查询条件 MUST 必须 SHOULD 或 MUST_NOT 必须不
// 注意:如果查询条件都是must not, 则查询不到结果
BooleanQuery.Builder builder = new BooleanQuery.Builder();
builder.add(query1, BooleanClause.Occur.SHOULD);
builder.add(query2, BooleanClause.Occur.SHOULD);
BooleanQuery query = builder.build();
// 4. 创建Directory目录, 指定索引库的位置
Directory directory = FSDirectory.open(Paths.get("/opt/lucene_dir"));
// 5. 创建输入流对象
IndexReader indexReader = DirectoryReader.open(directory);
// 6. 创建搜索对象
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
// 7. 搜索, 并返回结果
TopDocs docs = indexSearcher.search(query, 10);
System.out.println("查询到数据:" + docs.totalHits);
// 8. 获取结果集
ScoreDoc[] scoreDocs = docs.scoreDocs;
// 9. 遍历结果集
if (scoreDocs != null) {
for (ScoreDoc scoreDoc : scoreDocs) {
// 获取查询到的文档id, 该id为lucene创建文档时自动分配的id
int docId = scoreDoc.doc;
// 通过文档id,读取文档
Document doc = indexSearcher.doc(docId);
System.out.println(doc.get("name"));
System.out.println(doc.get("time"));
System.out.println(doc.get("id"));
System.out.println(doc.get("price"));
}
}
}