全文检索Lucene

  • lucene入门

​ 什么是lucene

​ Lucene的作用

​ 使用场景

​ 优点和缺点

  • lucene应用

​ 索引流程

​ 搜索流程

​ field域的使用

​ 索引库维护

​ 分词器

​ 高级搜索实战案例

  • Lucene高级

​ Lucene底层存储结构

​ 词典排序算法

​ Lucene优化

​ Lucene使用的一些注意事项

1. 搜索技术理论基础

1.1. 为什么要学习Lucene

原来的方式实现搜索功能,我们的搜索流程如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vAiWQwDP-1624282922149)(image/001.png)]

上图就是原始搜索引擎技术,如果用户比较少而且数据库的数据量比较小,那么这种方式实现搜索功能在企业中是比较常见的。

但是数据量过多时,数据库的压力就会变得很大,查询速度会变得非常慢。我们需要使用更好的解决方案来分担数据库的压力。

现在的方案(使用Lucene),如下图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YPv274Et-1624282922157)(image/002.png)]

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

1.2. 数据查询方法

1.2.1. 顺序扫描法

算法描述:

​ 所谓顺序扫描,例如要找内容包含一个字符串的文件,就是一个文档一个文档的看,对于每一个文档,从头看到尾,如果此文档包含此字符串,则此文档为我们要找的文件,接着看下一个文件,直到扫描完所有的文件。

**优点: **

​ 查询准确率高

缺点:

​ 查询速度会随着查询数据量的增大, 越来越慢

使用场景:

  • ​ 数据库中的like关键字模糊查询
  • ​ 文本编辑器的Ctrl + F 查询功能

1.2.2. 倒排索引

先举一个栗子:

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

Lucene会对文档建立倒排索引

1、 提取资源中关键信息, 建立索引 (目录)

2、 搜索时,根据关键字(目录),找到资源的位置

算法描述:

​ 查询前会先将查询的内容提取出来组成文档(正文), 对文档进行切分词组成索引(目录), 索引和文档有关联关系, 查询的时候先查询索引, 通过索引找文档的这个过程叫做全文检索。

切分词 : 就是将一句一句话切分成一个一个的词, 去掉停用词(的, 地, 得, a, an, the等)。去掉空格, 去掉标点符号, 大写字母转成小写字母, 去掉重复的词。

为什么倒排索引比顺序扫描快?

**理解 : ** 因为索引可以去掉重复的词, 汉语常用的字和词大概等于, 字典加词典, 常用的英文在牛津词典也有收录.如果用计算机的速度查询, 字典+词典+牛津词典这些内容是非常快的. 但是用这些字典, 词典组成的文章却是千千万万不计其数. 索引的大小最多也就是字典+词典. 所以通过查询索引, 再通过索引和文档的关联关系找到文档速度比较快. 顺序扫描法则是直接去逐个查询那些不计其数的文章就算是计算的速度也会很慢.

优点:

​ 查询准确率高

​ 查询速度快, 并且不会因为查询内容量的增加, 而使查询速度逐渐变慢

缺点:

​ 索引文件会占用额外的磁盘空间, 也就是占用磁盘量会增大。

使用场景:

​ 海量数据查询

1.3. 全文检索技术应用场景

应用场景 :

​ 1、 站内搜索 (baidu贴吧、论坛、 京东、 taobao)

​ 2、 垂直领域的搜索 (818工作网)

​ 3、 专业搜索引擎公司 (google、baidu)

2. Lucene介绍

2.1. 什么是全文检索

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

2.2. 什么是Lucene

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hIOiyJwe-1624282922163)(image/003.png)]

他是Lucene、Nutch 、Hadoop等项目的发起人Doug Cutting

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

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

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

  • Lucene是一套用于全文检索和搜寻的开源程式库,由Apache软件基金会支持和提供

  • Lucene提供了一个简单却强大的应用程式接口,能够做全文索引和搜寻, 在Java开发环境里Lucene是一个成熟的免费开放源代码工具

  • Lucene并不是现成的搜索引擎产品,但可以用来制作搜索引擎产品

2.3. Lucene官网

官网: http://lucene.apache.org/

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hk4DDIyF-1624282922165)(image/007.png)]

3. Lucene全文检索的流程

3.1. 索引和搜索流程图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iIT4UMvg-1624282922167)(image/clip_image008.jpg)]

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

确定原始内容即要搜索的内容

  • ​ 获得文档
  • ​ 创建文档
  • ​ 分析文档
  • ​ 索引文档

2、红色表示搜索过程,从索引库中搜索内容,搜索过程包括:

用户通过搜索界面

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

3.2. 索引流程

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

3.2.1. 原始内容

原始内容是指要索引和搜索的内容。

原始内容包括互联网上的网页、数据库中的数据、磁盘上的文件等。

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

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

采集数据分类:

1、对于互联网上网页,可以使用工具将网页抓取到本地生成html文件。

2、数据库中的数据,可以直接连接数据库读取表中的数据。

3、文件系统中的某个文件,可以通过I/O操作读取文件的内容。

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

3.2.3. 创建文档

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2EBdBzfe-1624282922169)(image/008.png)]

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

3.2.4. 分析文档

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

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

原文档内容:

vivo X23 8GB+128GB 幻夜蓝 全网通4G手机

华为 HUAWEI 麦芒7 6G+64G 亮黑色 全网通4G手机

分析后得到的词:

vivo, x23, 8GB, 128GB, 幻夜, 幻夜蓝, 全网, 全网通, 网通, 4G, 手机, 华为, HUAWEI, 麦芒7。。。。

3.2.5. 索引文档

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

创建索引是对语汇单元索引,通过词语找文档,这种索引的结构叫倒排索引结构。

倒排索引结构是根据内容(词汇)找文档,如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gPqeMfpZ-1624282922170)(image/009.png)]

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

3.2.6 Lucene底层存储结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OfFMeu1S-1624282922171)(image/004.png)]

3.3. 搜索流程

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

3.3.1. 用户

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

3.3.2. 用户搜索界面

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0wrSt1Db-1624282922172)(image/clip_image013.jpg)]

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

3.3.3. 创建查询

用户输入查询关键字执行搜索之前需要先构建一个查询对象,查询对象中可以指定查询要查询关键字、要搜索的Field文档域等,查询对象会生成具体的查询语法,比如:

name:手机 : 表示要搜索name这个Field域中,内容为“手机”的文档。

**name:华为 AND 手机 : ** 表示要搜索即包括关键字“华为” 并且也包括“手机”的文档。

3.3.4. 执行搜索

搜索索引过程:

1.根据查询语法在倒排索引词典表中分别找出对应搜索词的索引,从而找到索引所链接的文档链表。

例如搜索语法为 **“name:华为 AND 手机 ” **表示搜索出的文档中既要包括"华为"也要包括"手机"。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eRyqGupK-1624282922173)(image/010.png)]

