1. 前言

1.1 为什么要用Lucene

原来的方式实现搜索功能,我们的搜索流程如下图:如果用户比较少而且数据库的数据量比较小,那么这种方式实现搜索功能在企业中是比较常见的。

现在的方案(使用Lucene),如下图:为了解决数据库压力和速度的问题,我们的数据库就变成了索引库,我们使用Lucene的API的来操作服务器上的索引库。这样完全和数据库进行了隔离

1.2 数据查询方法

1.2.1 顺序扫描法
  • 算法描述:所谓顺序扫描,例如要找内容包含一个字符串的文件,就是一个文档一个文档的看,对于每一个文档,从头看到尾,如果此文档包含此字符串,则此文档为我们要找的文件,接着看下一个文件,直到扫描完所有的文件。
  • 优点:查询准确率高
  • 缺点:查询速度会随着查询数据量的增大, 越来越慢
  • 使用场景
    • 数据库中的like关键字模糊查询
    • 文本编辑器的Ctrl + F 查询功能
1.2.2 倒排索引

先举一个例子:
例如我们使用新华字典查询汉字,新华字典有偏旁部首的目录(索引),我们查字首先查这个目录,找到这个目录中对应的偏旁部首,就可以通过这个目录中的偏旁部首找到这个字所在的位置(文档)。

Lucene会对文档建立倒排索引
1、 提取资源中关键信息, 建立索引 (目录)
2、 搜索时,根据关键字(目录),找到资源的位置

  • 算法描述:查询前会先将查询的内容提取出来组成文档(正文), 对文档进行切分词组成索引(目录), 索引和文档有关联关系, 查询的时候先查询索引, 通过索引找文档的这个过程叫做全文检索。
  • 切分词 : 就是将一句一句话切分成一个一个的词, 去掉停用词(的, 地, 得, a, an, the等)。去掉空格, 去掉标点符号, 大写字母转成小写字母, 去掉重复的词
  • 为什么倒排索引比顺序扫描快:因为索引可以去掉重复的词, 汉语常用的字和词大概等于, 字典加词典, 常用的英文在牛津词典也有收录.如果用计算机的速度查询, 字典+词典+牛津词典这些内容是非常快的. 但是用这些字典, 词典组成的文章却是千千万万不计其数. 索引的大小最多也就是字典+词典. 所以通过查询索引, 再通过索引和文档的关联关系找到文档速度比较快. 顺序扫描法则是直接去逐个查询那些不计其数的文章就算是计算的速度也会很慢
  • 优点
    • 查询准确率高
    • 查询速度快, 并且不会因为查询内容量的增加, 而使查询速度逐渐变慢
  • 缺点:索引文件会占用额外的磁盘空间, 也就是占用磁盘量会增大。
  • 使用场景:海量数据查询

1.3 什么是全文检索

计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式

1.4 全文检索技术应用场景

  1. 站内搜索 (baidu贴吧、论坛、 京东、 taobao)
  2. 垂直领域的搜索 (818工作网)
  3. 专业搜索引擎公司 (google、baidu)

1.5 什么是Lucene

Lucene是apache软件基金会4 jakarta项目组的一个子项目,是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言)。

Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文检索引擎。

目前已经有很多应用程序的搜索功能是基于 Lucene 的,比如 Eclipse 的帮助系统的搜索功能。Lucene能够为文本类型的数据建立索引,所以你只要能把你要索引的数据格式转化的文本的,Lucene 就能对你的文档进行索引和搜索。比如你要对一些 HTML 文档,PDF 文档进行索引的话你就首先需要把HTML 文档和 PDF 文档转化成文本格式的,然后将转化后的内容交给 Lucene 进行索引,然后把创建好的索引文件保存到磁盘或者内存中,最后根据用户输入的查询条件在索引文件上进行查询。不指定要索引的文档的格式也使 Lucene 能够几乎适用于所有的搜索应用程序。

  • Lucene是一套用于全文检索和搜寻的开源程式库,由Apache软件基金会支持和提供
  • Lucene提供了一个简单却强大的应用程式接口,能够做全文索引和搜寻, 在Java开发环境里
  • Lucene是一个成熟的免费开放源代码工具
  • Lucene并不是现成的搜索引擎产品,但可以用来制作搜索引擎产品

2. Lucene全文检索的流程

2.1 索引和搜索流程图


其中:

  • 绿色表示索引过程,对要搜索的原始内容进行索引构建一个索引库,索引过程包括:
    确定原始内容即要搜索的内容

    • 获得文档
    • 创建文档
    • 分析文档
    • 索引文档
  • 红色表示搜索过程,从索引库中搜索内容,搜索过程包括:
    用户通过搜索界面

    • 创建查询
    • 执行搜索,从索引库搜索
    • 渲染搜索结果

2.2 索引流程

对文档索引的过程,将用户要搜索的文档内容进行索引,索引存储在索引库(index)中。

2.2.1. 原始内容

原始内容是指要索引和搜索的内容。
原始内容包括互联网上的网页、数据库中的数据、磁盘上的文件等。

2.2.2. 获得文档(采集数据)

从互联网上、数据库、文件系统中等获取需要搜索的原始信息,这个过程就是信息采集,采集数据的目的是为了对原始内容进行索引。

采集数据分类:

  1. 对于互联网上网页,可以使用工具将网页抓取到本地生成html文件。
  2. 数据库中的数据,可以直接连接数据库读取表中的数据。
  3. 文件系统中的某个文件,可以通过I/O操作读取文件的内容。

在Internet上采集信息的软件通常称为爬虫或蜘蛛,也称为网络机器人,爬虫访问互联网上的每一个网页,将获取到的网页内容存储起来。

2.2.3. 创建文档

获取原始内容的目的是为了索引,在索引前需要将原始内容创建成文档(Document),文档中包括一个一个的域(Field),域中存储内容。

这里我们可以将磁盘上的一个文件当成一个document,Document中包括一些Field,如下图:

注意:每个Document可以有多个Field,不同的Document可以有不同的Field,同一个Document可以有相同的Field(域名和域值都相同)

2.2.4. 分析文档

将原始内容创建为包含域(Field)的文档(document),需要再对域中的内容进行分析,分析成为一个一个的单词。

比如下边的文档经过分析如下:

  • 原文档内容:
    vivo X23 8GB+128GB 幻夜蓝 全网通4G手机
    华为 HUAWEI 麦芒7 6G+64G 亮黑色 全网通4G手机
  • 分析后得到的词:
    vivo, x23, 8GB, 128GB, 幻夜, 幻夜蓝, 全网, 全网通, 网通, 4G, 手机, 华为, HUAWEI, 麦芒7。。。。
2.2.5. 索引文档

对所有文档分析得出的语汇单元进行索引,索引的目的是为了搜索,最终要实现只搜索被索引的语汇单元从而找到Document(文档)。

创建索引是对语汇单元索引,通过关键字找文档,这种索引的结构叫倒排索引结构。
倒排索引结构是根据内容(词汇)找文档,如下图:

倒排索引结构也叫反向索引结构,包括索引和文档两部分,索引即词汇表,它的规模较小,而文档集合较大。

2.2.6 Lucene底层存储结构

通过关键字找到对应的文档号,通过文档号找到文档:

2.3 搜索流程

搜索就是用户输入关键字,从索引中进行搜索的过程。根据关键字搜索索引,根据索引找到对应的文档,从而找到要搜索的内容。

2.3.1. 用户

就是使用搜索的角色,用户可以是自然人,也可以是远程调用的程序。

2.3.2. 用户搜索界面

