Lucene--千锋修改+踩坑版本
1. 搜索技术理论基础
1.1. 为什么要学习Lucene
原来的⽅式实现搜索功能,我们的搜索流程如下图:
上图就是原始搜索引擎技术,如果⽤户⽐较少⽽且数据库的数据量⽐较⼩,那么这种⽅式实现搜索功能在企业中是⽐较常⻅的。但是数据量过多时,数据库的压⼒就会变得很⼤,查询速度会变得⾮常慢。我们需要使⽤更好的解决⽅案来分担数据库的压⼒。
现在的⽅案(使⽤Lucene),如下图
为了解决数据库压⼒和速度的问题,我们的数据库就变成了索引库,我们使⽤Lucene的API的
来操作服务器上的索引库。这样完全和数据库进⾏了隔离。
1.2. 数据查询⽅法
1.2.1. 顺序扫描法
算法描述:
所谓顺序扫描,例如要找内容包含⼀个字符串的⽂件,就是⼀个⽂档⼀个⽂档的看,对于每⼀个⽂档,从头看到尾,如果此⽂档包含此字符串,则此⽂档为我们要找的⽂件,接着看下⼀个⽂件,直到扫描完所有的⽂件。
优点:
查询准确率⾼
缺点:
查询速度会随着查询数据量的增⼤, 越来越慢
使⽤场景:
数据库中的like关键字模糊查询
⽂本编辑器的Ctrl + F 查询功能
1.2.2. 倒排索引
先举⼀个栗⼦:
例如我们使⽤新华字典查询汉字,新华字典有偏旁部⾸的⽬录(索引),我们查字⾸先查这个⽬录,找到这个⽬录中对应的偏旁部⾸,就可以通过这个⽬录中的偏旁部⾸找到这个字所在的位置(⽂档)。
Lucene会对⽂档建⽴倒排索引
1、 提取资源中关键信息, 建⽴索引 (⽬录)
2、 搜索时,根据关键字(⽬录),找到资源的位置
算法描述:
查询前会先将查询的内容提取出来组成⽂档(正⽂), 对⽂档进⾏切分词组成索引(⽬录), 索引和⽂档有关联关系, 查询的时候先查询索引, 通过索引找⽂档的这个过程叫做全⽂检索。
切分词 : 就是将⼀句⼀句话切分成⼀个⼀个的词, 去掉停⽤词(的, 地, 得, a, an, the等)。去掉空格, 去掉标点符号, ⼤写字⺟转成⼩写字⺟, 去掉重复的词。
为什么倒排索引⽐顺序扫描快?
理解 : 因为索引可以去掉重复的词, 汉语常⽤的字和词⼤概等于, 字典加词典, 常⽤的英⽂在⽜津词典也有收录.如果⽤计算机的速度查询, 字典+词典+⽜津词典这些内容是⾮常快的. 但是⽤这些字典, 词典组成的⽂章却是千千万万不计其数. 索引的⼤⼩最多也就是字典+词典. 所以通过查询索引, 再通过索引和⽂档的关联关系找到⽂档速度⽐较快. 顺序扫描法则是直接去逐个查询那些不计其数的⽂章就算是计算的速度也会很慢.
优点:
查询准确率⾼
查询速度快, 并且不会因为查询内容量的增加, ⽽使查询速度逐渐变慢
缺点:
索引⽂件会占⽤额外的磁盘空间, 也就是占⽤磁盘量会增⼤。
使⽤场景:
海量数据查询, pb级数据查询
1.3. 全⽂检索技术应⽤场景
应⽤场景 :
1、 站内搜索 (baidu贴吧、论坛、 京东、 taobao)
2、 垂直领域的搜索 (818⼯作⽹)
3、 专业搜索引擎公司 (google、baidu)
2. Lucene介绍
2.1. 什么是全⽂检索
计算机索引程序通过扫描⽂章中的每⼀个词,对每⼀个词建⽴⼀个索引,指明该词在⽂章中出现的次数和位置,当⽤户查询时,检索程序就根据事先建⽴的索引进⾏查找,并将查找的结果反馈给⽤户的检索⽅式
2.2. 什么是Lucene
Lucene是apache软件基⾦会4 jakarta项⽬组的⼀个⼦项⽬,是⼀个开放源代码的全⽂检索引擎⼯具包,但它不是⼀个完整的全⽂检索引擎,⽽是⼀个全⽂检索引擎的架构,提供了完整的查询引擎和索引引擎,部分⽂本分析引擎(英⽂与德⽂两种⻄⽅语⾔)。
Lucene的⽬的是为软件开发⼈员提供⼀个简单易⽤的⼯具包,以⽅便的在⽬标系统中实现全⽂检索的功能,或者是以此为基础建⽴起完整的全⽂检索引擎。
⽬前已经有很多应⽤程序的搜索功能是基于 Lucene 的,⽐如 Eclipse 的帮助系统的搜索功能。Lucene 能够为⽂本类型的数据建⽴索引,所以你只要能把你要索引的数据格式转化的⽂本的,Lucene 就能对你的⽂档进⾏索引和搜索。⽐如你要对⼀些 HTML ⽂档,PDF ⽂档进⾏索引的话你就⾸先需要把 HTML ⽂档和 PDF ⽂档转化成⽂本格式的,然后将转化后的内容交给 Lucene 进⾏索引,然后把创建好的索引⽂件保存到磁盘或者内存中,最后根据⽤户输⼊的查询条件在索引⽂件上进⾏查询。不指定要索引的⽂档的格式也使 Lucene 能够⼏乎适⽤于所有的搜索应⽤程序。
3. Lucene全⽂检索的流程
3.1. 索引和搜索流程图
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,如下图:
注意:每个Document可以有多个Field,不同的Document可以有不同的Field,同⼀个Document可以有相同的Field(域名和域值都相同)
3.2.4. 分析⽂档
将原始内容创建为包含域(Field)的⽂档(document),需要再对域中的内容进⾏分析,分析成为⼀个⼀个的单词。⽐如下边的⽂档经过分析如下:
原⽂档内容:
vivo Z3 6GB+64GB 极光蓝 性能实⼒派 全⾯屏游戏⼿机 移动联通电信全⽹通4G⼿机
华为 HUAWEI 畅享9 Plus 4GB+64GB 幻夜⿊ 全⽹通 四摄超清全⾯屏⼤电池 移动联通电信4G⼿机 双卡双待
分析后得到的词:vivo, Z3, 6GB, 64GB, 极光, 极光蓝, 全⽹, 全⽹通, ⽹通, 4G, ⼿机, 华为, HUAWEI, 畅享9 。。。。
3.2.5. 索引⽂档
对所有⽂档分析得出的语汇单元进⾏索引,索引的⽬的是为了搜索,最终要实现只搜索被索引的语汇单元从⽽找到Document(⽂档)。
创建索引是对语汇单元索引,通过词语找⽂档,这种索引的结构叫倒排索引结构。
倒排索引结构是根据内容(词汇)找⽂档,如下图:
3.2.6 Lucene底层存储结构
4. Lucene⼊⻔
4.1. 开发环境
mysql文件去千锋要,navicat先新建表,然后导入sqsl文件即可
4.2. 创建Java⼯程
配置文件(其中ik见另一篇博客–maven 导本地包)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>org.nan</groupId><artifactId>luceneDemo</artifactId><version>1.0-SNAPSHOT</version><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>8.11.1</version></dependency><dependency><groupId>org.apache.lucene</groupId><artifactId>lucene-analyzers-common</artifactId><version>8.11.1</version><exclusions><exclusion><groupId>org.apache.lucene</groupId><artifactId>lucene-core</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.apache.lucene</groupId><artifactId>lucene-queryparser</artifactId><version>8.11.1</version><exclusions><exclusion><groupId>org.apache.lucene</groupId><artifactId>lucene-core</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.apache.lucene</groupId><artifactId>lucene-backward-codecs</artifactId><version>8.11.1</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>com.nan</groupId><artifactId>luceneDemo</artifactId><version>8.5.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>
</project>
提前创建索引库的文件夹(在我的D盘)
查询过程
4.3. 索引流程
4.3.1. 数据采集
在电商⽹站中,全⽂检索的数据源在数据库中,需要通过jdbc访问数据库中 sku 表的内容。
4.3.1.1. 创建pojo
用lombok或者直接生成get/set,在生成一个tostring
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;}
4.3.1.2. 创建DAO接⼝
public interface SkuDao {/*** 查询所有的Sku数据* @return**/public List<Sku> querySkuList();
}
4.3.1.3. 创建DAO接⼝实现类
使⽤jdbc实现
public class SkuDaoImpl implements SkuDao{@Overridepublic 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 =(Connection) DriverManager.getConnection("jdbc:mysql://localhost:3306/你的表名?serverTimezone=GMT%2b8","你的用户名", "你的密码");// SQL语句String sql = "SELECT * FROM tb_sku";// 创建preparedStatementpreparedStatement = (PreparedStatement) 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.3.2. 实现索引流程(此处代码已经是修改域后的,即完全符合数据库的)
- 采集数据
- 创建Document⽂档对象
- 创建分析器(分词器)
- 创建IndexWriterConfig配置信息类
- 创建Directory对象,声明索引库存储位置
- 创建IndexWriter写⼊对象
- 把Document写⼊到索引库中
- 释放资源
第三步注意用IK分词器,因为数据库内的是中文。
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域,参数:域名,域值,是否存储,Store.YES:表示存储到⽂档域中// 商品Id, 不分词,索引,存储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 StringField("image", sku.getImage(), Field.Store.YES));// 分类名称, 不分词, 索引, 存储document.add(new StringField("categoryName", sku.getCategoryName(), Field.Store.YES));// 品牌名称, 不分词, 索引, 存储document.add(new StringField("brandName", sku.getBrandName(), Field.Store.YES));// 把Document放到list中documents.add(document);}// 3. 创建Analyzer分词器,分析⽂档,对⽂档进⾏分词Analyzer analyzer = new IKAnalyzer();// 4. 创建Directory对象,声明索引库的位置//FSDirectory file system Directory 会将数据存储到硬盘中Directory directory = FSDirectory.open(Paths.get("D:\\luceneDir"));// 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();}
}
代表成功
4.4. 搜索流程
4.4.1. 输⼊查询语句
Lucene可以通过query对象输⼊查询语句。同数据库的sql⼀样,lucene也有固定的查询语法:最基本的有⽐如:AND, OR, NOT 等(必须⼤写)
举个栗⼦:
⽤户想找⼀个 name 域中包括 ⼿ 或 机 关键字的⽂档。
它对应的查询语句:name:⼿ OR name:机
4.4.1.1. 搜索分词
和索引过程的分词⼀样,这⾥要对⽤户输⼊的关键字进⾏分词,⼀般情况索引和搜索使⽤的分词器⼀致。
⽐如:输⼊搜索关键字“java学习”,分词后为java和学习两个词,与java和学习有关的内容都搜索出来了,如下:
4.4.2. 代码实现
- 创建Query搜索对象
- 创建Directory流对象,声明索引库位置
- 创建索引读取对象IndexReader
- 创建索引搜索对象IndexSearcher
- 使⽤索引搜索对象,执⾏搜索,返回结果集TopDocs
- 解析结果集
- 释放资源
IndexSearcher搜索⽅法如下:
代码实现(此处要用ik,搜素与之前索引时对应)
public class TestSearch {@Testpublic void testIndexSearch() throws Exception {// 1. 创建Query搜索对象// 创建分词器,搜词用不上,但你要敲了一个句子,搜句子的话,句子要进行切分词,在跟索引进行对比,查询Analyzer analyzer = new IKAnalyzer();// 创建搜索解析器,第⼀个参数:默认Field域,第⼆个参数:分词器QueryParser queryParser = new QueryParser("brandName", analyzer);// 创建搜索对象Query query = queryParser.parse("华为手机");// 2. 创建Directory流对象,声明索引库位置Directory directory = FSDirectory.open(Paths.get("D:\\luceneDir"));// 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.value);// 获取查询结果集ScoreDoc[] docs = topDocs.scoreDocs;// 6. 解析结果集for (ScoreDoc scoreDoc : docs) {// 获取⽂档的唯一idint docID = scoreDoc.doc;//根据文档id获取文档内容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的属性,根据需求选择:
intpoint–索引+分词,不存储(拿不到文档数据)
storefield–与上面的正好相反,两个一起,起到了索引、分词、存储
所以Stringfield用于存储商品id等不需要分词的,textfield用于存商品名称,描述等大文本类型的。
5.3. Field修改
已完成修改,分析的内容上文就是。
6. 索引维护
6.1. 需求
管理⼈员通过电商系统更改图书信息,这时更新的是关系数据库,如果使⽤lucene搜索图书信息,需要在数据库表book信息变化时及时更新lucene索引库。
6.2. 添加索引
调⽤ indexWriter.addDocument(doc)添加索引。参考⼊⻔程序的创建索引。
6.3. 修改索引
更新索引是先删除再添加,建议对更新需求采⽤此⽅法并且要保证对已存在的索引执⾏更新,可以先查询出来,确定更新记录存在执⾏更新操作。如果更新索引的⽬标⽂档对象不存在,则执⾏添加。
对此条进行更新
代码
@Testpublic void testIndexUpdate() throws Exception {//1. 创建Document,需要变更的内容Document document = new Document();document.add(new StringField("id", "21233662915", Field.Store.YES));document.add(new TextField("name", "八嘎八嘎", Field.Store.YES));document.add(new StringField("image","你先别急.jpg", Field.Store.YES));//2. 创建分词器Analyzer analyzer = new IKAnalyzer();//3. 创建Directory流对象Directory directory = FSDirectory.open(Paths.get("D:\\luceneDir"));// 创建IndexWriteConfig对象,写⼊索引需要的配置IndexWriterConfig config = new IndexWriterConfig(analyzer);// 创建写⼊对象IndexWriter indexWriter = new IndexWriter(directory, config);// 执⾏更新,会把所有符合条件的Document删除,再新增。indexWriter.updateDocument(new Term("id", "21233662915"), document);// 释放资源indexWriter.close();}
6.4. 删除索引
6.4.1. 删除指定索引
根据Term项删除索引,满⾜条件的将全部删除。
@Testpublic void testIndexDelete() throws Exception {// 创建分词器Analyzer analyzer = new IKAnalyzer();// 创建Directory流对象Directory directory = FSDirectory.open(Paths.get("D:\\luceneDir"));// 创建IndexWriteConfig对象,写⼊索引需要的配置IndexWriterConfig config = new IndexWriterConfig(analyzer);// 创建输出流对象IndexWriter indexWriter = new IndexWriter(directory, config);// 根据Term删除索引库,name:javaindexWriter.deleteDocuments(new Term("id", "21233662915"));// 释放资源indexWriter.close();}
6.4.2. 删除全部索引(慎⽤)
将索引⽬录的索引信息全部删除,直接彻底删除,⽆法恢复。
建议参照关系数据库基于主键删除⽅式,所以在创建索引时需要创建⼀个主键Field,删除时根据此主键Field删除。
索引删除后将放在Lucene的回收站中,Lucene3.X版本可以恢复删除的⽂档,3.X之后⽆法恢复。
// 全部删除indexWriter.deleteAll();
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值进⾏分析.
对于⼀些Field可以不⽤分析:
1、不作为查询条件的内容,⽐如⽂件路径
2、不是匹配内容中的词⽽匹配Field的整体内容,⽐如订单号、身份证号等。
7.2.2. 搜索时使⽤Analyzer
对搜索关键字进⾏分析和索引分析⼀样,使⽤Analyzer对搜索关键字进⾏分析、分词处理,使⽤分析后每个词语进⾏搜索。⽐如:搜索关键字:spring web ,经过分析器进⾏分词,得出:spring web拿词去索引词典表查找 ,找到索引链接到Document,解析Document内容。
对于匹配整体Field域的查询可以在搜索时不分析,⽐如根据订单号、身份证号查询等。
若是按照精确匹配,则可能一个也搜不到(这就是为什么之前强调要用ik,且要一样)
注意:搜索使⽤的分析器要和索引使⽤的分析器⼀致。
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提供了很多的分词器过滤器,⽐如⼤⼩写转换、去除停⽤词等。
如下图是语汇单元的⽣成过程:
从⼀个Reader字符流开始,创建⼀个基于Reader的Tokenizer分词器,经过三个TokenFilter⽣成语汇单元Token。
⽐如下边的⽂档经过分析器分析如下:
7.3.2. WhitespaceAnalyzer
特点 :
仅仅是去掉了空格,没有其他任何操作,不⽀持中⽂。
7.3.3. SimpleAnalyzer
特点 :
将除了字⺟以外的符号全部去除,并且将所有字⺟变为⼩写,需要注意的是这个分词器同样把数字也去除了,同样不⽀持中⽂。
7.3.4. CJKAnalyzer
特点 :
这个⽀持中⽇韩⽂字,前三个字⺟也就是这三个国家的缩写。对中⽂是⼆分法分词, 去掉空格,去掉标点符号。个⼈感觉对中⽂⽀持依旧很烂
7.4. 第三⽅中⽂分词器
7.4.1. 什么是中⽂分词器
学过英⽂的都知道,英⽂是以单词为单位的,单词与单词之间以空格或者逗号句号隔开。所以对于英⽂,我们可以简单以空格判断某个字符串是否为⼀个单词,⽐如I love China,love 和 China很容易被程序区分开来。
⽽中⽂则以字为单位,字⼜组成词,字和词再组成句⼦。中⽂“我爱中国”就不⼀样了,电脑不知道“中国”是⼀个词语还是“爱中”是⼀个词语。
把中⽂的句⼦切分成有意义的词,就是中⽂分词,也称切词。我爱中国,分词的结果是:我、爱、中国。
7.4.2. 第三⽅中⽂分词器简介
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⽉后没有在更新。
7.4.3. 使⽤中⽂分词器IKAnalyzer
IKAnalyzer继承Lucene的Analyzer抽象类,使⽤IKAnalyzer和Lucene⾃带的分析器⽅法⼀样,将Analyzer测试代码改为IKAnalyzer测试中⽂分词效果。如果使⽤中⽂分词器ik-analyzer,就需要在索引和搜索程序中使⽤⼀致的分词器:IK-analyzer
7.4.4. 扩展中⽂词库
如果想配置扩展词和停⽤词,就创建扩展词的⽂件和停⽤词的⽂件。
停⽤词典stopword.dic作⽤ :
停⽤词典中的词例如: a, an, the, 的, 地, 得等词汇, 凡是出现在停⽤词典中的字或者词, 在切分词的时候会被过滤掉.
扩展词典ext.dic作⽤ :
扩展词典中的词例如: 千锋教育, 贵州茅台等专有名词, 在汉语中⼀些公司名称, ⾏业名称, 分类,品牌等不是汉语中的词汇, 是专有名词. 这些分词器默认不识别, 所以需要放⼊扩展词典中, 效果是被强制分成⼀个词.
8.高级搜索
8.1 多关键词查询
在之前创建搜索对象时,我们直接写的是华为手机,此时被分割成是 or连接还是and连接呢?(因为是将两个结果集进行合并操作)
// 1. 创建Query搜索对象// 创建分词器,搜词用不上,但你要敲了一个句子,搜句子的话,句子要进行切分词,在跟索引进行对比,查询Analyzer analyzer = new IKAnalyzer();// 创建搜索解析器,第⼀个参数:默认Field域,第⼆个参数:分词器QueryParser queryParser = new QueryParser("brandName", analyzer);// 创建搜索对象Query query = queryParser.parse("华为手机");
将最后一行替换进行检测
Query query = queryParser.parse("name:⼿机 AND 华为");
Query query = queryParser.parse("name:⼿机 OR 华为");
结论:华为手机与name:⼿机 OR 华为 结果集相同。
8.2 数据范围查询
/***根据数值范围查询* 需求:根据价格查询100-1000元的商品*/@Testpublic void testRangeQuery() throws Exception{//1.创建查询对象//域名,起始值,结束值Query query = IntPoint.newRangeQuery("price",100,1000);//2.创建Directory目录对象,指定索引库的位置FSDirectory dir = FSDirectory.open(Paths.get("D:\\luceneDir"));//3.创建输入流对象IndexReader reader = DirectoryReader.open(dir);//4.创建搜索对象IndexSearcher indexSearcher = new IndexSearcher(reader);//5.搜索并返回结果//查询10个TopDocs topDocs = indexSearcher.search(query,10);System.out.println("=======count==="+topDocs.totalHits);//6.获取结果ScoreDoc[] scoreDocs = topDocs.scoreDocs;//7.遍历结果集if(scoreDocs != null) {for (ScoreDoc scoreDoc : scoreDocs) {// 获取⽂档的唯一idint docID = scoreDoc.doc;//根据文档id获取文档内容Document doc = indexSearcher.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"));}}}
8.3 组合查询
/*** 组合查询* 需求:* 根据商品名字查询,查询华为手机关键字* 价格还要在100-1000范围内的商品*/@Testpublic void testBoolQuery() throws ParseException, IOException {//1.创建分词器Analyzer analyzer = new IKAnalyzer();//2.根据商品名称进行查询QueryParser queryParser = new QueryParser("name", analyzer);//根据关键字进行查询Query query1 = queryParser.parse("⼿机华为");//根据价格范围查询Query query2 = IntPoint.newRangeQuery("price",100,1000);//创建组合查询对象//BooleanClause.Occur.SHOULD 或者 相当于or//BooleanClause.Occur.MUST 并且 相当于and//BooleanClause.Occur.MUST_NOT 非 相当于notBooleanQuery.Builder booleanQuery = new BooleanQuery.Builder();booleanQuery.add(query1, BooleanClause.Occur.MUST);booleanQuery.add(query2,BooleanClause.Occur.MUST);// 3. 创建Directory流对象,声明索引库位置Directory directory = FSDirectory.open(Paths.get("D:\\luceneDir"));// 4. 创建索引读取对象IndexReaderIndexReader reader = DirectoryReader.open(directory);// 5. 创建索引搜索对象IndexSearcher searcher = new IndexSearcher(reader);// 6. 使⽤索引搜索对象,执⾏搜索,返回结果集TopDocsTopDocs topDocs = searcher.search(booleanQuery.build(), 10);System.out.println("查询到的数据总条数是:" + topDocs.totalHits.value);// 获取查询结果集ScoreDoc[] docs = topDocs.scoreDocs;// 7. 解析结果集for (ScoreDoc scoreDoc : docs) {// 获取⽂档的唯一idint docID = scoreDoc.doc;//根据文档id获取文档内容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"));}// 8. 释放资源reader.close();}
9. 综合案例
9.1引入依赖
pom.xml和上面一样
9.2 项⽬加⼊⻚⾯和资源
页面和静态资源在千锋教育包里,复制粘贴到resources目录下
9.3. 创建包和启动类
和之前一样
9.4. 配置⽂件
项⽬的resources⽬录下创建application.yml内容如下:
多一个关闭thymelaeaf缓存。若不关闭,调试完后,重启tomcat不生效。
server:port: 8080
spring:thymeleaf:cache: false
9.5. 业务代码:
9.5.1. 封装pojo
public class ResultModel {// 商品列表private List<Sku> skuList;// 商品总数private Long recordCount;// 总⻚数private Long pageCount;// 当前⻚private long curPage;get+set+toString
}
9.5.2. controller代码
@Controller
//<form th:id="actionForm" th:action="list" th:method="POST"> 一一对应
//<div class="form">
// <input th:type="text" class="text"
// th:name="queryString" th:id="key" th:value="${queryString }">
// <input type="button" value="搜索" class="button" οnclick="query()">
//</div>
//<input th:type="hidden" th:name="price" th:id="price" th:value="${price }"/>
//<input th:type="hidden" th:name="page" th:id="page" th:value="${result.curPage }"/>
//</form>
@RequestMapping("/list")
public class SearchController {@Autowiredprivate SearchService searchService;@RequestMapping// 价格在这里是范围,所以用string来接受public String list(String queryString, String price, Integer page, Model model) throws Exception {//1.处理当前页,页面参数if (StringUtils.isEmpty(page)) {//如果为空,page置1,没有第0页page = 1;}if (page <= 0) {page = 1;}//2.调用service搜索业务方法ResultModel resultModel = searchService.search(queryString, price, page);//3.封装返回的数据给页面,把参数返回给springMVC默认支持的model里面//比如:// <div class="pagin pagin-m">// <span class="text"><i th:text="${result.curPage }"></i>/<i th:text="${result.pageCount }"></i></span>// <a href="javascript:changePage(-1)" class="prev">上一页<b></b></a>// <a href="javascript:changePage(1)" class="next">下一页<b></b></a>// </div>// <div class="total">// <span>共<strong th:text="${result.recordCount }"></strong>个商品// </span>// </div>model.addAttribute("result", resultModel);//参数的回显model.addAttribute("queryString", queryString);model.addAttribute("price", price);model.addAttribute("page", page);//指定页面的位置--search.html,扩展名去掉return "search";}
}
9.5.3. service代码
service接⼝:
public interface SearchService {/*** 根据关键字全⽂检索** @param queryString 查询关键字* @param price 价格过滤条件* @param page 当前⻚*///把controller传入的三个参数复制过来public ResultModel search(String queryString, String price, Integer page) throws Exception;
}
service实现类:
不要用增强for循环遍历。要保证是从start开始遍历到end截至
@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 {long startTime = System.currentTimeMillis();//1. 封装一个返回的分页对象ResultModel resultModel = new ResultModel();//2.处理分页//从第⼏条开始查询int start = (page - 1) * PAGE_SIZE;//查询到多少条为⽌int end = page * PAGE_SIZE;//3.创建中文分词器Analyzer analyzer = new IKAnalyzer();//4.创建组合查询对象BooleanQuery.Builder builder = new BooleanQuery.Builder();//5. 根据查询关键字封装查询对象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);//6. 根据价格范围封装查询对象if (!StringUtils.isEmpty(price)) {String[] split = price.split("-");Query query2 = IntPoint.newRangeQuery("price", Integer.parseInt(split[0]), Integer.parseInt(split[1]));//7.将价格查询对象, 封装到组合查询对象中builder.add(query2, BooleanClause.Occur.MUST);}//8. 创建Directory⽬录对象, 指定索引库的位置/*** 使⽤MMapDirectory消耗的查询时间* ====消耗时间为=========324ms* ====消耗时间为=========18ms*/Directory directory = FSDirectory.open(Paths.get("D:\\luceneDir"));//9. 创建输⼊流对象IndexReader reader = DirectoryReader.open(directory);//10. 创建搜索对象IndexSearcher indexSearcher = new IndexSearcher(reader);//11. 搜索并获取搜索结果TopDocs topDocs = indexSearcher.search(builder.build(), end);//12. 获取查询到的总条数resultModel.setRecordCount(topDocs.totalHits.value);//13. 获取查询到的结果集ScoreDoc[] scoreDocs = topDocs.scoreDocs;long endTime = System.currentTimeMillis();System.out.println("====消耗时间为=========" + (endTime - startTime) + "ms");//14. 遍历结果集封装返回的数据List<Sku> skuList = new ArrayList<>();if (scoreDocs != null) {for (int i = start; i < end; i ++) {//获取文档id--scoreDocs[i].doc//通过查询到的⽂档编号, 找到对应的⽂档对象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);}}//15.封装查询到的结果集resultModel.setSkuList(skuList);//16.封装当前⻚resultModel.setCurPage(page);//17.总⻚数Long pageCount = topDocs.totalHits.value % PAGE_SIZE > 0 ?(topDocs.totalHits.value/PAGE_SIZE) + 1 :topDocs.totalHits.value/PAGE_SIZE;resultModel.setPageCount(pageCount);return resultModel;}
}
显示效果:
10. Lucene底层储存结构(⾼级)
10.1. 详细理解lucene存储结构
存储结构 :
索引库(Index) :
⼀个⽬录⼀个索引库,在Lucene中⼀个索引库是放在⼀个⽂件夹中的。比如商品数据–一个索引库,评论–另外一个索引库。
段(Segment) :
⼀个索引(逻辑索引)由多个段组成, 多个段可以合并, 以减少读取内容时候的磁盘IO.
Lucene中的数据写⼊会先写内存的⼀个Buffer,当Buffer内数据到⼀定量后会被flush成⼀个Segment,每个Segment有⾃⼰独⽴的索引,可独⽴被查询,但数据永远不能被更改。这种模式避免了随机写,数据写⼊都是批量追加,能达到很⾼的吞吐量。Segment中写⼊的⽂档不可被修改,但可被删除,删除的⽅式也不是在⽂件内部原地更改,⽽是会由另外⼀个⽂件保存需要被删除的⽂档的DocID,保证数据⽂件不可被修改。Index的查询需要对多个Segment进⾏查询并对结果进⾏合并,还需要处理被删除的⽂档,为了对查询进⾏优化,Lucene会有策略对多个Segment进⾏合并。
⽂档(Document) :
⽂档是我们建索引的基本单位,不同的⽂档是保存在不同的段中的,⼀个段可以包含多篇⽂档。新添加的⽂档是单独保存在⼀个新⽣成的段中,随着段的合并,不同的⽂档合并到同⼀个段中。
域(Field) :
⼀篇⽂档包含不同类型的信息,可以分开索引,⽐如标题,时间,正⽂,描述等,都可以
保存在不同的域⾥。
不同域的索引⽅式可以不同。
词(Term) :
词是索引的最⼩单位,是经过词法分析和语⾔处理后的字符串。
10.2. 索引库物理⽂件
10.3. 索引库⽂件扩展名对照表
10.4. 词典的构建
为何Lucene⼤数据量搜索快, 要分两部分来看 :
⼀点是因为底层的倒排索引存储结构.
另⼀点就是查询关键字的时候速度快, 因为词典的索引结构.
10.4.1. 词典数据结构对⽐
倒排索引中的词典位于内存,其结构尤为重要,有很多种词典结构,各有各的优缺点,最简单如排序数组,通过⼆分查找来检索数据,更快的有哈希表,磁盘查找有B树、B+树,但⼀个能⽀持TB级数据的倒排索引结构需要在时间和空间上有个平衡,下图列了⼀些常⻅词典的优缺点:
Lucene3.0之前使⽤的也是跳跃表结构,后换成了FST,但跳跃表在Lucene其他地⽅还有应⽤如倒排表合并和⽂档号索引。
10.4.2. 跳跃表原理
Lucene3.0版本之前使⽤的跳跃表结构后换成了FST结构
优点 :结构简单、跳跃间隔、级数可控,Lucene3.0之前使⽤的也是跳跃表结构,但跳跃表在Lucene其他地⽅还有应⽤如倒排表合并和⽂档号索引。
缺点 :模糊查询⽀持不好.
10.4.3. FST原理简析
FST, 全称Finite State Transducer, 中⽂翻译: 有限状态转换器或有限状态传感器。
FST最重要的功能是可以实现Key到Value的映射,相当于HashMap<Key,Value>。FST的内存消耗要⽐HashMap少很多,
但FST的查询速度⽐HashMap要慢。
FST在Lucene中被⼤量使⽤,例如:倒排索引的存储,同义词词典的存储,搜索关键字建议等。
Lucene现在采⽤的数据结构为FST,它的特点就是:
优点:内存占⽤率低,压缩率⼀般在3倍~20倍之间、模糊查询⽀持好、查询快
缺点:结构复杂、输⼊要求有序、更新不易
已知FST要求输⼊有序,所以Lucene会将解析出来的⽂档单词预先排序,然后构建FST,我们假设输⼊为abd,abe,acf,acg,那么整个构建过程如下:
按照26个英文字母的顺序
11. Lucene相关度排序(⾼级)
11.1.什么是相关度排序
Lucene对查询关键字和索引⽂档的相关度进⾏打分,得分⾼的就排在前边。
11.1.1. 如何打分
Lucene是在⽤户进⾏检索时实时根据搜索的关键字计算出来的,分两步:
- 计算出词(Term)的权重
- 根据词的权重值,计算⽂档相关度得分。
11.1.2. 什么是词的权重
明确索引的最⼩单位是⼀个Term(索引词典中的⼀个词),搜索也是要从Term中搜索,再根据Term找到⽂档,Term对⽂档的重要性称为权重,影响Term权重有两个因素:
Term Frequency (tf):
指此Term在此⽂档中出现了多少次。tf 越⼤说明越重要。 词(Term)在⽂档中出现的次数越多,说明此词(Term)对该⽂档越重要,如“Lucene”这个词,在⽂档中出现的次数很多,说明该⽂档主要就是讲Lucene技术的。
Document Frequency (df):
指有多少⽂档包含次Term。df 越⼤说明越不重要。⽐如,在⼀篇英语⽂档中,this出现的次数更多,就说明越重要吗?不是的,有越多的⽂档包含此词(Term), 说明此词(Term)太普通,不⾜以区分这些⽂档,因⽽重要性越低。
11.1.3. 怎样影响相关度排序
boost是⼀个加权值(默认加权值为1.0f),它可以影响权重的计算。
在索引时对某个⽂档中的field设置加权值⾼,在搜索时匹配到这个⽂档就可能排在前边。
在搜索时对某个域进⾏加权,在进⾏组合域查询时,匹配到加权值⾼的域最后计算的相关度得分就⾼。
设置boost是给域(field)或者Document设置的。
11.2.⼈为影响相关度排序
查询的时候, 通过设置查询域的权重, 可以⼈为影响查询结果
@Testpublic void testSearch() throws Exception {//1.创建分词器Analyzer analyzer = new IKAnalyzer();//2.设置从多个域中进行查询String[] fields = {"name","brandName","categoryName"};//3.设置权重--影响相关度排序的参数Map<String, Float> boots = new HashMap<>();boots.put("categoryName", 10000000000000f);//4.创建多个域查询对象MultiFieldQueryParser queryParser = new MultiFieldQueryParser(fields, analyzer, boots);//5.设置查询的关键词Query query = queryParser.parse("手机");// 2. 创建Directory流对象,声明索引库位置Directory directory = MMapDirectory.open(Paths.get("D:\\luceneDir"));// 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.value);// 获取查询结果集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();}
对应视频:https://www.bilibili.com/video/BV1Na411h7kk?p=36&spm_id_from=pageDriver
Lucene--千锋修改+踩坑版本相关推荐
- jmeter 3版本到5版本踩坑之路
jmeter 3-5版本升级踩坑路 新版本下载安装 踩坑之路 新版本下载安装 下载新版本软件 ,链接: https://jmeter.apache.org/download_jmeter.cgi: 配 ...
- windows系统安装docker版本davinci达芬奇踩坑指南
本文讲解windows系统如何安装docker版开源软件davinci(数据可视化web服务),并指出一系列的踩坑指南. 达芬奇github地址 https://github.com/edp963 ...
- 口罩、安全帽识别比赛踩坑记(二) 比赛流程及 SSD / YOLO V3 两版本实现
本篇文章主要对比赛流程中的各个环节进行展开说明,并对笔者践行过的代码及更改的地方进行记录.如哪里有侵权请联系笔者进行删除.另外在这里对比赛举办方表示感谢 ~ ~ 其中开源代码会在整理后放在github ...
- bootstrap-editable版本踩坑记
问题 最近想做一个可以编辑的表格,因为前端用的是bootstrap-table,那就索性用bootstrap-table-editable,谁知这是踩坑的开始,Bootstrap用的最新版4,而edi ...
- vue项目中更换tinymce版本踩坑
项目需求: vue项目中实现多图片批量上传功能 问题: tinymce富文本编辑器的多图片批量上传插件 支持版本:5.0.4+ 项目中现有的富文本编辑器版本:4.9.4 为实现这一功能选择更换tiny ...
- Vue 插槽(slot)详细介绍(对比版本变化,避免踩坑)
目录 前言 正文 插槽是什么? 怎么使用插槽? 基本用法 后备(默认)内容 具名插槽 作用域插槽 插槽版本变化 总结 前言 Vue中的插槽(slot)在项目中用的也是比较多的,今天就来介绍一下插槽的基 ...
- 【问题解决】Android JDK版本不匹配导致崩溃踩坑记录
[问题解决]Android JDK版本不匹配导致崩溃踩坑记录 部分机型反馈崩溃问题 谷歌回复与解决方案 Android打包脱糖操作 对比与排查 总结 前几天同事遇到一个非常诡异的报错,紧急处理后,趁着 ...
- Angular教程英雄之旅版本踩坑记录
Angular教程英雄之旅版本踩坑记录 前言 问题1 问题2 问题3 问题4 总结 前言 这两天心血来潮,跟着Angular官网的教程英雄之旅(https://angular.cn/tutorial) ...
- [踩坑合集] 双linux系统卸载/安装,nvidia驱动安装,引导项缺失,开机黑屏,nvidia-smi和nvcc -v CUDA版本不匹配
"个人文化属性" 双系统 1. 双linux系统格式化一个linux系统 2. 安装新的20系统(双系统) 3. 重启后直接进入grub界面(修复它) 3.1 寻找你的系统分区 3 ...
- 「后端小伙伴来学前端了」Element修改默认样式 | 记录自己学习前端踩坑日记
一.Element修改下拉框角标 就比如我最近遇上的一个问题,想要重定义 element 组件库中的下拉选择框的角标,一直不知道怎么覆盖才好. 最后才知道是由伪元素做的. 如果我们想要重定义eleme ...
最新文章
- Matplotlib: “Unknown projection '3d'” error
- request对象学习
- oracle sql比较时间大小,Oracle数据库中日期时间的操作比较和加减
- HDU4666 Hyperspace(数学推理+数据结构)
- 数字图像-6空域滤波
- java ssh优势_java框架组合ssm和ssh各自的优势是什么?
- Python运维插件——psutil
- SVN安装后创建仓库、用户、上传代码
- 使用Python基于BibTeX引用格式自动生成文献的IEEE引用格式
- 2015-2022年历年真题考研数学二难度概述
- 解析《啊哈C》--最终章:用C语言制作走迷宫和推箱子的小游戏
- 先进先出队列,优先级队列以及后进先出队列
- [CyanogenMOD移植教程] 前言:什么是CyanogenMod
- 微信公众平台开发--入门了解
- java-Scaner和Console类对象
- java 使用 POI 操作 XWPFDocumen 创建和读取 Office Word 文档基础篇
- 关于《算法(第四版 谢路云译)》标准库In、Out、StdOut和StdIn的正确配置和调用经验分享(以BinarySearch二分查找算法为例)
- 软件工程知识点总结——第三、四部分
- opengl 旋转中心理解
- 全球及中国润滑油市场调查及投资发展规模预测报告2022-2028年版
热门文章
- Win7定时任务——提醒自己喝水
- mac 外接显示器 发热严重 解决方案
- 韩顺平循序渐进学java百度云_韩顺平.循序渐进学java从入门到精通视频教程及笔记与源码下载(94讲)...
- bzoj4008亚瑟王
- Vmware安装MacOS提示请选择您要安装的macOS的磁盘
- Windows安全更新出现0x800f081f错误解决方案
- HTML层级设置,HTML----元素层级
- 我曾经爱过你---I Loved You
- 计算机网络 sci期刊,计算机方向的sci期刊有哪些
- dd命令详细(好文收藏)