2、由于是AND,所以要对包含 华为手机 词语的链表进行交集,得到文档链表应该包括每一个搜索词语

3、获取文档中的Field域数据。

3.3.5. 渲染结果

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6PDhvnQd-1624282922174)(image/clip_image016.jpg)]

4. Lucene入门

4.1. Lucene准备

Lucene可以在官网上下载。课程已经准备好了Lucene的文件,我们使用的是7.7.2版本,文件位置如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qZfH5dbW-1624282922175)(image/005.png)]

解压后的效果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NOXtTEVi-1624282922177)(image/006.png)]

使用这三个文件的jar包,就可以实现lucene功能

4.2. 开发环境

JDK: 1.8 (Lucene7以上,必须使用JDK1.8及以上版本)

数据库: MySQL

数据库脚本位置如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k7cGturK-1624282922178)(image/011.png)]

导入到MySQL效果如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y0rIPUf3-1624282922180)(image/012.png)]

4.3. 创建Java工程

创建maven工程不依赖骨架, 测试即可,效果如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jHN9HiKa-1624282922181)(image/013.png)]

4.4. 索引流程

4.4.1. 数据采集

在电商网站中,全文检索的数据源在数据库中,需要通过jdbc访问数据库中 sku 表的内容。

4.4.1.1. 创建pojo

public class Sku {//商品主键idprivate String id;//商品名称private String name;//价格private Integer price;//库存数量private Integer num;//图片private String image;//分类名称private String categoryName;//品牌名称private String brandName;//规格private String spec;//销量private Integer saleNum;get/set。。。}

4.4.1.2. 创建DAO接口

public interface SkuDao {/*** 查询所有的Sku数据* @return**/public List<Sku> querySkuList();
}

4.4.1.3. 创建DAO接口实现类

使用jdbc实现

public class SkuDaoImpl implements SkuDao {public List<Sku> querySkuList() {// 数据库链接Connection connection = null;// 预编译statementPreparedStatement preparedStatement = null;// 结果集ResultSet resultSet = null;// 商品列表List<Sku> list = new ArrayList<Sku>();try {// 加载数据库驱动Class.forName("com.mysql.jdbc.Driver");// 连接数据库connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/lucene", "root", "admin");// SQL语句String sql = "SELECT * FROM tb_sku";// 创建preparedStatementpreparedStatement = connection.prepareStatement(sql);// 获取结果集resultSet = preparedStatement.executeQuery();// 结果集解析while (resultSet.next()) {Sku sku = new Sku();sku.setId(resultSet.getString("id"));sku.setName(resultSet.getString("name"));sku.setSpec(resultSet.getString("spec"));sku.setBrandName(resultSet.getString("brand_name"));sku.setCategoryName(resultSet.getString("category_name"));sku.setImage(resultSet.getString("image"));sku.setNum(resultSet.getInt("num"));sku.setPrice(resultSet.getInt("price"));sku.setSaleNum(resultSet.getInt("sale_num"));list.add(sku);}} catch (Exception e) {e.printStackTrace();}return list;}
}

4.4.2. 实现索引流程

  1. 采集数据
  2. 创建Document文档对象
  3. 创建分析器(分词器)
  4. 创建IndexWriterConfig配置信息类
  5. 创建Directory对象,声明索引库存储位置
  6. 创建IndexWriter写入对象
  7. 把Document写入到索引库中
  8. 释放资源
public class TestManager {@Testpublic void createIndexTest() throws Exception {// 1. 采集数据SkuDao skuDao = new SkuDaoImpl();List<Sku> skuList = skuDao.querySkuList();// 2. 创建Document文档对象List<Document> documents = new ArrayList<Document>();for (Sku sku : skuList) {Document document = new Document();// Document文档中添加Field域// 商品Id// Store.YES:表示存储到文档域中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", sku.getPrice().toString(), Field.Store.YES));// 品牌名称document.add(new TextField("brandName", sku.getBrandName(), Field.Store.YES));// 分类名称document.add(new TextField("categoryName", sku.getCategoryName(), Field.Store.YES));// 图片地址document.add(new TextField("image", sku.getImage(), Field.Store.YES));// 把Document放到list中documents.add(document);}// 3. 创建Analyzer分词器,分析文档,对文档进行分词Analyzer analyzer = new StandardAnalyzer();// 4. 创建Directory对象,声明索引库的位置Directory directory = FSDirectory.open(Paths.get("E:\\dir"));// 5. 创建IndexWriteConfig对象,写入索引需要的配置IndexWriterConfig config = new IndexWriterConfig(analyzer);// 6.创建IndexWriter写入对象IndexWriter indexWriter = new IndexWriter(directory, config);// 7.写入到索引库,通过IndexWriter添加文档对象documentfor (Document doc : documents) {indexWriter.addDocument(doc);}// 8.释放资源indexWriter.close();}}

执行效果:

在文件夹中出现了以下文件,表示创建索引成功

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kcCQKPZ2-1624282922182)(image/014.png)]

4.5. 使用Luke查看索引

Luke作为Lucene工具包中的一个工具(http://www.getopt.org/luke/),可以通过界面来进行索引文件的查询、修改

luke所在位置如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e4Zafxfr-1624282922183)(image/015.png)]

将luke-swing-8.0.0里面的内容, 放到一个硬盘根目录的文件夹下, 不能有空格和中文名称.

运行luke.bat

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-447ICgMS-1624282922184)(image\016.png)]

打开后,使用如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sEeAO7QI-1624282922185)(image/017.png)]

下图是索引域的展示效果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PFy9Xepo-1624282922185)(image/018.png)]

下图是文档域展示效果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6Qq3rVnP-1624282922186)(image/019.png)]

4.6. 搜索流程

4.6.1. 输入查询语句

Lucene可以通过query对象输入查询语句。同数据库的sql一样,lucene也有固定的查询语法:

最基本的有比如:AND, OR, NOT 等(必须大写)

举个栗子:

用户想找一个 name 域中包括 关键字的文档。

它对应的查询语句:name:手 OR name:机

如下图是使用luke搜索的例子:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b9Ys915K-1624282922188)(image/020.png)]

4.6.1.1. 搜索分词

和索引过程的分词一样,这里要对用户输入的关键字进行分词,一般情况索引和搜索使用的分词器一致。

​ 比如:输入搜索关键字“java学习”,分词后为java和学习两个词,与java和学习有关的内容都搜索出来了,如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NdLElh7K-1624282922189)(image/clip_image042.jpg)]

4.6.2. 代码实现

  1. 创建Query搜索对象
  2. 创建Directory流对象,声明索引库位置
  3. 创建索引读取对象IndexReader
  4. 创建索引搜索对象IndexSearcher
  5. 使用索引搜索对象,执行搜索,返回结果集TopDocs
  6. 解析结果集
  7. 释放资源