全文检索系统提供用户搜索的界面供用户提交搜索的关键字,搜索完成展示搜索结果。如下图:

Lucene不提供制作用户搜索界面的功能,需要根据自己的需求开发搜索界面。

2.3.3. 创建查询

用户输入查询关键字执行搜索之前需要先构建一个查询对象,查询对象中可以指定查询要查询关键字、
要搜索的Field文档域等,查询对象会生成具体的查询语法,比如:
name:手机 : 表示要搜索name这个Field域中,内容为“手机”的文档。
name:华为 AND 手机 : 表示要搜索即包括关键字“华为” 并且也包括“手机”的文档。

2.3.4. 执行搜索

搜索索引过程:

  1. 根据查询语法在倒排索引词典表中分别找出对应搜索词的索引,从而找到索引所链接的文档链表。
    例如搜索语法为 “name:华为 AND 手机 ” 表示搜索出的文档中既要包括"华为"也要包括"手机"。
  2. 由于是AND,所以要对包含 华为 和 手机 词语的链表进行交集,得到文档链表应该包括每一个搜索词语
  3. 获取文档中的Field域数据。
2.3.5. 渲染结果

以一个友好的界面将查询结果展示给用户,用户根据搜索结果找自己想要的信息,为了帮助用户很快找到自己的结果,提供了很多展示的效果,比如搜索结果中将关键字高亮显示,百度提供的快照等。

3. Lucene操作入门

3.1 数据准备

  1. 数据库准备(略)

  2. dao准备(略)

  3. 项目准备(略)

3.2 索引过程

在目录中创建一个用于存放索引文件和文档文件的目录(文件名是英文)

3.2.1 创建索引库:

执行代码:

/*** 创建索引库*/
@Test
public void createIndexTest() throws Exception {//1. 采集数据SkuDao skuDao = new SkuDaoImpl();List<Sku> skuList = skuDao.querySkuList();//文档集合List<Document> docList = new ArrayList<>();for (Sku sku : skuList) {//2. 创建文档对象Document document = new Document();//创建域对象并且放入文档对象中document.add(new TextField("id", sku.getId(), Field.Store.YES));document.add(new TextField("name", sku.getName(), Field.Store.YES));document.add(new TextField("price", String.valueOf(sku.getPrice()), Field.Store.YES));document.add(new TextField("image", sku.getImage(), Field.Store.YES));document.add(new TextField("categoryName", sku.getCategoryName(), Field.Store.YES));document.add(new TextField("brandName", sku.getBrandName(), Field.Store.YES));//将文档对象放入到文档集合中docList.add(document);}//3. 创建分词器, StandardAnalyzer标准分词器, 对英文分词效果好, 对中文是单字分词, 也就是一个字就认为是一个词.Analyzer analyzer = new StandardAnalyzer();//4. 创建Directory目录对象, 目录对象表示索引库的位置Directory  dir = FSDirectory.open(Paths.get("C:\software-c\lucene-dir"));//5. 创建IndexWriterConfig对象, 这个对象中指定切分词使用的分词器IndexWriterConfig config = new IndexWriterConfig(analyzer);//6. 创建IndexWriter输出流对象, 指定输出的位置和使用的config初始化对象IndexWriter indexWriter = new IndexWriter(dir, config);//7. 写入文档到索引库for (Document doc : docList) {indexWriter.addDocument(doc);}//8. 释放资源indexWriter.close();
}

生成的文件:

3.2.2 使用luke工具查看索引库内容:
  1. 下载地址:https://github.com/DmitryKey/luke/releases

  2. 启动luke

  3. 打开刚刚生成的文件:

  4. 选中域对象点击show top terms可以查看分词后结果:

  5. 查看文档:

  6. 搜索文档中的数据:

    1. 对域进行筛选

    2. 如果上述查询operator选择的是or,就是分词查询,查询出包含华、为、手、机交集的数据,如果这里查看华为或者使用and,可以看到结果集少了很多

3.2.3 代码查看索引内容

代码:

 @Test
public void testIndexSearch() throws Exception {//1. 创建分词器(对搜索的关键词进行分词使用)//注意: 分词器要和创建索引的时候使用的分词器一模一样Analyzer analyzer = new StandardAnalyzer();//2. 创建查询对象,//第一个参数: 默认查询域, 如果查询的关键字中带搜索的域名, 则从指定域中查询, 如果不带域名则从, 默认搜索域中查询//第二个参数: 使用的分词器QueryParser queryParser = new QueryParser("name", analyzer);//3. 设置搜索关键词//华为手机Query query = queryParser.parse("华为手机");//4. 创建Directory目录对象, 指定索引库的位置Directory dir = FSDirectory.open(Paths.get("C:\\software-c\\lucene-dir"));//5. 创建输入流对象IndexReader indexReader = DirectoryReader.open(dir);//6. 创建搜索对象IndexSearcher indexSearcher = new IndexSearcher(indexReader);//7. 搜索, 并返回结果//第二个参数: 是返回多少条数据用于展示, 分页使用TopDocs topDocs = indexSearcher.search(query, 10);//获取查询到的结果集的总数, 打印System.out.println("=======count=======" + topDocs.totalHits);//8. 获取结果集ScoreDoc[] scoreDocs = topDocs.scoreDocs;//9. 遍历结果集if (scoreDocs != null) {for (ScoreDoc scoreDoc : scoreDocs) {//获取查询到的文档唯一标识, 文档id, 这个id是lucene在创建文档的时候自动分配的int  docID = scoreDoc.doc;//通过文档id, 读取文档Document doc = indexSearcher.doc(docID);System.out.println("==================================================");//通过域名, 从文档中获取域值System.out.println("===id==" + doc.get("id"));System.out.println("===name==" + doc.get("name"));System.out.println("===price==" + doc.get("price"));System.out.println("===image==" + doc.get("image"));System.out.println("===brandName==" + doc.get("brandName"));System.out.println("===categoryName==" + doc.get("categoryName"));}}//10. 关闭流
}

结果:

=======count=======24147
==================================================
===id==18182117877
===name==华为(HUAWEI) 华为 mate10 手机 亮黑色 4G+64G
===price==76300
===image==https://m.360buyimg.com/mobilecms/s720x720_jfs/t13615/278/1165446825/333206/55afe3aa/5a1bd4f4Nf02c806f.jpg!q70.jpg.webp
===brandName==华为
===categoryName==手机
==================================================
===id==18182117880
===name==华为(HUAWEI) 华为 mate10 手机 亮黑色 6G+128G
===price==53100
===image==https://m.360buyimg.com/mobilecms/s720x720_jfs/t13615/278/1165446825/333206/55afe3aa/5a1bd4f4Nf02c806f.jpg!q70.jpg.webp
===brandName==华为
===categoryName==手机
==================================================
===id==18182117882
===name==华为(HUAWEI) 华为 mate10 手机 亮黑色 6G+128G
===price==87300
===image==https://m.360buyimg.com/mobilecms/s720x720_jfs/t13615/278/1165446825/333206/55afe3aa/5a1bd4f4Nf02c806f.jpg!q70.jpg.webp
===brandName==华为
===categoryName==手机
==================================================
===id==18182117883
===name==华为(HUAWEI) 华为 mate10 手机 亮黑色 6G+128G
===price==29100
===image==https://m.360buyimg.com/mobilecms/s720x720_jfs/t13615/278/1165446825/333206/55afe3aa/5a1bd4f4Nf02c806f.jpg!q70.jpg.webp
===brandName==华为
===categoryName==手机
==================================================
===id==25387600556
===name==华为(HUAWEI) 华为 mate10 手机 亮黑色 6G+128G
===price==32500
===image==https://m.360buyimg.com/mobilecms/s720x720_jfs/t13615/278/1165446825/333206/55afe3aa/5a1bd4f4Nf02c806f.jpg!q70.jpg.webp
===brandName==华为
===categoryName==手机
==================================================
===id==39004962179
===name==华为(HUAWEI) 华为nova3 手机 亮黑色 6G+128G
===price==88800
===image==https://m.360buyimg.com/mobilecms/s720x720_jfs/t1/26940/35/866/202554/5c0dd453Eb6eeadcc/48ab841f324db426.jpg!q70.jpg.webp
===brandName==华为
===categoryName==手机
==================================================
===id==39004962180
===name==华为(HUAWEI) 华为nova3 手机 亮黑色 6G+128G
===price==80400
===image==https://m.360buyimg.com/mobilecms/s720x720_jfs/t1/26940/35/866/202554/5c0dd453Eb6eeadcc/48ab841f324db426.jpg!q70.jpg.webp
===brandName==华为
===categoryName==手机
==================================================
===id==39004962181
===name==华为(HUAWEI) 华为nova3 手机 亮黑色 6G+128G
===price==67400
===image==https://m.360buyimg.com/mobilecms/s720x720_jfs/t1/26940/35/866/202554/5c0dd453Eb6eeadcc/48ab841f324db426.jpg!q70.jpg.webp
===brandName==华为
===categoryName==手机
==================================================
===id==39004962182
===name==华为(HUAWEI) 华为nova3 手机 亮黑色 6G+128G
===price==28900
===image==https://m.360buyimg.com/mobilecms/s720x720_jfs/t1/26940/35/866/202554/5c0dd453Eb6eeadcc/48ab841f324db426.jpg!q70.jpg.webp
===brandName==华为
===categoryName==手机
==================================================
===id==39004962183
===name==华为(HUAWEI) 华为nova3 手机 亮黑色 6G+128G
===price==1000
===image==https://m.360buyimg.com/mobilecms/s720x720_jfs/t1/26940/35/866/202554/5c0dd453Eb6eeadcc/48ab841f324db426.jpg!q70.jpg.webp
===brandName==华为
===categoryName==手机

如果将华为手机改成华为,结果:可以看到结果少了很多

=======count=======8029
==================================================
。。。。

4. Field域类型

4.1 Field属性

Field是文档中的域,包括Field名和Field值两部分,一个文档可以包括多个Field,Document只是Field的一个承载体,Field值即为要索引的内容,也是要搜索的内容。

4.1.1 是否分词(tokenized)
  • 是:作分词处理,即将Field值进行分词,分词的目的是为了索引。
    比如:商品名称、商品描述等,这些内容用户要输入关键字搜索,由于搜索的内容格式大、内容多需要分词后将语汇单元建立索引
  • 否:不作分词处理
    比如:商品id、订单号、身份证号等
4.1.2 是否索引(indexed)
  • 是:进行索引。将Field分词后的词或整个Field值进行索引,存储到索引域,索引的目的是为了搜索。
    比如:商品名称、商品描述分析后进行索引,订单号、身份证号不用分词但也要索引,这些将来都要作为查询条件。
  • 否:不索引。
    比如:图片路径、文件路径等,不用作为查询条件的不用索引。
4.1.3 是否存储(stored)
  • 是:将Field值存储在文档域中,存储在文档域中的Field才可以从Document中获取。
    比如:商品名称、订单号,凡是将来要从Document中获取的Field都要存储。
  • 否:不存储Field值
    比如:商品描述,内容较大不用存储。如果要向用户展示商品描述可以从系统的关系数据库中获取。

4.2 Field常用类型

下边列出了开发中常用 的Filed类型,注意Field的属性,根据需求选择:

4.3 改造原有代码:

/*** 创建索引库*/
@Test
public void createIndexTest() throws Exception {//1. 采集数据SkuDao skuDao = new SkuDaoImpl();List<Sku> skuList = skuDao.querySkuList();//文档集合List<Document> docList = new ArrayList<>();for (Sku sku : skuList) {//2. 创建文档对象Document document = new Document();//创建域对象并且放入文档对象中/*** 是否分词: 否, 因为主键分词后无意义* 是否索引: 是, 如果根据id主键查询, 就必须索引* 是否存储: 是, 因为主键id比较特殊, 可以确定唯一的一条数据, 在业务上一般有重要所用, 所以存储*      存储后, 才可以获取到id具体的内容*/document.add(new StringField("id", sku.getId(), Field.Store.YES));/*** 是否分词: 是, 因为名称字段需要查询, 并且分词后有意义所以需要分词* 是否索引: 是, 因为需要根据名称字段查询* 是否存储: 是, 因为页面需要展示商品名称, 所以需要存储*/document.add(new TextField("name", sku.getName(), Field.Store.YES));/*** 是否分词: 是(因为lucene底层算法规定, 如果根据价格范围查询, 必须分词)* 是否索引: 是, 需要根据价格进行范围查询, 所以必须索引* 是否存储: 是, 因为页面需要展示价格*/document.add(new IntPoint("price", sku.getPrice()));document.add(new StoredField("price", sku.getPrice()));/*** 是否分词: 否, 因为不查询, 所以不索引, 因为不索引所以不分词* 是否索引: 否, 因为不需要根据图片地址路径查询* 是否存储: 是, 因为页面需要展示商品图片*/document.add(new StoredField("image", sku.getImage()));/*** 是否分词: 否, 因为分类是专有名词, 是一个整体, 所以不分词* 是否索引: 是, 因为需要根据分类查询* 是否存储: 是, 因为页面需要展示分类*/document.add(new StringField("categoryName", sku.getCategoryName(), Field.Store.YES));/*** 是否分词: 否, 因为品牌是专有名词, 是一个整体, 所以不分词* 是否索引: 是, 因为需要根据品牌进行查询* 是否存储: 是, 因为页面需要展示品牌*/document.add(new StringField("brandName", sku.getBrandName(), Field.Store.YES));//将文档对象放入到文档集合中docList.add(document);}//3. 创建分词器, StandardAnalyzer标准分词器, 对英文分词效果好, 对中文是单字分词, 也就是一个字就认为是一个词.Analyzer analyzer = new StandardAnalyzer();//4. 创建Directory目录对象, 目录对象表示索引库的位置Directory  dir = FSDirectory.open(Paths.get("C:\\software-c\\lucene-dir"));//5. 创建IndexWriterConfig对象, 这个对象中指定切分词使用的分词器IndexWriterConfig config = new IndexWriterConfig(analyzer);//6. 创建IndexWriter输出流对象, 指定输出的位置和使用的config初始化对象IndexWriter indexWriter = new IndexWriter(dir, config);//7. 写入文档到索引库for (Document doc : docList) {indexWriter.addDocument(doc);}//8. 释放资源indexWriter.close();
}

5. 索引维护

5.1 需求

管理人员通过电商系统更改图书信息,这时更新的是关系数据库,如果使用lucene搜索图书信息,需要在数据库表book信息变化时及时更新lucene索引库。

5.2 添加索引

调用 indexWriter.addDocument(doc)添加索引。
参考入门程序的创建索引。

5.3 修改索引

更新索引是先删除再添加,建议对更新需求采用此方法并且要保证对已存在的索引执行更新,可以先查询出来,确定更新记录存在执行更新操作。

如果更新索引的目标文档对象不存在,则执行添加。

代码:

/*** 索引库修改操作* @throws Exception*/
@Test
public void updateIndexTest() throws Exception {//需要变更成的内容Document document = new Document();document.add(new StringField("id", "100000003145", Field.Store.YES));document.add(new TextField("name", "xxxx", Field.Store.YES));document.add(new IntPoint("price", 123));document.add(new StoredField("price", 123));document.add(new StoredField("image", "xxxx.jpg"));document.add(new StringField("categoryName", "手机", Field.Store.YES));document.add(new StringField("brandName", "华为", Field.Store.YES));//3. 创建分词器, StandardAnalyzer标准分词器, 对英文分词效果好, 对中文是单字分词, 也就是一个字就认为是一个词.Analyzer analyzer = new StandardAnalyzer();//4. 创建Directory目录对象, 目录对象表示索引库的位置Directory  dir = FSDirectory.open(Paths.get("E:\\dir"));//5. 创建IndexWriterConfig对象, 这个对象中指定切分词使用的分词器IndexWriterConfig config = new IndexWriterConfig(analyzer);//6. 创建IndexWriter输出流对象, 指定输出的位置和使用的config初始化对象IndexWriter indexWriter = new IndexWriter(dir, config);//修改, 第一个参数: 修改条件, 第二个参数: 修改成的内容indexWriter.updateDocument(new Term("id", "100000003145"), document);//8. 释放资源indexWriter.close();
}

结果:可以通过id在luke中查询,不多演示

5.4 删除索引

根据Term项删除索引,满足条件的将全部删除。

/*** 测试根据条件删除* @throws Exception*/
@Test
public void deleteIndexTest() throws Exception {//3. 创建分词器, StandardAnalyzer标准分词器, 对英文分词效果好, 对中文是单字分词, 也就是一个字就认为是一个词.Analyzer analyzer = new StandardAnalyzer();//4. 创建Directory目录对象, 目录对象表示索引库的位置Directory  dir = FSDirectory.open(Paths.get("E:\\dir"));//5. 创建IndexWriterConfig对象, 这个对象中指定切分词使用的分词器IndexWriterConfig config = new IndexWriterConfig(analyzer);//6. 创建IndexWriter输出流对象, 指定输出的位置和使用的config初始化对象IndexWriter indexWriter = new IndexWriter(dir, config);//测试根据条件删除//indexWriter.deleteDocuments(new Term("id", "100000003145"));//测试删除所有内容indexWriter.deleteAll();//8. 释放资源indexWriter.close();
}

不多演示。

6. 分词器

6.1 分词理解

在对Document中的内容进行索引之前,需要使用分词器进行分词 ,分词的目的是为了搜索。分词的主要过程就是先分词后过滤。

  • 分词:采集到的数据会存储到document对象的Field域中,分词就是将Document中Field的value值切分成一个一个的词。
  • 过滤:包括去除标点符号过滤、去除停用词过滤(的、是、a、an、the等)、大写转小写(大写在lucene中是关键字)、词的形还原(复数形式转成单数形参、过去式转成现在式。。。)等。

什么是停用词?停用词是为节省存储空间和提高搜索效率,搜索引擎在索引页面或处理搜索请求时会自动忽略某些字或词,这些字或词即被称为Stop Words(停用词)。比如语气助词、副词、介词、连接词等,通常自身并无明确的意义,只有将其放入一个完整的句子中才有一定作用,如常见的“的”、“在”、“是”、“啊”等。

对于分词来说,不同的语言,分词规则不同。Lucene作为一个工具包提供不同国家的分词器

6.2 Analyzer使用时机

6.2.1 索引时使用Analyzer

输入关键字进行搜索,当需要让该关键字与文档域内容所包含的词进行匹配时需要对文档域内容进行分析,需要经过Analyzer分析器处理生成语汇单元(Token)。分析器分析的对象是文档中的Field域。当Field的属性tokenized(是否分词)为true时会对Field值进行分析,如下图:

6.2.2 搜索时使用Analyzer

对搜索关键字进行分析和索引分析一样,使用Analyzer对搜索关键字进行分析、分词处理,使用分析后每个词语进行搜索。比如:搜索关键字:spring web ,经过分析器进行分词,得出:spring和web拿词去索引词典表查找 ,找到索引链接到Document,解析Document内容。

对于匹配整体Field域的查询可以在搜索时不分析,比如根据订单号、身份证号查询等。

注意:搜索使用的分析器要和索引使用的分析器一致

6.3 Lucene原生分词器

以下是Lucene中自带的分词器

6.3.1 StandardAnalyzer

特点 :
Lucene提供的标准分词器, 可以对用英文进行分词, 对中文是单字分词, 也就是一个字就认为是一个词

如下是org.apache.lucene.analysis.standard.standardAnalyzer的部分源码:

protected TokenStreamComponents createComponents(String fieldName) {final StandardTokenizer src = new StandardTokenizer();src.setMaxTokenLength(this.maxTokenLength);TokenStream tok = new LowerCaseFilter(src);TokenStream tok = new StopFilter(tok, this.stopwords);return new TokenStreamComponents(src, tok) {protected void setReader(Reader reader) {src.setMaxTokenLength(StandardAnalyzer.this.maxTokenLength);super.setReader(reader);}};
}

Tokenizer就是分词器,负责将reader转换为语汇单元即进行分词处理,Lucene提供了很多的分词器,也可以使用第三方的分词,比如IKAnalyzer一个中文分词器。

TokenFilter是分词过滤器,负责对语汇单元进行过滤,TokenFilter可以是一个过滤器链儿,Lucene提供了很多的分词器过滤器,比如大小写转换、去除停用词等。

如下图是语汇单元的生成过程:

从一个Reader字符流开始,创建一个基于Reader的Tokenizer分词器,经过三个TokenFilter生成语汇单元Token。

比如下边的文档经过分析器分析如下:

  • 原文档内容:Lucene is a java full-text search engine

  • 分析后得到的多个语汇单元:lucene、java、full、text、search、engine

6.3.2 WhitespaceAnalyzer

特点 :
仅仅是去掉了空格,没有其他任何操作,不支持中文。

测试代码:
略。

6.3.3 SimpleAnalyzer

特点 :
将除了字母以外的符号全部去除,并且将所有字母变为小写,需要注意的是这个分词器同样把数字也去除了,同样不支持中文。

测试:略

6.3.4 CJKAnalyzer

特点 :
这个支持中日韩文字,前三个字母也就是这三个国家的缩写。对中文是二分法分词, 去掉空格, 去掉标点符号。个人感觉对中文支持依旧很烂

测试代码:
略。

6.3.5 SmartChineseAnalyzer

特点 :
对中文支持也不是很好,扩展性差,扩展词库,禁用词库和同义词库等不好处理。

代码:略

6.4 第三方中文分词器

6.4.1 什么是中文分词器

学过英文的都知道,英文是以单词为单位的,单词与单词之间以空格或者逗号句号隔开。所以对于英文,我们可以简单以空格判断某个字符串是否为一个单词,比如I love China,love 和 China很容易被程序区分开来。

而中文则以字为单位,字又组成词,字和词再组成句子。中文“我爱中国”就不一样了,电脑不知道“中 国”是一个词语还是“爱中”是一个词语。

把中文的句子切分成有意义的词,就是中文分词,也称切词。我爱中国,分词的结果是:我、爱、中国。

