教程链接: https://www.bilibili.com/video/BV17a4y1x7zq
序言
最近看了一篇关于全文搜索效率与安全性的paper, 因为对这块不是很熟悉, 一个字一个字的抠出来写了一篇几乎是全文翻译的大纲https://caoyang.blog.csdn.net/article/details/109139934, 结果发现依然不能搞得很明白, 在百度很多专业术语, 如倒排索引(inverted index), 最小桶聚合, 跨度查询等时, 搜索结果无一例外指向了ElasticSearch, 这事情说起来很讽刺, 半个月前学SpringBoot时恰好也学到了SpringBoot整合ElasticSearch(https://caoyang.blog.csdn.net/article/details/108740232), 结果自己的笔记里写了这么一段P话👇
笔者出于加强对论文理解的角度, 决定把ElasticSearch入个门, 有幸观看了狂神说的B站教学视频, 他的CSDN主页是https://blog.csdn.net/qq_33369905/
其实大概之前也看过几个他的教程视频,今天才在他写自己的生日时知道他是97年的,只比我大了一岁,但是他的话里行间表现出的完全不像是只比我大一岁的见识,可以很清晰地从他的话里听出那种桀骜不驯,就和他的网名"狂神"一般,确实是个很有实力,而且履历很丰富的大佬,看他的博客似乎正准备自主创业,这种性格加上这种实力让我艳羡不已,狂神那种人生即便不能最终走向辉煌,也注定是毫无遗憾的精彩。
非常感觉狂神的教学视频, 很清晰易懂, 下文是笔者对其教学视频的摘要, 以备自查, 仅供分享;
PS:实战部分略过(20201021截至Lesson14), 以后真要用到再补, 感觉前面讲得很详细差不多够用了, 而且笔者的目的是以了解为主;
目录
- 序言
- Lesson1 ElasticSearch课程简介
- Lesson2 Lucene创始人 Doug Cutting
- Lesson3 Elastic概述
- Lesson4 Elastic v.s. solr
- Lesson5 ES安装及head插件安装
- Lesson6 kibana安装
- Lesson7 ES核心概念理解
- Lesson8 IK分词器详解
- Lesson9 Rest风格操作
- Lesson10&11 基本操作回顾 & 花式查询详解
- Lesson12 SpringBoot集成ES详解
- Lesson13 关于索引的API操作详解
- Lesson14 关于文档的API操作详解
- Lesson15 京东搜索: 项目搭建
- Lesson16 京东搜索: 爬取数据
- Lesson17 京东搜索: 业务编写
- Lesson18 京东搜索: 前后端交互
- Lesson19 京东搜索: 关键字高亮实现
- Lesson20 狂神聊ES小结
Lesson1 ElasticSearch课程简介
-
本教程基于
ElasticSearch7.6.1
, 注意ES7的语法与ES6的API调用差别很大, 教程发布时最新版本为ES7.6.2(20200401更新); -
ES是用于全文搜索的工具:
- SQL: 使用
like %关键词%
来进行模糊搜索在大数据情况下是非常慢的, 即便设置索引提升也有限; - ElasticSearch: 搜索引擎(baidu, github, taobao)
- 一些ES涉及的概念:
- 分词器 ik
- Restful操作ES
- CRUD
- SpringBoot集成ES
Lesson2 Lucene创始人 Doug Cutting
- Lucene: java写成的为各种中小型应用软件加入全文检索功能;
- Nutch: 一个建立在Lucene核心之上的网页搜索应用程序, Nutch的应用比Lucene要更加广泛
- 大数据解决存储与计算(MapReduce)两个问题:
- 2004年Doug Cutting基于GFS系统开发了分布式文件存储系统;
- 2005年Doug Cutting基于MapReduce在Nutch搜索引擎实现了这种算法;
- 加入Yahoo后, Doug Cutting将MapReduce和NDFS结合创建了Hadoop, 成为了Hadoop之父;
- Doug Cutting将BigTable集成到Hadoop中
- 回到主题:
- Lucene是一套信息检索工具包, jar包, 不包含搜索引擎系统;
- Lucene包含索引结构, 读写索引的工具, 排序, 搜索规则, 工具类;
- Lucene和ES的关系:
- ES是基于Lucene做了一些封装和增强, 上手是比较简单的, 比Redis要简单
Lesson3 Elastic概述
- 分布式的全文搜索引擎, 高扩展性;
- 接近实时更新的查询搜索;
- ES是基于Restful的(即用get, post, delete, put来访问);
- ES进行复杂的数据分析, ELK技术(elastic+logstash+kibana)
Lesson4 Elastic v.s. solr
- 当使用索引时, solr会发生io阻塞, 查询性较差, elastic则在索引情况下的优势明显;
- elastic的效率在传统项目下一般有50倍的提升;
- elastic解压即可用, solr需要配置
- solr用zookeeper进行分布式管理, elastic自带分布式
- solr支持更多格式的数据, json, xml, csv, elastic只支持json
- solr比elastic的功能更强大
- solr查询快, 但是更新索引时慢(如插入和删除慢), elastic查询慢, 但是实时性查询快, 用于facebook新浪等搜索
- solr是传统搜索应用的解决方案, elastic适用于新兴的实时搜索应用
- solr比较成熟, elastic目前更新换代快;
Lesson5 ES安装及head插件安装
-
要求jdk1.8以上, 这是最低要求;
-
elastic客户端, 界面工具;
-
springboot添加依赖时默认没有到ES7, 需要自己修改
-
可以直接在windows或linux上学习, 而无需使用虚拟机或者其他配置;
-
elastic 7.x.x的解压目录:
- bin: 启动文件
- config: 配置文件
- log4j2: 日志配置
- jvm.options: java虚拟机相关配置
- elasticsearch.yml: ES配置文件, 默认端口是9200
- lib: 相关jar包
- logs: 日志
- modules: 功能模块
- plugins: 插件(如ik)
- 启动, 访问9200端口
- 双击: bin/elasticsearch.bat
- 访问: localhost:9200
- 可以看到如下json字符串
{ "name" : "CAOYANG", "cluster_name" : "elasticsearch", "cluster_uuid" : "xEDZ4q2JSxq54mJM2UiTxQ", "version" : { "number" : "7.9.2", "build_flavor" : "default", "build_type" : "zip", "build_hash" : "d34da0ea4a966c4e49417f2da2f244e3e97b4e6e", "build_date" : "2020-09-23T00:45:33.626720Z", "build_snapshot" : false, "lucene_version" : "8.6.2", "minimum_wire_compatibility_version" : "6.8.0", "minimum_index_compatibility_version" : "6.0.0-beta1" }, "tagline" : "You Know, for Search" }
- 可以看到如下json字符串
- 安装可视化插件 ES head:
- 下载地址: https://github.com/mobz/elasticsearch-head
- 安装步骤:
git clone git://github.com/mobz/elasticsearch-head.git cd elasticsearch-head npm install npm run start open http://localhost:9100/
- 连接测试: 访问
http://localhost:9100/
- 会发生跨域问题, 需要在ES解压目录下的bin/elasticsearch.yml中配置跨域信息, 在配置文件末尾添加:
http.cors.enable: true
http.cors.allow-origin: "*"
- 会发生跨域问题, 需要在ES解压目录下的bin/elasticsearch.yml中配置跨域信息, 在配置文件末尾添加:
- 重新连接测试: 访问
http://localhost:9100/
- 出现可视化界面, 一些健康状况为显示在页面上
- 初学时, 可以把ES当作一个数据库, 可以建立索引(库), 文档(库中的数据)
- 这个head插件可以当作一个数据展示工具, 后面所有的查询在kibana上运行
Lesson6 kibana安装
- kibana是一个针对ES的开源分析及可视化界面, 用来搜索查看和交互存储在ES索引中的数据
- 下载地址: https://www.elastic.co/cn/kibana
- 下载完毕后, 解压需要一段时间, 是一个标准的工程
- 好处: ELK基本上都是拆箱即用;
- 启动bin/kibana.bat, 然后访问localhost:5601即可
- 在页面上可以直接在console里写查询语句执行, 查询结果直接显示在页面上
- 汉化: 直接在kibana.yml末尾添加:
i18n.locale: "zh-CN"
, 即SpringBoot中的国际化
Lesson7 ES核心概念理解
- elasticsearch是面向文档的, 索引和搜索数据的最小单位是文档, 与关系型数据库比较:
- 数据库(database) v.s. 索引(indices, 理解为数据库)
- 表(tables) v.s. 类型(types, 7.x弃用, 8.x会完全丢掉)
- 行(rows) v.s. 文档(documents)
- 字段(columns) v.s. 字段(fields)
- ES所有数据都是json
- ES在后台把索引划分成多个切片, 每份分片可以在集群中不同服务器间迁移
- ES一个人就是一个集群, 默认的集群名称就是elasticsearch
- ES文档(可以理解为json或是yml中的数据格式)的重要属性:
- 自我包含, 一篇文档同时包含字段和对应的值, 也就是同时包含keyvalue
- 可以是层次型的, 一个文档中包含子文档, 复杂的逻辑实体, 就是一个json对象
- 灵活的结构, 文档不依赖预先定义的模式, 我们知道关系型数据库中, 要提前定义字段才能使用, elasticsearch中, 对于字段是灵活的, 有时可以忽略该字段, 或是动态添加一个新的字段
- ES索引: 就是数据库
- ES索引是一个非常大的文档集合, 索引存储了映射类型的字段和其他设置, 然后存储到各个分片上, 一般是存在集群的不同节点上, 一旦某个节点挂了, 数据不会丢失;
- 分片是一个Lucene索引, 一个包含==倒排索引(inverted index)==的文件目录, 倒排索引的结构使得elasticsearch在不扫描全部文档的情况下, 就可以告诉你哪些文档包含特定的关键字;
- 通过各个关键字的权重(可以理解为score)之和来对查询结果进行排序
- 使用倒排索引可以过滤掉完全无关的数据
- 总结:
- ES中, 索引(库)这个词被频繁使用, 就是数据库, 索引被分为多个分片, 每份分片是一个Lucene的索引, 所以一个ES索引是由多个Lucene索引组成, Lucene索引是一种倒排索引;
Lesson8 IK分词器详解
- 分词: 类似python的jieba和nltk
- IK分词器下载地址: https://github.com/medcl/elasticsearch-analysis-ik-releases
- 注意与ES版本对应
- 下载完毕后直接解压即可, 然后移动到elasticsearch7.x.x里的plugins/的ik目录下
- IK项目的README中写道:
拷贝和解压release下的文件: #{project_path}/elasticsearch-analysis-ik/target/releases/elasticsearch-analysis-ik-*.zip 到你的 elasticsearch 插件目录, 如: plugins/ik
- 注意别下载错了, 解压或里面应该是一个config文件夹, 1个properties文件, 1个policy文件, 还有5个jar包
- 配置好后重启ES, 可以看到ik分词器被加载了;
- 可以通过elasticsearch-plugin来查看加载的插件(把ES目录的bin文件夹添加到Path里)
- 开始使用IK, 在kibana的界面控制台里写:
- 查看的不同的分词效果:
GET _analyze
{
"analyzer": "ik_smart",
"text": "中国人民军队"
}
GET _analyze
{
"analyzer": "ik_max_word",
"text": "中国人民军队"
}
+ ```ik_smart```为最少切分, 返回结果只有包含中国人民军队的结果, 就是尽可能少的切分;
+ ```ik_max_word```为最细粒度划分, 穷尽词库的可能, 如会划分中国, 人民, 军队, 及这三个分词;
+ 如果搜索```超级喜欢狂神说```, 会发现即使用最少切分, 狂神说三个字都被分开了;
+ 所以需要配置用户字典: 配置文件路径在ik/config/IKAnalyzer.cfg.xml
* 可以新建一个kuang.dic文件, 然后在IKAnalyzer.cfg.xml中添加配置
- ```<entry key="ext_dict">kuang.dic</entry>```
- ```<entry key="ext_stopwords">kuang_stop.dic</entry>```
Lesson9 Rest风格操作
- PUT: 创建文档(指定文档id), localhost:9200/索引名称/类型名称/文档ID
- 示例:
PUT /test1/type1/1 { "name": "狂神说", "age": 3 }
- 命令执行返回结果: 完成了自动增加索引, 数据也成功添加了
{ _index: test1, _type: type1, _id: 1, _score: 1, name: "狂神说", age: 3 }
- 命令执行返回结果: 完成了自动增加索引, 数据也成功添加了
- 数据类型:
- 字符串类型: text和keyword
- 数值类型: long, integer, short, byte, double, float, half float, scaled float
- 日期类型: date
- te布尔值类型: boolean
- 二进制类型: binary
- 指定字段类型: 添加一个库并添加字段规则(类似mysql建表)
PUT /test2 { "mappings": { "properties": { "name": { "type": "text" }, "age": { "type": "long" }, "birthday": { "type": "date" } } } }
- 返回结果发现acknowledged是true, 说明规则创建成功
- POST: 创建文档(随机文档id), localhost:9200/索引名称/类型名称
- POST: 修改文档, localhost:9200/索引名称/文档id/_update
- 示例:
POST /test3/_doc/1/_update { "doc": { "name": "法外狂徒张三" } }
- 则索引test3中的_doc类型里的name值被更新
- 注意如果不创建文档则默认是_doc
- DELETE: 删除文档, localhost:9200/索引名称/类型名称/文档id
DELETE test1
: 删除索引- 通过请求来判断是删除索引还是删除文档记录
- GET: 查询文档(通过文档id), localhost:9200/索引名称/类型名称/文档id
- 获得索引表的规则:
GET test2
GET 表
就是拿到表的信息,GET 索引
就是拿到索引的信息
GET _cat/health
: 查看健康信息GET _cat/indices?v
: 索引状态- ge _cat可以获得es的当前的很多信息
- POST: 查询所有数据, localhost:9200/索引名称/类型名称/_search
Lesson10&11 基本操作回顾 & 花式查询详解
- 关于文档的基本操作(重点!)
-
基本操作
PUT /kuangshen/user/1 { "name": "xxx", "age": 12, "desc": "xxx", "tags": {"a","b","c"} }
- kuangshen就是_index, user就是_type, 1就是_id
- 简单搜索
GET kuangshen/user/1
: 取出kuangshen库中的user表里的id为1的文档结果;- 有个_version字段, 表明被更新了几次
- 可以添加条件来搜索:
GET kuangshen/user/_search?q=name:狂神说Java
, 注意这个是精确搜索, 少一些(比如搜索"狂神说")就找不到"狂神说Java" - 之前提到字符串有keyword和text的区别, keyword是不会被分词处理, text会被分词处理
POST kuangshen/user/1/_update {"doc":{"name": "xxxx"}}
: 更新数据, 注意更新的数据放在doc键下, 是一个字典格式的, 一次可以同时更新多个字段
-
复杂操作: select(排序, 分页, 高亮, 模糊查询, 精准查询)
-
hits字段下是所有查询结果, 如果存在多条查询出来的结果, 则每个结果有_score值返回, 指匹配度, 会降序列出;
-
例1: 查询参数体(一个json对象), 把name字段包含"狂神"的结果都搜索出来
GET kuangshen/user/_search { "query": { "match": { "name": "狂神" } }, "_source": ["name","desc"], // 结果过滤, 只返回name和desc字段 "sort": [ //排序 { "age": { order: "asc" } } ], // 分页参数: 总第from个数据开始, 返回多少条数据(当前页面) "from": 0, "size": 2 }
-
例2: 多条件查询, 通过bool值字段
- bool下的must命令, 下面的所有条件都要符合(and)
- bool下的should命令, 下面的所有条件都要符合(or)
- bool下的must_not命令, 下面的所有条件都不能符合
- bool下的filter字段, 过滤条件, 包括range下的lte, lt, gt, gte字段为大于小于等等
- match字段是包含这个字符串的结果都会被搜索出来
GET kuangshen/user/_search { "query": { "bool": { "must": [ { "match": { "name": "狂神说" } }, { "match": { "age": 23 } } ], "should": [ { "match": 13 }, { "match": { "age": 12 } } ], "filter": { // 过滤条件 "range": { "age": { "gte": 10, "lt": 25 } } } } } }
-
例3: 匹配多个条件
GET kuangshen/user/_search { "query": { "match": { "tags": "a b" // 会把tag字段包含(指match)"a"和"b"的都拿出来 } } }
- 多个条件用空格隔开, 只要满足一个结果就被查出, 会有得分结果返回, 得分越高匹配度越高
-
精确查询!
- term查询就是直接通过倒排索引指定的词条进程精确查找的
-
关于分词:
- term, 直接查询精确的, 把上面例子中的match换成term就是精确而非包含的查询
- match, 会使用分词器解析(先分析文档, 然后再通过分析的文档进行查询)
- text字符串会被分词解析, keyword则不会被分词解析
-
多个值匹配的精确查询: bool.should + term
-
高亮:
GET kuangshen/user/_search { "query": { "match": { "tags": "a b" // 会把tag字段包含(指match)"a"和"b"的都拿出来 } }, "highlight": { // 搜索结果高亮name "pre_tags": "<p class='key' style='color:red'>", "post_tags": "</p>", // 自定义高亮的tag "fields": { "name": {} } } }
-
Lesson12 SpringBoot集成ES详解
- 找到 Java Rest Client 里的高级客户端
- ① 引入原生依赖: sts4直接选nosql里的elasticsearch就行了, 但是有可能默认是ES6, 要改成ES7的依赖才行;
<dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-level-client</artifactId> <version>7.6.2</version> </dependency>
- ② 初始化
- 创建: 可以参数里是多个new(表示多个集群, 一般本地测试一个就行了)
RestHighLevelClient client = new RestHighLevelClient( RestClient.builder( new HttpHost("localhost",9200,"http"), new HttpHost("localhost",9201,"http")));
- 关闭:
client.close();
- ③ ES的配置文件:
@Configuration public class ElasticSearchConfig { @Bean public RestHighLevelClient restHighLevelClient() { RestHighLevelClient client = new RestHighLevelClient( RestClient.builder( new HttpHost("127.0.0.1",9200,"http"), )); return client; } }
- ④ 测试文件:
@SpringBootTest class KuangshenEsApiApplicationTests { @Autowired private RestHighLevelClient restHighLevelClient; @Test void contextLoads() { } }
Lesson13 关于索引的API操作详解
- 测试文件(续)
- 例1: 直接运行西面的测试代码, ES中创建的一个新的空索引kuang_index, 然后判断存在. 最后删除
@SpringBootTest class KuangshenEsApiApplicationTests { @Autowired @Qualifier("restHighLevelClient") private RestHighLevelClient client; @Test void testCreateIndex() { // 测试索引创建 // 1. 创建索引请求 CreateIndexRequest request = new CreateIndexRequest("kuang_index"); // 2. 客户端执行请求 CreateIndexResponse createIndexResponse = client.indices().create(request,RequestOptions.DEFAULT); System.out.println(createIndexResponse); } @Test void testExistIndex() { // 测试获取索引 GetIndexRequest request = new GetIndexRequest("kuang_index"); boolean exists = client.indices.exist(request,RequestOptions.DEFAULT); System.out.println(exists); } @Test void testDeleteIndex() { // 测试删除索引 DeleteIndexRequest request = new DeleteIndexRequest("kuang_index"); AcknowledgedResponse delete = client.indices.delete(request,RequestOptions.DEFAULT); System.out.println(delete.isAcknowledged()); } }
Lesson14 关于文档的API操作详解
-
先创一个用户BEAN类, name和age两个字段
-
继续编写测试文件(汗)
-
例1: 测试添加文档
@SpringBootTest class KuangshenEsApiApplicationTests { @Autowired @Qualifier("restHighLevelClient") private RestHighLevelClient client; // 测试添加文档 @Test void testAddDocument() { // 创建对象 User user = new User("狂神说",3); // 创建请求 IndexRequest request = new IndexRequest("kuang_index"); // 规则 put /kuang_index/_doc/1 request.id("1"); request.timeout(TimeValue.timeValueSeconds(1)); request.timeout("1s"); // 将我们的数据放入请求 json (核心本质!!!) request.source(JSON.toJSONString(user),XContentType.JSON); // 客户端发送请求, 获取响应的结果 IndexResponse indexResponse = client.index(request,RequestOptions.DEFAULT); System.out.pringln(indexResponse.toString()); System.out.pringln(indexResponse.status()); // 对应命令返回的状态 CREATED } }
-
例2: 测试获取文档, 判断是否存在
@Test void testExistsDocument() throws IOException { GetRequest getRequest = new GetRequest("kuang_index","1"); // 不获取返回的_source的上下文: 不必要 getRequest.fetchSourceContext(new FetchSourceContext(false)); getRequest.storedFields("_none_"); boolean exists = client.exists(getRequest,RequestOptions.DEFAULT); System.out.pringln(exists); }
-
例3: 测试获取文档信息
@Test void testGetDocument() throws IOException { GetRequest getRequest = new GetRequest("kuang_index","1"); GetResponse getResponse = client.get("getRequest",RequestOptions.DEFAULT); System.out.pringln(getResponse.getSourceAsString()); System.out.pringln(getResponse); // 返回的全部内容与命令式一样 }
-
例4: 测试更新文档信息
@Test void testUpdateDocument() throws IOException { UpdateRequest updateRequest = new UpdateRequest("kuang_index","1"); updateRequest.timeout("1s"); User user = new User("狂神说Java",18) updateRequest.doc(JSON.toJSONString(user),XContentType.JSON); UpdateResponse update = client.update(updateRequest,RequestOptions.DEFAULT); System.out.pringln(updateRequest.status()); }
-
例5: 测试删除文档信息
@Test void testDeleteDocument() throws IOException { DeleteRequest request = new DeleteRequest("kuang_index",3); deleteRequest.timeout("1s"); DeleteResponse deleteResponse = client.delete(request,RequestOptions.DEFAULT); System.out.pringln(deleteResponse.status()); }
-
例6: 特殊的, 真的项目一般会批量插入数据
@Test void testBulkRequest() throws IOException { BulkRequest bulkRequest = new BulkRequest(); bulkRequest.timeout("10s"); ArrayList<User> userList = new ArrayList<>(); userList.add(new User("kuangshen1",3)); userList.add(new User("kuangshen2",3)); userList.add(new User("kuangshen3",3)); userList.add(new User("kuangshen4",3)); userList.add(new User("kuangshen5",3)); userList.add(new User("kuangshen6",3)); // 批处理请求 for (int i = 0; i<userList.size(); i++) { bulkRequest.add( new IndexRequest("kuang_index").id(""+(i+1)).source(JSON.toJSONString(userList.get(i),XContentType.JSON)); ); } BulkResponse bulkResponse = client.bulk(bulkRequest,RequestOptions.DEFAULT); System.out.pringln(bulkResponse.hasFailures()); // 是否失败, 返回false表示成功 }
-
例7: 查询
- SearchRequest 搜索请求
- SearchSourceBuilder 条件构造
- HighlightBuilder 构造高亮
- TermQueryBuilder 构造精确查询
- MatchQueryBuilder
- xxxQueryBuilder 对应上面非SpringBoot部分看到的那些控制台的命令
@Test void testSearch() throws IOException { SearchRequest searchRequest = new SearchRequest(kuang_index); // 构建搜索条件 SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); //sourceBuilder.highligher(); 高亮 // 查询条件, 可以用QueryBuilders工具实现 // QueryBuilders.termQuery 精确 // QueryBuilders.matchAllQuery() 匹配所有 TermQueryBuilder termQueryBuilder = QueryBuilder.termQuery("name","kuangshen1") // MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery(); sourceBuilder.query(termQueryBuilder); sourceBuilder.timeout(new TimeValue(60,TimeUnit.SECONDS)); SearchResponse searchResponse = searchRequest.source(sourceBuilder,RequestOptions.DEFAULT); System.out.pringln(JSON.toJSONString(searchRequest.getHits())); // 记得那个hit键了吗? for (SearchHit documentFields : (searchRequest.getHits())) { System.out.pringln(documentFields.getSourceAsMap()); } }
Lesson15 京东搜索: 项目搭建
NOTIMPLEMENT ERROR
Lesson16 京东搜索: 爬取数据
NOTIMPLEMENT ERROR
Lesson17 京东搜索: 业务编写
NOTIMPLEMENT ERROR
Lesson18 京东搜索: 前后端交互
NOTIMPLEMENT ERROR
Lesson19 京东搜索: 关键字高亮实现
NOTIMPLEMENT ERROR
Lesson20 狂神聊ES小结
NOTIMPLEMENT ERROR
END