IndexSearcher搜索方法如下:

方法 说明
indexSearcher.search(query, n) 根据Query搜索,返回评分最高的n条记录
indexSearcher.search(query, filter, n) 根据Query搜索,添加过滤策略,返回评分最高的n条记录
indexSearcher.search(query, n, sort) 根据Query搜索,添加排序策略,返回评分最高的n条记录
indexSearcher.search(booleanQuery, filter, n, sort) 根据Query搜索,添加过滤策略,添加排序策略,返回评分最高的n条记录

代码实现

public class TestSearch {@Testpublic void testIndexSearch() throws Exception {// 1. 创建Query搜索对象// 创建分词器Analyzer analyzer = new StandardAnalyzer();// 创建搜索解析器,第一个参数:默认Field域,第二个参数:分词器QueryParser queryParser = new QueryParser("brandName", analyzer);// 创建搜索对象Query query = queryParser.parse("name:手机 AND 华为");// 2. 创建Directory流对象,声明索引库位置Directory directory = FSDirectory.open(Paths.get("E:\\dir"));// 3. 创建索引读取对象IndexReaderIndexReader reader = DirectoryReader.open(directory);// 4. 创建索引搜索对象IndexSearcher searcher = new IndexSearcher(reader);// 5. 使用索引搜索对象,执行搜索,返回结果集TopDocs// 第一个参数:搜索对象,第二个参数:返回的数据条数,指定查询结果最顶部的n条数据返回TopDocs topDocs = searcher.search(query, 10);System.out.println("查询到的数据总条数是:" + topDocs.totalHits);// 获取查询结果集ScoreDoc[] docs = topDocs.scoreDocs;// 6. 解析结果集for (ScoreDoc scoreDoc : docs) {// 获取文档int docID = scoreDoc.doc;Document doc = searcher.doc(docID);System.out.println("=============================");System.out.println("docID:" + docID);System.out.println("id:" + doc.get("id"));System.out.println("name:" + doc.get("name"));System.out.println("price:" + doc.get("price"));System.out.println("brandName:" + doc.get("brandName"));System.out.println("image:" + doc.get("image"));}// 7. 释放资源reader.close();}
}

5. Field域类型

5.1. Field属性

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

  • 是否分词(tokenized)

是:作分词处理,即将Field值进行分词,分词的目的是为了索引。

比如:商品名称、商品描述等,这些内容用户要输入关键字搜索,由于搜索的内容格式大、内容多需要分词后将语汇单元建立索引

否:不作分词处理

比如:商品id、订单号、身份证号等

  • 是否索引(indexed)

是:进行索引。将Field分词后的词或整个Field值进行索引,存储到索引域,索引的目的是为了搜索。

比如:商品名称、商品描述分析后进行索引,订单号、身份证号不用分词但也要索引,这些将来都要作为查询条件。

否:不索引。

比如:图片路径、文件路径等,不用作为查询条件的不用索引。

  • 是否存储(stored)

是:将Field值存储在文档域中,存储在文档域中的Field才可以从Document中获取。

比如:商品名称、订单号,凡是将来要从Document中获取的Field都要存储。

否:不存储Field值

比如:商品描述,内容较大不用存储。如果要向用户展示商品描述可以从系统的关系数据库中获取。

5.2. Field常用类型

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

Field类 数据类型 Analyzed 是否分词 Indexed 是否索引 Stored 是否存储 说明
StringField(FieldName, FieldValue,Store.YES)) 字符串 N Y Y或N 这个Field用来构建一个字符串Field,但是不会进行分词,会将整个串存储在索引中,比如(订单号,身份证号等) 是否存储在文档中用Store.YES或Store.NO决定
FloatPoint(FieldName, FieldValue) Float型 Y Y N 这个Field用来构建一个Float数字型Field,进行分词和索引,不存储, 比如(价格) 存储在文档中
DoublePoint(FieldName, FieldValue) Double型 Y Y N 这个Field用来构建一个Double数字型Field,进行分词和索引,不存储
LongPoint(FieldName, FieldValue) Long型 Y Y N 这个Field用来构建一个Long数字型Field,进行分词和索引,不存储
IntPoint(FieldName, FieldValue) Integer型 Y Y N 这个Field用来构建一个Integer数字型Field,进行分词和索引,不存储
StoredField(FieldName, FieldValue) 重载方法,支持多种类型 N N Y 这个Field用来构建不同类型Field 不分析,不索引,但要Field存储在文档中
TextField(FieldName, FieldValue, Store.NO) 或 TextField(FieldName, reader) 字符串 或 流 Y Y Y或N 如果是一个Reader, lucene猜测内容比较多,会采用Unstored的策略.
NumericDocValuesField(FieldName, FieldValue) 数值 _ _ _ 配合其他域排序使用

5.3. Field修改

5.3.1. 修改分析

图书id:

是否分词:不用分词,因为不会根据商品id来搜索商品

是否索引:不索引,因为不需要根据图书ID进行搜索

是否存储:要存储,因为查询结果页面需要使用id这个值。

图书名称:

是否分词:要分词,因为要根据图书名称的关键词搜索。

是否索引:要索引。

是否存储:要存储。

图书价格:

是否分词:要分词,lucene对数字型的值只要有搜索需求的都要分词和索引,因 为lucene对数字型的内容要特殊分词处理,需要分词和索引。

是否索引:要索引

是否存储:要存储

图书图片地址:

是否分词:不分词

是否索引:不索引

是否存储:要存储

图书描述:

是否分词:要分词

是否索引:要索引

是否存储:因为图书描述内容量大,不在查询结果页面直接显示,不存储。

不存储是不在lucene的索引域中记录,节省lucene的索引文件空间。

如果要在详情页面显示描述,解决方案:

从lucene中取出图书的id,根据图书的id查询关系数据库(MySQL)中book表得到描述信息。

5.3.2. 代码修改

对之前编写的testCreateIndex()方法进行修改。

代码片段

Document document = new Document();// Document文档中添加Field域
// 商品Id, 不分词,索引,存储
document.add(new StringField("id", sku.getId(), Field.Store.YES));
// 商品名称, 分词, 索引, 存储
document.add(new TextField("name", sku.getName(), Field.Store.YES));// 商品价格, 分词,索引,不存储, 不排序
document.add(new FloatPoint("price", sku.getPrice()));
//添加价格存储支持
document.add(new StoredField("price", sku.getPrice()));
//添加价格排序支持
//document.add(new NumericDocValuesField("price",sku.getPrice()));// 品牌名称, 不分词, 索引, 存储
document.add(new StringField("brandName", sku.getBrandName(), Field.Store.YES));
// 分类名称, 不分词, 索引, 存储
document.add(new StringField("categoryName", sku.getCategoryName(), Field.Store.YES));
// 图片地址, 不分词,不索引,存储
document.add(new StoredField("image", sku.getImage()));// 把Document放到list中
documents.add(document);