6.4.2 第三方中文分词器简介
  • paoding: 庖丁解牛最新版在 https://code.google.com/p/paoding/ 中最多支持Lucene 3.0,且最新提交的代码在 2008-06-03,在svn中最新也是2010年提交,已经过时,不予考虑。
  • mmseg4j:最新版已从 https://code.google.com/p/mmseg4j/ 移至 https://github.com/chenlb/mmseg4j-solr,支持Lucene 4.10,且在github中最新提交代码是2014年6月,从09年~14年一共有:18个版本,也就是一年几乎有3个大小版本,有较大的活跃度,用了mmseg算法。
  • IK-analyzer: 最新版在https://code.google.com/p/ik-analyzer/上,支持Lucene 4.10从2006年12月推出1.0版开始, IKAnalyzer已经推出了4个大版本。最初,它是以开源项目Luence为应用主体的,结合词典分词和文法分析算法的中文分词组件。从3.0版本开 始,IK发展为面向Java的公用分词组件,独立于Lucene项目,同时提供了对Lucene的默认优化实现。在2012版本中,IK实现了简单的分词 歧义排除算法,标志着IK分词器从单纯的词典分词向模拟语义分词衍化。 但是也就是2012年12月后没有在更新。
  • ansj_seg:最新版本在 https://github.com/NLPchina/ansj_seg tags仅有1.1版本,从2012年到2014年更新了大小6次,但是作者本人在2014年10月10日说明:“可能我以后没有精力来维护ansj_seg了”,现在由”nlp_china”管理。2014年11月有更新。并未说明是否支持Lucene,是一个由CRF(条件随机场)算法所做的分词算法。
  • imdict-chinese-analyzer:最新版在 https://code.google.com/p/imdict-chinese-analyzer/ , 最新更新也在2009年5月,下载源码,不支持Lucene 4.10 。是利用HMM(隐马尔科夫链)算法。
  • Jcseg:最新版本在git.oschina.net/lionsoul/jcseg,支持Lucene 4.10,作者有较高的活跃度。利用mmseg算法。
6.4.3 使用中文分词器IKAnalyzer

IKAnalyzer继承Lucene的Analyzer抽象类,使用IKAnalyzer和Lucene自带的分析器方法一样,将
Analyzer测试代码改为IKAnalyzer测试中文分词效果。
如果使用中文分词器ik-analyzer,就需要在索引和搜索程序中使用一致的分词器:IK-analyzer。

  1. 从github上拉取ik-analyzer项目,并install到本地仓库

  2. 添加依赖,,pom.xml中加入依赖

<dependency> <groupId>org.wltea.ik-analyzer</groupId> <artifactId>ik-analyzer</artifactId> <version>8.1.0</version>
</dependency>
  1. 测试代码:
/*** 使用第三方分词器(IK分词)* 特点: 支持中文语义分析, 提供停用词典, 提供扩展词典, 供程序员扩展使用* @throws Exception*/
@Test
public void TestIKAnalyzer() throws Exception{// 1. 创建分词器,分析文档,对文档进行分词Analyzer analyzer = new IKAnalyzer();// 2. 创建Directory对象,声明索引库的位置Directory directory = FSDirectory.open(Paths.get("C:\\software-c\\lucene-dir"));// 3. 创建IndexWriteConfig对象,写入索引需要的配置IndexWriterConfig config = new IndexWriterConfig(analyzer);// 4.创建IndexWriter写入对象IndexWriter indexWriter = new IndexWriter(directory, config);// 5.写入到索引库,通过IndexWriter添加文档对象documentDocument doc = new Document();doc.add(new TextField("name", "vivo X23 8GB+128GB 幻夜蓝,水滴屏全面屏,游戏手机.移动联通电信全网通4G手机", Field.Store.YES));indexWriter.addDocument(doc);// 6.释放资源indexWriter.close();}
  1. 结果:
6.4.4 扩展中文词库

如果想配置扩展词和停用词,就创建扩展词的文件和停用词的文件。

从ikanalyzer包中拷贝配置文件

拷贝到资源文件夹中

IKAnalyzer.cfg.xml配置文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties><comment>IK Analyzer 扩展配置</comment><!--用户可以在这里配置自己的扩展字典 --><entry key="ext_dict">ext.dic;</entry><!--用户可以在这里配置自己的扩展停止词字典--><entry key="ext_stopwords">stopword.dic;</entry></properties>
  • 停用词典stopword.dic作用 :
    停用词典中的词例如: a, an, the, 的, 地, 得等词汇, 凡是出现在停用词典中的字或者词, 在切分词的时候会被过滤掉.
  • 扩展词典ext.dic作用 :
    扩展词典中的词例如: 传智播客, 黑马程序员, 贵州茅台等专有名词, 在汉语中一些公司名称, 行业名称, 分 类, 品牌等不是汉语中的词汇, 是专有名词. 这些分词器默认不识别, 所以需要放入扩展词典中, 效果是被强制分成一个词

7. Lucene高级搜索

7.1 文本搜索

QueryParser支持默认搜索域, 第一个参数为默认搜索域。
如果在执行parse方法的时候, 查询语法中包含域名则从指定的这个域名中搜索, 如果只有查询的关键字,则从默认搜索域中搜索结果。

需求描述 : 查询名称中包含华为手机关键字的结果.
测试代码:

@Test
public void testIndexSearch() throws Exception {//1. 创建分词器(对搜索的关键词进行分词使用)//注意: 分词器要和创建索引的时候使用的分词器一模一样Analyzer analyzer = new IKAnalyzer();//2. 创建查询对象,//第一个参数: 默认查询域, 如果查询的关键字中带搜索的域名, 则从指定域中查询, 如果不带域名则从, 默认搜索域中查询//第二个参数: 使用的分词器QueryParser queryParser = new QueryParser("name", analyzer);//3. 设置搜索关键词//华为手机(OR)// 华为 AND 手机,求交集// 华为 OR 手机,求并集Query query = queryParser.parse("华为 AND 手机");//4. 创建Directory目录对象, 指定索引库的位置Directory dir = FSDirectory.open(Paths.get("C:\\software-c\\lucene-dir"));//5. 创建输入流对象IndexReader indexReader = DirectoryReader.open(dir);//6. 创建搜索对象IndexSearcher indexSearcher = new IndexSearcher(indexReader);//7. 搜索, 并返回结果//第二个参数: 是返回多少条数据用于展示, 分页使用TopDocs topDocs = indexSearcher.search(query, 10);//获取查询到的结果集的总数, 打印System.out.println("=======count=======" + topDocs.totalHits);//8. 获取结果集ScoreDoc[] scoreDocs = topDocs.scoreDocs;//9. 遍历结果集if (scoreDocs != null) {for (ScoreDoc scoreDoc : scoreDocs) {//获取查询到的文档唯一标识, 文档id, 这个id是lucene在创建文档的时候自动分配的int  docID = scoreDoc.doc;//通过文档id, 读取文档Document doc = indexSearcher.doc(docID);System.out.println("==================================================");//通过域名, 从文档中获取域值System.out.println("===id==" + doc.get("id"));System.out.println("===name==" + doc.get("name"));System.out.println("===price==" + doc.get("price"));System.out.println("===image==" + doc.get("image"));System.out.println("===brandName==" + doc.get("brandName"));System.out.println("===categoryName==" + doc.get("categoryName"));}}//10. 关闭流
}

7.2 数值范围搜索

需求描述 : 查询价格大于等于100, 小于等于1000的商品
测试代码:

/*** 数值范围查询* @throws Exception*/
@Test
public void testRangeQuery() throws Exception {//1. 创建分词器(对搜索的关键词进行分词使用)//注意: 分词器要和创建索引的时候使用的分词器一模一样Analyzer analyzer = new IKAnalyzer();//2. 创建查询对象,Query query = IntPoint.newRangeQuery("price", 100, 1000);//4. 创建Directory目录对象, 指定索引库的位置Directory dir = FSDirectory.open(Paths.get("C:\\software-c\\lucene-dir"));//5. 创建输入流对象IndexReader indexReader = DirectoryReader.open(dir);//6. 创建搜索对象IndexSearcher indexSearcher = new IndexSearcher(indexReader);//7. 搜索, 并返回结果//第二个参数: 是返回多少条数据用于展示, 分页使用TopDocs topDocs = indexSearcher.search(query, 10);//获取查询到的结果集的总数, 打印System.out.println("=======count=======" + topDocs.totalHits);//8. 获取结果集ScoreDoc[] scoreDocs = topDocs.scoreDocs;//9. 遍历结果集if (scoreDocs != null) {for (ScoreDoc scoreDoc : scoreDocs) {//获取查询到的文档唯一标识, 文档id, 这个id是lucene在创建文档的时候自动分配的int  docID = scoreDoc.doc;//通过文档id, 读取文档Document doc = indexSearcher.doc(docID);System.out.println("==================================================");//通过域名, 从文档中获取域值System.out.println("===id==" + doc.get("id"));System.out.println("===name==" + doc.get("name"));System.out.println("===price==" + doc.get("price"));System.out.println("===image==" + doc.get("image"));System.out.println("===brandName==" + doc.get("brandName"));System.out.println("===categoryName==" + doc.get("categoryName"));}}//10. 关闭流
}

7.3 组合搜索:

需求描述 : 查询价格大于等于100, 小于等于1000, 并且名称中不包含华为手机关键字的商品

  • BooleanClause.Occur.MUST:必须 相当于and, 并且
  • BooleanClause.Occur.MUST_NOT:必须不 相当于not, 非
  • BooleanClause.Occur.SHOULD:应该 相当于or, 或者

注意 : 如果逻辑条件中, 只有MUST_NOT, 或者多个逻辑条件都是MUST_NOT, 无效, 查询不出任何数据

测试代码:

/*** 组合查询* @throws Exception*/
@Test
public void testBooleanQuery() throws Exception {//1. 创建分词器(对搜索的关键词进行分词使用)//注意: 分词器要和创建索引的时候使用的分词器一模一样Analyzer analyzer = new IKAnalyzer();//2. 创建查询对象,Query query1 = IntPoint.newRangeQuery("price", 100, 1000);QueryParser queryParser = new QueryParser("name", analyzer);//3. 设置搜索关键词//华 OR  为   手   机Query query2 = queryParser.parse("华为手机");//创建布尔查询对象(组合查询对象)/***  BooleanClause.Occur.MUST 必须相当于and, 也就是并且的关系*  BooleanClause.Occur.SHOULD 应该相当于or, 也就是或者的关系*  BooleanClause.Occur.MUST_NOT 不必须, 相当于not, 非*  注意: 如果查询条件都是MUST_NOT, 或者只有一个查询条件, 然后这一个查询条件是MUST_NOT则*  查询不出任何数据.*/BooleanQuery.Builder query = new BooleanQuery.Builder();query.add(query1, BooleanClause.Occur.MUST);query.add(query2, BooleanClause.Occur.MUST);//4. 创建Directory目录对象, 指定索引库的位置Directory dir = FSDirectory.open(Paths.get("C:\\software-c\\lucene-dir"));//5. 创建输入流对象IndexReader indexReader = DirectoryReader.open(dir);//6. 创建搜索对象IndexSearcher indexSearcher = new IndexSearcher(indexReader);//7. 搜索, 并返回结果//第二个参数: 是返回多少条数据用于展示, 分页使用TopDocs topDocs = indexSearcher.search(query.build(), 10);//获取查询到的结果集的总数, 打印System.out.println("=======count=======" + topDocs.totalHits);//8. 获取结果集ScoreDoc[] scoreDocs = topDocs.scoreDocs;//9. 遍历结果集if (scoreDocs != null) {for (ScoreDoc scoreDoc : scoreDocs) {//获取查询到的文档唯一标识, 文档id, 这个id是lucene在创建文档的时候自动分配的int  docID = scoreDoc.doc;//通过文档id, 读取文档Document doc = indexSearcher.doc(docID);System.out.println("==================================================");//通过域名, 从文档中获取域值System.out.println("===id==" + doc.get("id"));System.out.println("===name==" + doc.get("name"));System.out.println("===price==" + doc.get("price"));System.out.println("===image==" + doc.get("image"));System.out.println("===brandName==" + doc.get("brandName"));System.out.println("===categoryName==" + doc.get("categoryName"));}}//10. 关闭流
}

7.4 实际使用案例:

 @Override
public ResultModel query(String queryString, String price, Integer page) throws Exception {long startTime = System.currentTimeMillis();//1. 需要使用的对象封装ResultModel resultModel = new ResultModel();//从第几条开始查询int start = (page - 1) * PAGE_SIZE;//查询到多少条为止Integer end = page * PAGE_SIZE;//创建分词器Analyzer analyzer = new StandardAnalyzer();//创建组合查询对象BooleanQuery.Builder builder = new BooleanQuery.Builder();//2. 根据查询关键字封装查询对象QueryParser queryParser = new QueryParser("name", analyzer);Query query1 = null;//判断传入的查询关键字是否为空, 如果为空查询所有, 如果不为空, 则根据关键字查询if (StringUtils.isEmpty(queryString)) {query1 = queryParser.parse("*:*");} else {query1 = queryParser.parse(queryString);}//将关键字查询对象, 封装到组合查询对象中builder.add(query1, BooleanClause.Occur.MUST);//3. 根据价格范围封装查询对象if (!StringUtils.isEmpty(price)) {String[] split = price.split("-");Query query2 = IntPoint.newRangeQuery("price", Integer.parseInt(split[0]), Integer.parseInt(split[1]));//将价格查询对象, 封装到组合查询对象中builder.add(query2, BooleanClause.Occur.MUST);}//4. 创建Directory目录对象, 指定索引库的位置/*** 使用MMapDirectory消耗的查询时间* ====消耗时间为=========324ms* ====消耗时间为=========18ms*/Directory directory = FSDirectory.open(Paths.get("C:\\software-c\\lucene-dir"));//5. 创建输入流对象IndexReader reader = DirectoryReader.open(directory);//6. 创建搜索对象IndexSearcher indexSearcher = new IndexSearcher(reader);//7. 搜索并获取搜索结果TopDocs topDocs = indexSearcher.search(builder.build(), end);//8. 获取查询到的总条数resultModel.setRecordCount(topDocs.totalHits);//9. 获取查询到的结果集ScoreDoc[] scoreDocs = topDocs.scoreDocs;long endTime = System.currentTimeMillis();System.out.println("====消耗时间为=========" + (endTime - startTime) + "ms");//10. 遍历结果集封装返回的数据List<Sku> skuList = new ArrayList<>();if (scoreDocs != null) {for (int i = start; i < end; i ++) {//通过查询到的文档编号, 找到对应的文档对象Document document = reader.document(scoreDocs[i].doc);//封装Sku对象Sku sku = new Sku();sku.setId(document.get("id"));sku.setPrice(Integer.parseInt(document.get("price")));sku.setImage(document.get("image"));sku.setName(document.get("name"));sku.setBrandName(document.get("brandName"));sku.setCategoryName(document.get("categoryName"));skuList.add(sku);}}//封装查询到的结果集resultModel.setSkuList(skuList);//封装当前页resultModel.setCurPage(page);//总页数Long pageCount = topDocs.totalHits % PAGE_SIZE > 0 ? (topDocs.totalHits/PAGE_SIZE) + 1 : topDocs.totalHits/PAGE_SIZE;resultModel.setPageCount(pageCount);return resultModel;
}

8. Lucene底层储存结构(高级)

8.1 详细理解lucene存储结构

8.1.1 存储结构

8.1.2 索引(index)
  • 一个目录一个索引,在Lucene中一个索引是放在一个文件夹中的
8.1.3 段(Segment) :
  • 一个索引(逻辑索引)由多个段组成, 多个段可以合并, 以减少读取内容时候的磁盘IO。
  • Lucene中的数据写入会先写内存的一个Buffer,当Buffer内数据到一定量后会被flush成一个Segment,每个Segment有自己独立的索引,可独立被查询,但数据永远不能被更改。这种模式避免了随机写,数据写入都是批量追加,能达到很高的吞吐量。Segment中写入的文档不可被修改,但可被删除,删除的方式也不是在文件内部原地更改,而是会由另外一个文件保存需要被删除的文档的DocID,保证数据文件不可被修改。Index的查询需要对多个Segment进行查询并对结果进行合并,还需要处理被删除的文档,为了对查询进行优化,Lucene会有策略对多个Segment进行合并
8.1.4 文档(Document) :
  • 文档是我们建索引的基本单位,不同的文档是保存在不同的段中的,一个段可以包含多篇文档。
  • 新添加的文档是单独保存在一个新生成的段中,随着段的合并,不同的文档合并到同一个段中。
8.1.5 域(Field) :
  • 一篇文档包含不同类型的信息,可以分开索引,比如标题,时间,正文,描述等,都可以保存在不同的域里。
  • 不同域的索引方式可以不同。
8.1.6 词(Term) :
  • 词是索引的最小单位,是经过词法分析和语言处理后的字符串

8.2 索引库物理文件

8.3 索引库文件扩展名对照表

8.4 词典的构建

为何Lucene大数据量搜索快, 要分两部分来看 :

  • 一点是因为底层的倒排索引存储结构.
  • 另一点就是查询关键字的时候速度快, 因为词典的索引结构
8.4.1 词典数据结构对比

倒排索引中的词典位于内存,其结构尤为重要,有很多种词典结构,各有各的优缺点,最简单如排序数组,通过二分查找来检索数据,更快的有哈希表,磁盘查找有B树、B+树,但一个能支持TB级数据的倒排索引结构需要在时间和空间上有个平衡,下图列了一些常见词典的优缺点:

Lucene3.0之前使用的也是跳跃表结构,后换成了FST,但跳跃表在Lucene其他地方还有应用如倒排表合并和文档号索引。

8.4.2 跳跃表原理

Lucene3.0版本之前使用的跳跃表结构后换成了FST结构

  • 优点:结构简单、跳跃间隔、级数可控,Lucene3.0之前使用的也是跳跃表结构,,但跳跃表在Lucene其他地方还有应用如倒排表合并和文档号索引。
  • 缺点 :模糊查询支持不好.
8.4.3 单链表 :

单链表中查询一个元素即使是有序的,我们也不能通过二分查找法的方式缩减查询时间。
通俗的讲也就是按照链表顺序一个一个找.
举例: 查找85这个节点, 需要查找7次

8.4.3.1 跳跃表:

举例: 查询85这个节点, 一共需要查询6次

  1. 在level3层, 查询3次, 查询到1结尾, 退回到37节点
  2. 在level2层, 从37节点开始查询, 查询2次, 查询到1结尾, 退回到71节点
  3. 在level1层, 从71节点开始查询, 查询1次, 查询到85节点

8.4.3.2 FST原理简析

Lucene现在采用的数据结构为FST,它的特点就是: 优点:内存占用率低,压缩率一般在3倍~20倍之间、模糊查询支持好、查询快 缺点:结构复杂、输入要求有序、更新不易

已知FST要求输入有序,所以Lucene会将解析出来的文档单词预先排序,然后构建FST,我们假设输入为abd,abe,acf,acg,那么整个构建过程如下:

例:
输入数据:

输入的数据如下:

存储的结果如下:

9. Lucene优化

9.1 解决大量磁盘IO

  • config.setMaxBufferedDocs(100000); 控制写入一个新的segment前内存中保存的document的数目,设置较大的数目可以加快建索引速度。
    数值越大索引速度越快, 但是会消耗更多的内存
  • indexWriter.forceMerge(文档数量); 设置N个文档合并为一个段
    数值越大索引速度越快, 搜索速度越慢; 值越小索引速度越慢, 搜索速度越快
    更高的值意味着索引期间更低的段合并开销,但同时也意味着更慢的搜索速度,因为此时的索引通常会包含更多的段。如果该值设置的过高,能获得更高的索引性能。但若在最后进行索引优化,那么较低的值会带来更快的搜索速度,因为在索引操作期间程序会利用并发机制完成段合并操作。故建议对程序分别进行高低多种值的测试,利用计算机的实际性能来告诉你最优值

创建索引代码优化测试:

/*** 测试创建索引速度优化* @throws Exception*/
@Test
public void createIndexTest2() throws Exception {//1. 采集数据SkuDao skuDao = new SkuDaoImpl();List<Sku> skuList = skuDao.querySkuList();//文档集合List<Document> docList = new ArrayList<>();for (Sku sku : skuList) {//2. 创建文档对象Document document = new Document();document.add(new StringField("id", sku.getId(), Field.Store.YES));document.add(new TextField("name", sku.getName(), Field.Store.YES));document.add(new IntPoint("price", sku.getPrice()));document.add(new StoredField("price", sku.getPrice()));document.add(new StoredField("image", sku.getImage()));document.add(new StringField("categoryName", sku.getCategoryName(), Field.Store.YES));document.add(new StringField("brandName", sku.getBrandName(), Field.Store.YES));//将文档对象放入到文档集合中docList.add(document);}long start = System.currentTimeMillis();//3. 创建分词器, StandardAnalyzer标准分词器, 对英文分词效果好, 对中文是单字分词, 也就是一个字就认为是一个词.Analyzer analyzer = new StandardAnalyzer();//4. 创建Directory目录对象, 目录对象表示索引库的位置Directory  dir = FSDirectory.open(Paths.get("C:\\software-c\\lucene-dir"));//5. 创建IndexWriterConfig对象, 这个对象中指定切分词使用的分词器/*** 没有优化 小100万条数据, 创建索引需要7725ms*/IndexWriterConfig config = new IndexWriterConfig(analyzer);//设置在内存中多少个文档向磁盘中批量写入一次数据//如果设置的数字过大, 会过多消耗内存, 但是会提升写入磁盘的速度// config.setMaxBufferedDocs(500000);//6. 创建IndexWriter输出流对象, 指定输出的位置和使用的config初始化对象IndexWriter indexWriter = new IndexWriter(dir, config);//设置多少给文档合并成一个段文件,数值越大索引速度越快, 搜索速度越慢;  值越小索引速度越慢, 搜索速度越快indexWriter.forceMerge(1000000);//7. 写入文档到索引库for (Document doc : docList) {indexWriter.addDocument(doc);}//8. 释放资源indexWriter.close();long end = System.currentTimeMillis();System.out.println("=====消耗的时间为:==========" + (end - start) + "ms");
}

9.2 选择合适的分词器

不同的分词器分词效果不同, 所用时间也不同

虽然StandardAnalyzer切分词速度快过IKAnalyzer, 但是由于StandardAnalyzer对中文支持不好, 所以为了追求好的分词效果, 为了追求查询时的准确率, 也只能用IKAnalyzer分词器, IKAnalyzer支持停用词典和扩展词典, 可以通过调整两个词典中的内容, 来提升查询匹配的精度

9.3 选择合适的位置存放索引库

9.4 搜索api的选择

  1. 尽量使用TermQuery代替QueryParser
  2. 尽量避免大范围的日期查询

10. Lucene相关度排序

10.1 什么是相关度排序

Lucene对查询关键字和索引文档的相关度进行打分,得分高的就排在前边。

10.1.1. 如何打分

Lucene是在用户进行检索时实时根据搜索的关键字计算出来的,分两步:

  1. 计算出词(Term)的权重
  2. 根据词的权重值,计算文档相关度得分。
10.1.2. 什么是词的权重

明确索引的最小单位是一个Term(索引词典中的一个词),搜索也是要从Term中搜索,再根据Term找到文档,Term对文档的重要性称为权重,影响Term权重有两个因素:

  • Term Frequency (tf): 指此Term在此文档中出现了多少次。tf 越大说明越重要。 词(Term)在文档中出现的次数越多,说明此词(Term)对该文档越重要,如“Lucene”这个词,在文档中出现的次数很多,说明该文档主要就是讲Lucene技术的。
  • Document Frequency (df): 指有多少文档包含次Term。df 越大说明越不重要。 比如,在一篇英语文档中,this出现的次数更多,就说明越重要吗?不是的,有越多的文档包含此词(Term), 说明此词(Term)太普通,不足以区分这些文档,因而重要性越低。
10.1.3. 怎样影响相关度排序

boost是一个加权值(默认加权值为1.0f),它可以影响权重的计算。

  • 在索引时对某个文档中的field设置加权值高,在搜索时匹配到这个文档就可能排在前边。
  • 在搜索时对某个域进行加权,在进行组合域查询时,匹配到加权值高的域最后计算的相关度得分就高。

设置boost是给域(field)或者Document设置的。

10.2 人为影响相关度排序

查询的时候, 通过设置查询域的权重, 可以人为影响查询结果.

11. Lucene使用注意事项(高级)

  • 关键词区分大小写 OR AND TO等关键词是区分大小写的,lucene只认大写的,小写的当做普通单词。
  • 读写互斥性 同一时刻只能有一个对索引的写操作,在写的同时可以进行搜索文件锁 在写索引的过程中强行退出将在tmp目录留下一个lock文件,使以后的写操作无法进行,可以将其手工删除
  • 时间格式 lucene只支持一种时间格式yyMMddHHmmss,所以你传一个yy-MM-dd HH:mm:ss的时间给lucene它是不会当作时间来处理的
  • 设置boost 有些时候在搜索时某个字段的权重需要大一些,例如你可能认为标题中出现关键词的文章比正文中出现关键词的文章更有价值,你可以把标题的boost设置的更大,那么搜索结果会优先显示标题中出现关键词的文章.

全文检索(一) -- Lucene相关推荐

  1. 全文检索框架Lucene——原理

    一.总论 根据 http://lucene.apache.org/java/docs/index.html 定义: Lucene 是一个高效的,基于Java 的全文检索库. 所以在了解Lucene之前 ...

  2. 全文检索工具Lucene入门教程

    目录 1.什么是Lucene 1.1什么是全文检索 1.2 全文检索的应用场景 1.3. 如何实现全文检索 2.Lucene实现全文检索的流程 2.1. 创建索引和搜索流程图 2.2. 创建索引 2. ...

  3. 全文检索工具包Lucene

    什么是全文检索 数据的分类 结构化数据:指的是格式固定.长度固定.数据类型固定的数据,例如数据库中的数据. 非结构化数据:指的是格式不固定.长度不固定.数据类型不固定的数据,例如 word 文档.pd ...

  4. 【手把手教你全文检索】Lucene索引的【增、删、改、查】

    2019独角兽企业重金招聘Python工程师标准>>> 前言 搞检索的,应该多少都会了解Lucene一些,它开源而且简单上手,官方API足够编写些小DEMO.并且根据倒排索引,实现快 ...

  5. JavaEE进阶——全文检索之Lucene框架

    I. 引言 全文检索 全文检索首先对要搜索的文档进行分词,然后形成索引,通过查询索引来查询文档.先创建索引,然后根据索引来进行搜索.比如查字典,字典的偏旁部首就类似于索引,字典的具体内容则类似于文档内 ...

  6. 全文检索技术Lucene入门和学习、与数据库数据结合的demo实现

    首先,要非常感谢 博客园的 "觉先"先生的分享, 他的博客带我进入了Lucene的大门 :http://www.cnblogs.com/forfuture1978/category ...

  7. 全文检索工具 Lucene 入门

    最近在了解 Halo 博客后端源码,而全文检索是 Halo 做的比较差的一块内容,仅通过数据库的模糊查询来实现文章检索.对于搜索引擎之前了解的也不多,所以开始入门 Lucene 检索引擎,如果可以的话 ...

  8. 全文检索技术 Lucene

    文章目录 全文检索简介(什么是全文检索) 数据分类 结构化数据搜索 非结构化数据查询方法 全文检索的应用场景 Lucene简介 创建索引库 理论部分(很重要) 创建原始文档 创建文档对象 分析文档 创 ...

  9. (转)全文检索技术学习(一)——Lucene的介绍

    http://blog.csdn.net/yerenyuan_pku/article/details/72582979 本文我将为大家讲解全文检索技术--Lucene,现在这个技术用到的比较多,我觉得 ...

最新文章

  1. linux命令后缀-d和 都表示后台启动,Linux复习材料_关宇亮整理版.doc
  2. 嵌入式Linux教程一:安装Ubuntu并进行基本配置、交叉编译环境和Minicom
  3. 密码技术--椭圆曲线算法EDCSA数字签名及Go语言应用
  4. 多媒体调度系统如何实现对水库大坝的防洪调度
  5. [网络安全自学篇] 十五.Python攻防之多线程、C段扫描和数据库编程(二)
  6. upload-labs_pass 9_::$DATA_绕过
  7. paip.字符串操作uapi java php python总结..
  8. centos配置ipv6地址
  9. linux命令行效率,聊聊那些可以提高工作效率的Linux命令
  10. websocket 带头部信息请求 header_BeetleX之Websocket协议分析详解
  11. 使用Docker部署Gitlab
  12. Symantec清除工具 CleanWipe_14.3.558.1000
  13. esp8266教程:文件系统之spiffs
  14. 基于Qt开发的游戏手柄小程序例子
  15. 在IE缓存文件夹找到我想要的视频
  16. c语言让数码管显示时间,请问51单片机肿么编写程序让8个数码管显示时间(时分秒)...
  17. 计算机的dp接口在哪,主机dp接口在哪
  18. pest分析和swot分析的区别
  19. THREEJS辉光与景深特效
  20. 线性稳压器与开关稳压器的对比分析

热门文章

  1. springboot 问题记录
  2. tp6中的助手函数是什么意思?
  3. crmeb知识付费系统直播列表管理
  4. zt:寻找Nina Reiser的悬赏已增至2万5千美元
  5. Linux系统查看有几块硬盘
  6. 解析一个xml文件并将数据存入数据库
  7. leetcode | 971. Flip Binary Tree To Match Preorder Traversal(DFS/preorder)
  8. 《计算机组成原理》——计算题
  9. 关于printf()函数。
  10. 芳香四十二点一九五里...