6. 索引维护

6.1. 需求

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

6.2. 添加索引

调用 indexWriter.addDocument(doc)添加索引。

参考入门程序的创建索引。

6.3. 修改索引

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

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

代码

@Test
public void testIndexUpdate() throws Exception {// 创建分词器Analyzer analyzer = new StandardAnalyzer();// 创建Directory流对象Directory directory = FSDirectory.open(Paths.get("E:\\dir"));// 创建IndexWriteConfig对象,写入索引需要的配置IndexWriterConfig config = new IndexWriterConfig(analyzer);// 创建写入对象IndexWriter indexWriter = new IndexWriter(directory, config);// 创建DocumentDocument document = new Document();document.add(new TextField("id", "1202790956", Field.Store.YES));document.add(new TextField("name", "lucene测试test 002", Field.Store.YES));// 执行更新,会把所有符合条件的Document删除,再新增。indexWriter.updateDocument(new Term("id", "1202790956"), document);// 释放资源indexWriter.close();
}

6.4. 删除索引

6.4.1. 删除指定索引

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

@Test
public void testIndexDelete() throws Exception {// 创建分词器Analyzer analyzer = new StandardAnalyzer();// 创建Directory流对象Directory directory = FSDirectory.open(Paths.get("E:\\dir"));// 创建IndexWriteConfig对象,写入索引需要的配置IndexWriterConfig config = new IndexWriterConfig(analyzer);// 创建写入对象IndexWriter indexWriter = new IndexWriter(directory, config);// 根据Term删除索引库,name:javaindexWriter.deleteDocuments(new Term("id", "998188"));// 释放资源indexWriter.close();
}

效果如下图:索引域没有变化

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fGtNXrNo-1624282922190)(image/021.png)]

文档域数据被删除掉

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HepgQmw4-1624282922191)(image/022.png)]

6.4.2. 删除全部索引(慎用)

将索引目录的索引信息全部删除,直接彻底删除,无法恢复。

建议参照关系数据库基于主键删除方式,所以在创建索引时需要创建一个主键Field,删除时根据此主键Field删除。

索引删除后将放在Lucene的回收站中,Lucene3.X版本可以恢复删除的文档,3.X之后无法恢复。

代码:

@Test
public void testIndexDeleteAll() throws Exception {// 创建分词器Analyzer analyzer = new StandardAnalyzer();// 创建Directory流对象Directory directory = FSDirectory.open(Paths.get("E:\\dir"));// 创建IndexWriteConfig对象,写入索引需要的配置IndexWriterConfig config = new IndexWriterConfig(analyzer);// 创建写入对象IndexWriter indexWriter = new IndexWriter(directory, config);// 全部删除indexWriter.deleteAll();// 释放资源indexWriter.close();
}

索引域数据清空

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-99AoJQL3-1624282922192)(image/023.png)]

文档域数据也清空

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b0odNYFO-1624282922193)(image/024.png)]

7. 分词器

7.1. 分词理解

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

  • 分词:采集到的数据会存储到document对象的Field域中,分词就是将Document中Field的value值切分成一个一个的词。

  • 过滤:包括去除标点符号过滤、去除停用词过滤(的、是、a、an、the等)、大写转小写、词的形还原(复数形式转成单数形参、过去式转成现在式。。。)等。

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

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

7.2. Analyzer使用时机

7.2.1. 索引时使用Analyzer

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2Bi2SHly-1624282922193)(image/clip_image058.jpg)]

对于一些Field可以不用分析:

1、不作为查询条件的内容,比如文件路径

2、不是匹配内容中的词而匹配Field的整体内容,比如订单号、身份证号等。

7.2.2. 搜索时使用Analyzer

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

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

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

7.3. Lucene原生分词器

以下是Lucene中自带的分词器

7.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提供了很多的分词器过滤器,比如大小写转换、去除停用词等。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NmlzWX6F-1624282922198)(image/clip_image054.jpg)]

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

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

原文档内容:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-evyGYf99-1624282922199)(image/clip_image055.gif)]

分析后得到的多个语汇单元:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kriUxP0j-1624282922200)(image/clip_image056.gif)]

7.3.2. WhitespaceAnalyzer

特点 :

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

测试代码:

@Test
public void TestWhitespaceAnalyzer() throws Exception{// 1. 创建分词器,分析文档,对文档进行分词Analyzer analyzer = new WhitespaceAnalyzer();// 2. 创建Directory对象,声明索引库的位置Directory directory = FSDirectory.open(Paths.get("E:\\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 幻夜蓝", Field.Store.YES));indexWriter.addDocument(doc);// 6.释放资源indexWriter.close();
}

结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s9oi5Lwm-1624282922200)(image/025.png)]

7.3.3. SimpleAnalyzer

特点 :

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

测试:

@Test
public void TestSimpleAnalyzer() throws Exception{// 1. 创建分词器,分析文档,对文档进行分词Analyzer analyzer = new SimpleAnalyzer();// 2. 创建Directory对象,声明索引库的位置Directory directory = FSDirectory.open(Paths.get("E:\\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; 幻夜蓝", Field.Store.YES));indexWriter.addDocument(doc);// 6.释放资源indexWriter.close();
}

结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qGBzgdXd-1624282922201)(image/026.png)]

7.3.4. CJKAnalyzer

特点 :

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

代码:

@Test
public void TestCJKAnalyzer() throws Exception{// 1. 创建分词器,分析文档,对文档进行分词Analyzer analyzer = new CJKAnalyzer();// 2. 创建Directory对象,声明索引库的位置Directory directory = FSDirectory.open(Paths.get("E:\\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; 幻夜蓝", Field.Store.YES));indexWriter.addDocument(doc);// 6.释放资源indexWriter.close();
}

结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J0g61IVR-1624282922202)(image/027.png)]

7.3.5. SmartChineseAnalyzer

特点 :

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

代码:

@Test
public void TestSmartChineseAnalyzer() throws Exception{// 1. 创建分词器,分析文档,对文档进行分词Analyzer analyzer = new SmartChineseAnalyzer();// 2. 创建Directory对象,声明索引库的位置Directory directory = FSDirectory.open(Paths.get("E:\\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; 幻夜蓝", Field.Store.YES));indexWriter.addDocument(doc);// 6.释放资源indexWriter.close();
}

结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kzFAw7tV-1624282922204)(image/028.png)]

7.4. 第三方中文分词器

7.4.1. 什么是中文分词器

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

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

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

7.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算法。

7.4.3. 使用中文分词器IKAnalyzer

IKAnalyzer继承Lucene的Analyzer抽象类,使用IKAnalyzer和Lucene自带的分析器方法一样,将Analyzer测试代码改为IKAnalyzer测试中文分词效果。

如果使用中文分词器ik-analyzer,就需要在索引和搜索程序中使用一致的分词器:IK-analyzer。

  1. 添加依赖, pom.xml中加入依赖
<dependency><groupId>org.wltea.ik-analyzer</groupId><artifactId>ik-analyzer</artifactId><version>8.1.0</version>
</dependency>
  1. 加入配置文件:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D80rmYPq-1624282922209)(image/029.png)]

  2. 测试代码

@Test
public void TestIKAnalyzer() throws Exception{// 1. 创建分词器,分析文档,对文档进行分词Analyzer analyzer = new IKAnalyzer();// 2. 创建Directory对象,声明索引库的位置Directory directory = FSDirectory.open(Paths.get("E:\\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. 测试结果

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oZiwFM5B-1624282922210)(image/030.png)]

7.4.4. 扩展中文词库

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

从ikanalyzer包中拷贝配置文件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Sa26i31H-1624282922212)(image/031.png)]

拷贝到资源文件夹中

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PTcV06fe-1624282922213)(image/029.png)]

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作用 : **

扩展词典中的词例如: 传智播客, 黑马程序员, 贵州茅台等专有名词, 在汉语中一些公司名称, 行业名称, 分类, 品牌等不是汉语中的词汇, 是专有名词. 这些分词器默认不识别, 所以需要放入扩展词典中, 效果是被强制分成一个词.

8. Lucene高级搜索

8.1.文本搜索

QueryParser支持默认搜索域, 第一个参数为默认搜索域.

如果在执行parse方法的时候, 查询语法中包含域名则从指定的这个域名中搜索, 如果只有查询的关键字, 则从默认搜索域中搜索结果.

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

测试代码:

 @Test
public void testIndexSearch() throws Exception {// 1. 创建Query搜索对象// 创建分词器Analyzer analyzer = new IKAnalyzer();// 创建搜索解析器,第一个参数:默认Field域,第二个参数:分词器QueryParser queryParser = new QueryParser("brandName", analyzer);// 创建搜索对象Query query = queryParser.parse("name:华为手机");// 2. 创建Directory流对象,声明索引库位置Directory directory = FSDirectory.open(Paths.get("E:\\dir"));// 3. 创建索引读取对象IndexReaderIndexReader reader = DirectoryReader.open(directory);// 4. 创建索引搜索对象IndexSearcher searcher = new IndexSearcher(reader);// 5. 使用索引搜索对象,执行搜索,返回结果集TopDocs// 第一个参数:搜索对象,第二个参数:返回的数据条数,指定查询结果最顶部的n条数据返回TopDocs topDocs = searcher.search(query, 50);System.out.println("查询到的数据总条数是:" + topDocs.totalHits);// 获取查询结果集ScoreDoc[] docs = topDocs.scoreDocs;// 6. 解析结果集for (ScoreDoc scoreDoc : docs) {// 获取文档int docID = scoreDoc.doc;Document doc = searcher.doc(docID);System.out.println("=============================");System.out.println("docID:" + docID);System.out.println("id:" + doc.get("id"));System.out.println("name:" + doc.get("name"));System.out.println("price:" + doc.get("price"));System.out.println("brandName:" + doc.get("brandName"));System.out.println("image:" + doc.get("image"));}// 7. 释放资源reader.close();
}

8.2.数值范围搜索

**需求描述 : **查询价格大于等于100, 小于等于1000的商品

测试代码:

@Test
public void testNumberSearch() throws Exception {// 1. 创建Query搜索对象Query query = FloatPoint.newRangeQuery("price", 100, 1000);// 2. 创建Directory流对象,声明索引库位置Directory directory = FSDirectory.open(Paths.get("E:\\dir"));// 3. 创建索引读取对象IndexReaderIndexReader reader = DirectoryReader.open(directory);// 4. 创建索引搜索对象IndexSearcher searcher = new IndexSearcher(reader);// 5. 使用索引搜索对象,执行搜索,返回结果集TopDocs// 第一个参数:搜索对象,第二个参数:返回的数据条数,指定查询结果最顶部的n条数据返回TopDocs topDocs = searcher.search(query, 10);System.out.println("查询到的数据总条数是:" + topDocs.totalHits);// 获取查询结果集ScoreDoc[] docs = topDocs.scoreDocs;// 6. 解析结果集for (ScoreDoc scoreDoc : docs) {// 获取文档int docID = scoreDoc.doc;Document doc = searcher.doc(docID);System.out.println("=============================");System.out.println("docID:" + docID);System.out.println("id:" + doc.get("id"));System.out.println("name:" + doc.get("name"));System.out.println("price:" + doc.get("price"));System.out.println("brandName:" + doc.get("brandName"));System.out.println("image:" + doc.get("image"));}// 7. 释放资源reader.close();
}

8.3.组合搜索

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

BooleanClause.Occur.MUST 必须 相当于and, 并且

BooleanClause.Occur.MUST_NOT 不必须 相当于not, 非

BooleanClause.Occur.SHOULD 应该 相当于or, 或者

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

@Test
public void testBooleanSearch() throws Exception {// 创建分词器Analyzer analyzer = new IKAnalyzer();// 创建数值范围搜索对象Query query1 = FloatPoint.newRangeQuery("price", 100, 1000);// 创建文本搜索对象QueryParser queryParser = new QueryParser("name", analyzer);// 创建搜索对象Query query2 = queryParser.parse("华为手机");//创建组合搜索对象BooleanQuery.Builder builder = new BooleanQuery.Builder();builder.add(new BooleanClause(query1, BooleanClause.Occur.MUST));builder.add(new BooleanClause(query2, BooleanClause.Occur.MUST_NOT));// 2. 创建Directory流对象,声明索引库位置Directory directory = FSDirectory.open(Paths.get("E:\\dir"));// 3. 创建索引读取对象IndexReaderIndexReader reader = DirectoryReader.open(directory);// 4. 创建索引搜索对象IndexSearcher searcher = new IndexSearcher(reader);// 5. 使用索引搜索对象,执行搜索,返回结果集TopDocs// 第一个参数:搜索对象,第二个参数:返回的数据条数,指定查询结果最顶部的n条数据返回TopDocs topDocs = searcher.search(builder.build(), 10);System.out.println("查询到的数据总条数是:" + topDocs.totalHits);// 获取查询结果集ScoreDoc[] docs = topDocs.scoreDocs;// 6. 解析结果集for (ScoreDoc scoreDoc : docs) {// 获取文档int docID = scoreDoc.doc;Document doc = searcher.doc(docID);System.out.println("=============================");System.out.println("docID:" + docID);System.out.println("id:" + doc.get("id"));System.out.println("name:" + doc.get("name"));System.out.println("price:" + doc.get("price"));System.out.println("brandName:" + doc.get("brandName"));System.out.println("image:" + doc.get("image"));}// 7. 释放资源reader.close();
}

9. 搜索案例

成品效果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rRHYzPUN-1624282922214)(image\032.png)]

9.1. 引入依赖

在项目的pom.xml中引入依赖:

<properties><maven.compiler.source>1.8</maven.compiler.source><maven.compiler.target>1.8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><skipTests>true</skipTests>
</properties><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.4.RELEASE</version>
</parent><dependencies><dependency><groupId>commons-io</groupId><artifactId>commons-io</artifactId><version>2.6</version></dependency><dependency><groupId>org.apache.lucene</groupId><artifactId>lucene-core</artifactId><version>7.7.2</version></dependency><dependency><groupId>org.apache.lucene</groupId><artifactId>lucene-analyzers-common</artifactId><version>7.7.2</version></dependency><dependency><groupId>org.apache.lucene</groupId><artifactId>lucene-queryparser</artifactId><version>7.7.2</version></dependency><!-- 测试 --><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version><scope>test</scope></dependency><!-- mysql数据库驱动 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.48</version></dependency><!-- IK中文分词器 --><dependency><groupId>org.wltea.ik-analyzer</groupId><artifactId>ik-analyzer</artifactId><version>8.1.0</version></dependency><!--web起步依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- 引入thymeleaf --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><!-- Json转换工具 --><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.51</version></dependency>
<dependencies>

9.2. 项目加入页面和资源

将Lucene课程资料\资源\页面和静态资源, 下的页面和静态资源拷贝到项目的resources目录下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hy31ig3o-1624282922216)(image\033.png)]

9.3. 创建包和启动类

创建目录, 并加入启动类:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K6EYBMpn-1624282922217)(image\034.png)]

启动类代码:

@SpringBootApplication
public class LuceneApplication {public static void main(String[] args) {SpringApplication.run(LuceneApplication.class, args);}
}

9.4. 配置文件

项目的resources目录下创建application.yml内容如下:

spring:thymeleaf:cache: false

9.5. 业务代码:

9.5.1. 封装pojo

pojo包下加入ResultModel实体类

public class ResultModel {// 商品列表private List<Sku> skuList;// 商品总数private Long recordCount;// 总页数private Long pageCount;// 当前页private long curPage;.....get和set方法.......略

9.5.2. controller代码

@Controller
@RequestMapping("/list")
public class SearchController {@Autowiredprivate SearchService searchService;@RequestMappingpublic String list(String queryString, String price, Integer page, Model model) throws Exception {if (StringUtils.isEmpty(page)) {page = 1;}if (page <= 0) {page = 1;}ResultModel resultModel = searchService.search(queryString, price, page);model.addAttribute("result", resultModel);model.addAttribute("queryString", queryString);model.addAttribute("price", price);model.addAttribute("page", page);return "search";}
}

9.5.3. service代码

service接口:

public interface SearchService {/*** 根据关键字全文检索** @param queryString   查询关键字* @param price         价格过滤条件* @param page          当前页*/public ResultModel search(String queryString, String price, Integer page) throws Exception;
}

service实现类:

@Service
public class SearchServiceImpl implements SearchService {//设置每页查询20条数据public final static Integer PAGE_SIZE = 20;@Overridepublic ResultModel search(String queryString, String price, Integer page) throws Exception{/*** 1. 需要使用的对象封装*/ResultModel resultModel = new ResultModel();List<Sku> skuList = new ArrayList<>();//从第几条开始查询Integer start = (page - 1) * PAGE_SIZE;//查询到多少条数据为止Integer end = page * PAGE_SIZE;// 创建分词器Analyzer analyzer = new IKAnalyzer();// 创建组合搜索对象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(new BooleanClause(query1, BooleanClause.Occur.MUST));/*** 3. 根据数字范围过滤查询*/if (!StringUtils.isEmpty(price)) {//搜索价格, 切分出最小值和最大值String[] split = price.split("-");// 创建数值范围搜索对象Query query2 = FloatPoint.newRangeQuery("price", Float.parseFloat(split[0]), Float.parseFloat(split[1]));builder.add(new BooleanClause(query2, BooleanClause.Occur.MUST));}// 4. 创建Directory流对象,声明索引库位置Directory directory = FSDirectory.open(Paths.get("E:\\dir"));// 5. 创建索引读取对象IndexReaderIndexReader reader = DirectoryReader.open(directory);// 6. 创建索引搜索对象IndexSearcher searcher = new IndexSearcher(reader);// 7. 使用索引搜索对象,执行搜索,返回结果集TopDocs// 第一个参数:搜索对象,第二个参数:返回的数据条数,指定查询结果最顶部的n条数据返回TopDocs topDocs = searcher.search(builder.build(), end);System.out.println("查询到的数据总条数是:" + topDocs.totalHits);//8. 获取查询到的总条数resultModel.setRecordCount(topDocs.totalHits);//9. 获取查询结果集ScoreDoc[] docs = topDocs.scoreDocs;//10. 解析结果集for (int i = start; i < end; i++) {//读取文档Document doc = reader.document(docs[i].doc);//封装查询到的结果数据Sku sku = new Sku();sku.setId(doc.get("id"));sku.setPrice(Integer.parseInt(String.valueOf(doc.get("price"))));sku.setImage(doc.get("image"));sku.setName(doc.get("name"));sku.setBrandName(doc.get("brandName"));sku.setCategoryName(doc.get("categoryName"));skuList.add(sku);}/*** 11. 封装返回的结果集*///. 结果集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);//12. 释放资源reader.close();return resultModel;}
}

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

10.1. 详细理解lucene存储结构

**存储结构 : **

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-00oq37n4-1624282922218)(image\036.png)]

**索引(Index) : **

  • 一个目录一个索引,在Lucene中一个索引是放在一个文件夹中的。

**段(Segment) : **

  • 一个索引(逻辑索引)由多个段组成, 多个段可以合并, 以减少读取内容时候的磁盘IO.
  • Lucene中的数据写入会先写内存的一个Buffer,当Buffer内数据到一定量后会被flush成一个Segment,每个Segment有自己独立的索引,可独立被查询,但数据永远不能被更改。这种模式避免了随机写,数据写入都是批量追加,能达到很高的吞吐量。Segment中写入的文档不可被修改,但可被删除,删除的方式也不是在文件内部原地更改,而是会由另外一个文件保存需要被删除的文档的DocID,保证数据文件不可被修改。Index的查询需要对多个Segment进行查询并对结果进行合并,还需要处理被删除的文档,为了对查询进行优化,Lucene会有策略对多个Segment进行合并。

**文档(Document) : **

  • 文档是我们建索引的基本单位,不同的文档是保存在不同的段中的,一个段可以包含多篇文档。
  • 新添加的文档是单独保存在一个新生成的段中,随着段的合并,不同的文档合并到同一个段中。

**域(Field) : **

  • 一篇文档包含不同类型的信息,可以分开索引,比如标题,时间,正文,描述等,都可以保存在不同的域里。
  • 不同域的索引方式可以不同。

**词(Term) : **

  • 词是索引的最小单位,是经过词法分析和语言处理后的字符串。

10.2. 索引库物理文件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FchUeVRt-1624282922219)(image\035.png)]

10.3. 索引库文件扩展名对照表

名称 文件扩展名 简短描述
Segments File segments_N 保存了一个提交点(a commit point)的信息
Lock File write.lock 防止多个IndexWriter同时写到一份索引文件中
Segment Info .si 保存了索引段的元数据信息
Compound File .cfs,.cfe 一个可选的虚拟文件,把所有索引信息都存储到复合索引文件中
Fields .fnm 保存fields的相关信息
Field Index .fdx 保存指向field data的指针
Field Data .fdt 文档存储的字段的值
Term Dictionary .tim term词典,存储term信息
Term Index .tip 到Term Dictionary的索引
Frequencies .doc 由包含每个term以及频率的docs列表组成
Positions .pos 存储出现在索引中的term的位置信息
Payloads .pay 存储额外的per-position元数据信息,例如字符偏移和用户payloads
Norms .nvd,.nvm .nvm文件保存索引字段加权因子的元数据,.nvd文件保存索引字段加权数据
Per-Document Values .dvd,.dvm .dvm文件保存索引文档评分因子的元数据,.dvd文件保存索引文档评分数据
Term Vector Index .tvx 将偏移存储到文档数据文件中
Term Vector Documents .tvd 包含有term vectors的每个文档信息
Term Vector Fields .tvf 字段级别有关term vectors的信息
Live Documents .liv 哪些是有效文件的信息
Point values .dii,.dim 保留索引点,如果有的话

10.4. 词典的构建

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

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

10.4.1. 词典数据结构对比

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

数据结构 优缺点
跳跃表 占用内存小,且可调,但是对模糊查询支持不好
排序列表Array/List 使用二分法查找,不平衡
字典树 查询效率跟字符串长度有关,但只适合英文词典
哈希表 性能高,内存消耗大,几乎是原始数据的三倍
双数组字典树 适合做中文词典,内存占用小,很多分词工具均采用此种算法
Finite State Transducers (FST) 一种有限状态转移机,Lucene 4有开源实现,并大量使用
B树 磁盘索引,更新方便,但检索速度慢,多用于数据库

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

10.4.2. 跳跃表原理

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

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

单链表 :

单链表中查询一个元素即使是有序的,我们也不能通过二分查找法的方式缩减查询时间。

通俗的讲也就是按照链表顺序一个一个找.

举例: 查找85这个节点, 需要查找7次.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xsOhAJzB-1624282922220)(image\040.png)]

跳跃表:

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zvqguaA2-1624282922221)(image\041.png)]

10.4.3. FST原理简析

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fEmyNVD8-1624282922222)(image\038.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aDypurqS-1624282922225)(image\039.png)]

输入数据:

String inputValues[] = {"hei","ma","cheng","xu","yuan","good"};
long outputValues[] = {0,1,2,3,4,5}; 

输入的数据如下:

hei/0
ma/1
cheng/2
xu/3
yuan/4
good/5

存储结果如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LDN67oFE-1624282922226)(image\037.png)]

9. Lucene优化(高级)

9.1. 解决大量磁盘IO

  • config.setMaxBufferedDocs(100000); 控制写入一个新的segment前内存中保存的document的数目,设置较大的数目可以加快建索引速度。

    数值越大索引速度越快, 但是会消耗更多的内存

  • indexWriter.forceMerge(文档数量); 设置N个文档合并为一个段

    数值越大索引速度越快, 搜索速度越慢; 值越小索引速度越慢, 搜索速度越快

    更高的值意味着索引期间更低的段合并开销,但同时也意味着更慢的搜索速度,因为此时的索引通常会包含更多的段。如果该值设置的过高,能获得更高的索引性能。但若在最后进行索引优化,那么较低的值会带来更快的搜索速度,因为在索引操作期间程序会利用并发机制完成段合并操作。故建议对程序分别进行高低多种值的测试,利用计算机的实际性能来告诉你最优值。

创建索引代码优化测试:

@Test
public void createIndexTest() throws Exception {// 1. 采集数据SkuDao skuDao = new SkuDaoImpl();List<Sku> skuList = skuDao.querySkuList();// 2. 创建Document文档对象List<Document> documents = new ArrayList<Document>();for (Sku sku : skuList) {Document document = new Document();// Document文档中添加Field域// 商品Id, 不分词,索引,存储document.add(new StringField("id", sku.getId(), Field.Store.YES));// 商品名称, 分词, 索引, 存储document.add(new TextField("name", sku.getName(), Field.Store.YES));// 商品价格, 分词,索引,不存储, 不排序document.add(new FloatPoint("price", sku.getPrice()));//添加价格存储支持document.add(new StoredField("price", sku.getPrice()));//添加价格排序支持//document.add(new NumericDocValuesField("price",sku.getPrice()));// 品牌名称, 不分词, 索引, 存储document.add(new StringField("brandName", sku.getBrandName(), Field.Store.YES));// 分类名称, 不分词, 索引, 存储document.add(new StringField("categoryName", sku.getCategoryName(), Field.Store.YES));// 图片地址, 不分词,不索引,存储document.add(new StoredField("image", sku.getImage()));// 把Document放到list中documents.add(document);}long startTime = System.currentTimeMillis();// 3. 创建Analyzer分词器,分析文档,对文档进行分词Analyzer analyzer = new IKAnalyzer();// 4. 创建Directory对象,声明索引库的位置Directory directory = FSDirectory.open(Paths.get("E:\\dir"));// 5. 创建IndexWriteConfig对象,写入索引需要的配置IndexWriterConfig config = new IndexWriterConfig(analyzer);//控制写入一个新的segment前内存中保存的document的数目,设置较大的数目可以加快建索引速度。config.setMaxBufferedDocs(100000);// 6.创建IndexWriter写入对象IndexWriter indexWriter = new IndexWriter(directory, config);//设置100000个文档合并为一个段indexWriter.forceMerge(100000);// 7.写入到索引库,通过IndexWriter添加文档对象documentfor (Document doc : documents) {indexWriter.addDocument(doc);}// 8.释放资源indexWriter.close();long endTime = System.currentTimeMillis();System.out.println("======运行时间为:===" + (endTime - startTime) + "ms");
}

9.2. 选择合适的分词器

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

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

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

写操作 读操作 特点
SimpleFSDirectory java.io.RandomAccessFile java.io.RandomAccessFile 简单实现,并发能力差
NIOFSDirectory java.nio.FileChannel FSDirectory.FSIndexOutput 并发能力强, windows平台下有重大bug
MMapDirectory 内存映射 FSDirectory.FSIndexOutput 读取操作基于内存

测试代码修改:

Directory directory = MMapDirectory.open(Paths.get("E:\\dir"));

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.人为影响相关度排序

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

@Test
public void testIndexSearch() throws Exception {long startTime = System.currentTimeMillis();// 1. 创建Query搜索对象// 创建分词器Analyzer analyzer = new IKAnalyzer();//查询的域名String[] fields = {"name","brandName","categoryName"};//设置权重Map<String, Float> boots = new HashMap<>();boots.put("categoryName", 10000000f);// 根据多个域进行搜索MultiFieldQueryParser queryParser = new MultiFieldQueryParser(fields, analyzer, boots);// 创建搜索对象Query query = queryParser.parse("手机");// 2. 创建Directory流对象,声明索引库位置Directory directory = MMapDirectory.open(Paths.get("E:\\dir"));// 3. 创建索引读取对象IndexReaderIndexReader reader = DirectoryReader.open(directory);// 4. 创建索引搜索对象IndexSearcher searcher = new IndexSearcher(reader);// 5. 使用索引搜索对象,执行搜索,返回结果集TopDocs// 第一个参数:搜索对象,第二个参数:返回的数据条数,指定查询结果最顶部的n条数据返回TopDocs topDocs = searcher.search(query, 50);System.out.println("查询到的数据总条数是:" + topDocs.totalHits);// 获取查询结果集ScoreDoc[] docs = topDocs.scoreDocs;// 6. 解析结果集for (ScoreDoc scoreDoc : docs) {// 获取文档int docID = scoreDoc.doc;Document doc = searcher.doc(docID);System.out.println("=============================");System.out.println("docID:" + docID);System.out.println("id:" + doc.get("id"));System.out.println("name:" + doc.get("name"));System.out.println("price:" + doc.get("price"));System.out.println("brandName:" + doc.get("brandName"));System.out.println("image:" + doc.get("image"));}// 7. 释放资源reader.close();long endTime = System.currentTimeMillis();System.out.println("==========消耗时间:============" + (startTime - endTime) + "ms");
}

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

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

全文检索Lucene相关推荐

  1. 全文检索 Lucene

    全文检索 Lucene 网上学习黑马课程后,在原有笔记的基础上进行总结 课程计划 lucene入门 什么是lucene Lucene的作用 使用场景 优点和缺点 lucene应用 索引流程 搜索流程 ...

  2. 全文检索lucene中文分词的一些总结

    为什么80%的码农都做不了架构师?>>>    全文检索几乎是所有内容管理系统软件(CMS)必备的功能,在对公司的CMS产品的开发维护过程中,全文检索始终是客户重点关注的模块,为满足 ...

  3. 全文检索Lucene(一)--Document操作与文本域加权

    一.搭建基本环境        在eclipse中创建普通的maven项目,并在pom.xml中引入相应的依赖.        这里只给出pom.xml中的依赖 <dependencies> ...

  4. ajax+lucene pdf,基于Ajax/Lucene的站内搜索技术研究

    摘要: 站内搜索引擎是找出网站重要信息的必要工具,高效的站内搜索将有助于提升网站的价值,发挥网站应有的作用.虽然现在一些网络巨头已开始研究并应用这类工具,但整个互联网行业中,受制于技术的门槛,真正的站 ...

  5. Lucene-01 全文检索基本介绍

    文章目录 课程计划 什么是全文检索 数据分类 结构化数据搜索 非结构化数据查询方法 如何实现全文检索 全文检索的应用场景 Lucene实现全文检索的流程 索引和搜索流程图 创建索引 获得原始文档 创建 ...

  6. lucene ---- 黑马教程

    https://www.bilibili.com/video/BV1eJ411q7nw/?spm_id_from=333.337.search-card.all.click 文章目录 全文检索Luce ...

  7. java徐仕锋_传智播客云计算开发 Hadopp视频教程 传智播客之javaEE + Hadoop...

    课程介绍 本套教程课件+代码+PPT+文档齐全!java零基础入门的高端课程 小白的最好教程,27天学会Java基础,80天左右学会JavaEE开发.包括最新的ssh国家税务协同平台项目,ssm进销存 ...

  8. .NET常用第三方库(包)总结

    序列化与反序列化 JSON.NET 应该是.NET平台上使用最为广泛的序列化/反序列化包了,ASP.NET和ASP.NET Core中默认序列化/反序列化包 Jil 官网上说性能优于JSON.NET ...

  9. java 开源控件_一些好用的开源控件

    工作两年,一直都在做些编码方面的表面功夫,实现了很多很炫的功能,在此写下一些体验.有些比较小的dll文件我会发上来,如果是开源组织的代码我会把地址附上,毕竟人家是会更新的.大家还有什么好用的开源控件欢 ...

最新文章

  1. Android 7.0 出现 ”FileUriExposedException“ 和 ”解析包出现错误“ 异常的解决办法...
  2. 动态引入/添加js脚本
  3. 省队集训Day3 tree
  4. ghelper怎么在手机上用_当长时间不用手机玩《崩坏3》、《战双》
  5. nehe教程混合这一节需要注意的两个地方
  6. @Repository , @Service , @Controller 和 @Component
  7. c语言10个数如何求最大值,C语言,输入10个数怎样输出10个数中最大值,最小值(大一计算机)...
  8. Esxi直接使用vmdk文件创建虚机
  9. 关于window.showModalDialog遭遇frameset不能初始化对话框大小
  10. sql select 0 字段 某字段是不在指定的表
  11. SharePoint 2019 图文安装教程
  12. 简单介绍会计师事务所
  13. cmos与非门电路、或非门电路
  14. 文献阅读(167)NoC神经网络加速器
  15. 二维码如何制作?简单的制作方法介绍
  16. Axure RP 8.0原型设计(一):入门
  17. 统一网关 Gateway
  18. 乔布斯和盖茨的历史性对话(转录)
  19. android 家长控制app下载,任天堂家长控制app下载-任天堂家长控制 安卓版v1.0.2-PC6安卓网...
  20. 毕博与中国大学mooc对比

热门文章

  1. python拼多多1分抢手机_如何抢到拼多多1元秒杀?抢不到是怎么回事?
  2. 【单细胞高级绘图】10.KEGG富集结果的圆圈图
  3. 一篇文章带你摸清高保真、低保真原型间的区别
  4. Sql Server 2012完全彻底卸载教程
  5. css像素px,物理像素(pt),设备像素比(dpr),1px边框问题,viewport自适应
  6. java-php-python-springboot线上教学平台计算机毕业设计
  7. 从多核到众核处理器究竟经历了什么?
  8. JavaScript中的数据结构和算法
  9. swaks伪造钓鱼邮件
  10. HashMap为什么用链表加红黑树?目的是什么?原理是什么