目录
Elasticsearch是一款非常强大的搜索引擎,可以帮助我们从海量数据中快速找到需要的内容,elasticsearch结合kibana、Logstash、Beats,也就是elastic stack(ELK)被广泛应用在日志数据分析、实时监控等领域。,elasticsearch主要负责存储、搜索、分析数据。

想必大家都用过mySQL的模糊查询,like语句,但是SQL的查询时通过建立正向索引的方式,如果通过id查询效率还是很高的,但是如果查询某一文本数据在数据量很大时,效率就会非常的低下。而ES在文本查询使用了倒排索引,进而提高了查询效率。
下面我们将以一个简单的查询案例来介绍正向和倒排索引:
我们将查询title中的字段,来模拟搜索数据。

当我们输入关键字手机时,正向索引会逐行对title字段进行查询,如果包含手机字段就将数据存入结果集,不存在就丢弃进行下一行的查询。
显然这就是在进行逐行的字符串匹配,所以针对海量数据时模糊搜索的效率极低,完全无法应对大数据的搜索。

Elasticsearch首先会建立一个索引库,将数据切割成不同的词条,每个词条都对应着它所在数据的id。 注:词条不能重复,必须唯一。
例:我们搜索“华为手机”,Elasticsearch先将其切割成两个词条,然后去与索引库的词条表进行比对,保存符合条件的词条对应的id,最后按id查询所有符合条件的数据返回。

elasticsearch是面向文档存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为json格式后存储在elasticsearch中。
相当于MySQL中的一个行,每一个字段(id,title相当于一个列)
例如刚刚我们用到数据表,在数据库中是这样存的:

Elasticsearch会将相同类型的文档进行归类,索引就是相同类型文档的集合。
相当于MySQL中的一个表
下图本是一推杂乱无章的数据,进行归档之后就会将相同的合并:

归档之后:


二者对比:

我们知道ES搜索快速的原因就是因为建立了索引表,利用词条进行搜索,所以核心就在于如何将词条进行切割,ES内置的算法对英文的切割效果极佳,但是对于中文的切割效果不太理想,所以这里采用了ik分词器来弥补这一缺陷。官网。
ik分词器包含两种模式:
但ik也有一定的缺陷,因为新词频出,ik对目前我们常见的词汇分割效果十分明显,但是对新颖的组合词常见词就无法切割,但是好在我们可以自己配置。
1、进入ik的目录找到一个叫IkAnalyzer.cfg.xml的文件,进去进行编辑:

2、创建刚刚写的两个文件名:

3、之后我们在里面进行编辑就可以随着我们的需求进行词条的添加和删除了。
例:
未将奥力给添加到新增词汇,会将其切分为三个字

将其添加进去之后:

将其添加到禁止列表中:将不再将“奥里给“添加到词条。

上述操作页面在安装好kibana之后就可以通过 http://虚拟机IP地址:5601 访问
mapping是对索引库中文档的约束,常见的属性值有:
1、type:字段数据类型,常见的简单类型有:
字符串:text(可分词的文本)、keyword(id用此类型保存。精确值,例:品牌、国家、ip)
数值:long、integer、short、byte、double、float、
布尔:boolean
日期:date
对象:object
2、index:是否创建索引,默认为true
3、analyzer:使用哪种分词器
4、properties:该字段的子字段
索引库相当于创建MySQL中的一个表:
1、只有text类型才是要进行分词的字段。
2、index表示该字段是否要参加搜索,默认是true
3、properties定义子字段类型,类似于一个对象内部的属性。
4、索引库名必须小写。
- 语法案例:
- PUT /索引库名称
- {
- "mappings": {
- "properties": {
- "字段名":{
- "type": "text",
- "analyzer": "ik_smart"
- },
- "字段名2":{
- "type": "keyword",
- "index": "false"
- },
- "字段名3":{
- "properties": {
- "子字段": {
- "type": "keyword"
- }
- }
- },
- }
- }
- }
-
- 真实语句:
- PUT /mine
- {
- "mappings": {
- "properties": {
- "info":{
- "type": "text",
- "analyzer": "ik_smart"
- },
- "email":{
- "type": "keyword",
- "index": "false"
- },
- "name":{
- "properties": {
- "firstName": {
- "type": "keyword"
- },
- "lastName": {
- "type": "keyword"
- }
- }
- }
- }
- }
- }
创建成功提示:




索引库相当于数据库的表,但是数据库的表是允许进行字段的删除,修改名称操作的,索引库只支持添加新字段。
- 语法:
- PUT /索引库名/_mapping
- {
- "properties": {
- "新字段名":{
- "type": "integer"
- }
- }
- }
-
- 演示:
- PUT /mine/_mapping
- {
- "properties":{
- "age":{
- "type":"integer"
- }
- }
- }
通过get语句查询:

文档相当于MySQL中的一行,是一条一条的信息,只不过在ES中是以JSON的形式保存的。每一个文档都要有一个唯一的id
属性名和值的类型必须与索引库的结构保持一致。
- POST /mine/_doc/1
- {
- "age":12,
- "email":"123@qq.com",
- "info":"我学了ES啦",
- "name":{
- "firstName":"张",
- "lastName":"三"
- }
- }

上例若未写info,则产生的文档中无此属性:




当文档id存在时,会覆盖之前的内容,不存在时会新增一个文档


当我们向ES中插入文档时,如果文档中字段没有对应的mapping,ES会帮助我们字段设置mapping ,规则如下:

ES会自动将数据类型转化为它内置的类型。
RestClient是ES官方提供的针对各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过http请求发送给ES。 官网
1、引入es的RestHighLevelClient依赖:
- <dependency>
- <groupId>org.elasticsearch.clientgroupId>
- <artifactId>elasticsearch-rest-high-level-clientartifactId>
- dependency>
2、修改的版本号,因为SpringBoot默认的ES版本与服务器上的ES版本不同,所以我们需要覆盖默认的ES版本:
版本号必须与服务器上的版本号保持一致,否则无法操作
- <properties>
- <java.version>1.8java.version>
- <elasticsearch.version>7.12.1elasticsearch.version>
- properties>
3、初始化RestHighLevelClient对象,该对象用于对索引表和索引库的操作:
之后可以将其注入到SpringBoot容器中,直接使用
- //创建对象
- RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
- HttpHost.create("http://服务器地址:9200")
- ));
-
- //使用之后销毁对象
- client.close();
client.indices()包含了对索引库所有的操作
- @SpringBootTest
- class HotelIndexTest {
-
- private RestHighLevelClient client;
-
- //在每次TEST前创建链接
- @BeforeEach
- void setUp() {
- client = new RestHighLevelClient(RestClient.builder(
- HttpHost.create("http://虚拟机地址:9200")
- ));
- }
-
- //每次TEST结束之后关闭链接
- @AfterEach
- void tearDown() throws IOException {
- client.close();
- }
-
- //创建一个索引库
- @Test
- void testCreateIndex() throws IOException {
- // 1.准备Request PUT /hotel <=DSL语法
- CreateIndexRequest request = new CreateIndexRequest("hotel");
- // 2.准备请求参数
- //第一个参数是DLS语句,与之前创建索引表的内容是一致的,由于太长,单独创建了一个文件保存
- //第二个参数是将前面的字符串转化为JSON形式
- request.source(MAPPING_TEMPLATE, XContentType.JSON);
- // 3.发送请求
- client.indices().create(request, RequestOptions.DEFAULT);
- }
-
- //查看索引库是否存在
- @Test
- void testExistsIndex() throws IOException {
- // 1.准备Request
- GetIndexRequest request = new GetIndexRequest("hotel");
- // 2.发送请求
- boolean isExists = client.indices().exists(request, RequestOptions.DEFAULT);
-
- System.out.println(isExists ? "存在" : "不存在");
- }
-
- //删除一个索引库
- @Test
- void testDeleteIndex() throws IOException {
- // 1.准备Request
- DeleteIndexRequest request = new DeleteIndexRequest("hotel");
- // 2.发送请求
- client.indices().delete(request, RequestOptions.DEFAULT);
- }
- }
上面的常量:
- public class HotelIndexConstants {
- public static final String MAPPING_TEMPLATE = "{\n" +
- " \"mappings\": {\n" +
- " \"properties\": {\n" +
- " \"id\": {\n" +
- " \"type\": \"keyword\"\n" +
- " },\n" +
- " \"name\": {\n" +
- " \"type\": \"text\",\n" +
- " \"analyzer\": \"ik_max_word\",\n" +
- " \"copy_to\": \"all\"\n" +
- " },\n" +
- " \"address\": {\n" +
- " \"type\": \"keyword\",\n" +
- " \"index\": false\n" +
- " },\n" +
- " \"price\": {\n" +
- " \"type\": \"integer\"\n" +
- " },\n" +
- " \"score\": {\n" +
- " \"type\": \"integer\"\n" +
- " },\n" +
- " \"brand\": {\n" +
- " \"type\": \"keyword\",\n" +
- " \"copy_to\": \"all\"\n" +
- " },\n" +
- " \"city\": {\n" +
- " \"type\": \"keyword\"\n" +
- " },\n" +
- " \"starName\": {\n" +
- " \"type\": \"keyword\"\n" +
- " },\n" +
- " \"business\": {\n" +
- " \"type\": \"keyword\",\n" +
- " \"copy_to\": \"all\"\n" +
- " },\n" +
- " \"pic\": {\n" +
- " \"type\": \"keyword\",\n" +
- " \"index\": false\n" +
- " },\n" +
- " \"location\": {\n" +
- " \"type\": \"geo_point\"\n" +
- " },\n" +
- " \"all\": {\n" +
- " \"type\": \"text\",\n" +
- " \"analyzer\": \"ik_max_word\"\n" +
- " }\n" +
- " }\n" +
- " }\n" +
- "}";
- }
操作索引表是client.indices()操作文档直接用client对象即可
下面包含了:
用于将MySQL中的数据导入的ES中
- @SpringBootTest
- class HotelDocumentTest {
-
- private RestHighLevelClient client;
-
- @Autowired
- private IHotelService hotelService;
-
- //在每次TEST前创建链接
- @BeforeEach
- void setUp() {
- client = new RestHighLevelClient(RestClient.builder(
- HttpHost.create("http://虚拟机地址:9200")
- ));
- }
- //每次TEST结束之后关闭链接
- @AfterEach
- void tearDown() throws IOException {
- client.close();
- }
-
- //添加一个文档数据
- @Test
- void testAddDocument() throws IOException {
- //一、查询出要添加的数据
- // 1.查询数据库hotel数据
- Hotel hotel = hotelService.getById(61083L);
- // 2.转换为HotelDoc
- HotelDoc hotelDoc = new HotelDoc(hotel);
- // 3.转JSON
- String json = JSON.toJSONString(hotelDoc);
-
- //二、执行添加操作
- // 1.准备Request
- IndexRequest request = new IndexRequest("hotel").id(hotelDoc.getId().toString());
- // 2.准备请求参数DSL,其实就是文档的JSON字符串
- request.source(json, XContentType.JSON);
- // 3.发送请求
- client.index(request, RequestOptions.DEFAULT);
- }
-
- //通过文档的ID来查询数据
- @Test
- void testGetDocumentById() throws IOException {
- // 1.准备Request // GET /hotel/_doc/{id}
- GetRequest request = new GetRequest("hotel", "61083");
- // 2.发送请求
- GetResponse response = client.get(request, RequestOptions.DEFAULT);
- // 3.解析响应结果
- String json = response.getSourceAsString();
-
- HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
- System.out.println("hotelDoc = " + hotelDoc);
- }
- //通过ID删除文档
- @Test
- void testDeleteDocumentById() throws IOException {
- // 1.准备Request // DELETE /hotel/_doc/{id}
- DeleteRequest request = new DeleteRequest("hotel", "61083");
- // 2.发送请求
- client.delete(request, RequestOptions.DEFAULT);
- }
- //通过ID修改数据
- @Test
- void testUpdateById() throws IOException {
- // 1.准备Request
- UpdateRequest request = new UpdateRequest("hotel", "61083");
- // 2.准备参数
- request.doc(
- "price", "870"
- );
- // 3.发送请求
- client.update(request, RequestOptions.DEFAULT);
- }
- //将多个内容同时添加进
- @Test
- void testBulkRequest() throws IOException {
- // 查询所有的酒店数据
- List
list = hotelService.list(); -
- // 1.准备Request
- BulkRequest request = new BulkRequest();
- // 2.准备参数
- for (Hotel hotel : list) {
- // 2.1.转为HotelDoc
- HotelDoc hotelDoc = new HotelDoc(hotel);
- // 2.2.转json
- String json = JSON.toJSONString(hotelDoc);
- // 2.3.添加请求
- request.add(new IndexRequest("hotel").id(hotel.getId().toString()).source(json, XContentType.JSON));
- }
-
- // 3.发送请求
- client.bulk(request, RequestOptions.DEFAULT);
- }
- }
| 查询 所有 | 查询出所有数据,一般测试用 | much_all |
| 全文 检索 | 利用分词器对用户输入内容分词,然后去倒排索引库中匹配。模糊查询 | much mutil_much |
| 精确 查询 | 根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型字段 | term、ids、range |
| 地理 查询 | 根据经纬度查询 | geo_distance geo_bounding_box |
| 复合查询 | 复合查询可以将上述各种查询条件组合起来,合并查询条件 | function_score、bool |

模糊查询,用于对搜索框的查询:
match查询:全文检索查询的一种,会对用户输入内容分词,然后去倒排索引库检索
multi_match:与match查询类似,只不过允许同时查询多个字段,
精确查询一般是查找keyword、数值、日期、boolean等类型字段。所以不会对搜索条件分词。常见的有: 只会查询出与输入的一致的信息
term :根据词条精确值查询range :根据值的范围查询


根据经纬度对数据进行查询
查询geo_point值落在某个矩形范围的所有文档

查询到指定中心点小于某个距离值的所有文档

对应Java部分:
5、复合查询:复合查询:复合查询可以将其它简单查询组合起来,实现更复杂的搜索逻辑
算分函数查询,可以控制文档相关性算分,控制文档排名。例如百度竞价:


布尔查询是一个或多个查询子句的组合:
常见的组合方式:
| must | 必须匹配每个子查询,类似“与” |
| should | 选择性匹配子查询,类似“或” |
| must_not | 必须不匹配,不参与算分,类似“非” |
| filter | 必须匹配,不参与算分 |
案例:搜索名字包含“如家”,价格不高于400,在坐标31.21,121.5周围10km范围内的酒店。

elasticsearch支持对搜索结果排序,默认是根据相关度算分(_score)来排序。可以排序字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等。
案例1:对酒店数据按照用户评价降序排序,评价相同的按照价格升序排序。
会默认按照先后顺序进行排序,先写得分就按得分排,当得分相同时按价格排。

案例2:对酒店数据按照到你的位置坐标的距离升序排序

elasticsearch 默认情况下只返回top10的数据。而如果要查询更多数据就需要修改分页参数进行分页查询。elasticsearch中通过修改from、size参数来控制要返回的分页结果:
ES是分布式的,所以会面临深度分页问题,如果搜索页数过深,或者结果集(from + size)越大,对内存和CPU的消耗也越高。因此ES设定结果集查询的上限是10000
将搜索结果中把搜索关键字突出显示。

语法:


主要分两步:
1、发送查询请求
2、解析数据。解析数据就对照通过DSL语句查询出来的结果进行JSON模式解析即可。
- @Test
- void testMatchAll() throws IOException {
- // 1.准备Request
- SearchRequest request = new SearchRequest("hotel");
- // 2.组织DSL参数
- request.source()
- .query(QueryBuilders.matchAllQuery());
- // 3.发送请求,得到响应结果
- SearchResponse response = client.search(request, RequestOptions.DEFAULT);
-
- // 4.解析结果
- SearchHits searchHits = response.getHits();
- // 4.1.查询的总条数
- long total = searchHits.getTotalHits().value;
- // 4.2.查询的结果数组
- SearchHit[] hits = searchHits.getHits();
- for (SearchHit hit : hits) {
- // 4.3.得到source
- String json = hit.getSourceAsString();
- // 4.4.打印
- System.out.println(json);
- }
- }
通过DSL语句查询出来的信息:

根据DSL语句我们发现查询所有、查询单一字段、查询多个字段只是query部分不同,所以对于Java代码也是只有参数不同即可实现:

只需要修改第二步即可实现对应的功能:
- // 2.组织DSL参数
-
- //单一字段查询
- request.source()
- .query(QueryBuilders.matchQuery("all", "如家"));
-
- //多字段查询
- request.source()
- .query(QueryBuilders.multiMatchQuery("如家", "name", "business"));
与上面也是相同,只需要修改第二步的查询参数即可:

- // 2.组织DSL参数
-
- //词条查询
- request.source()
- .query(QueryBuilders.termQuery("city", "杭州"));
-
- //范围查询
- request.source()
- .query(QueryBuilders.rangeQuery("price").gte(100).lte(150));
同样是修第二步的代码,其余部分不变,相比前几个这里会稍微复杂一些:

- //2、准备DSl
- // 1、创建布尔查询
- BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
- // 2、添加must条件
- boolQuery.must(QueryBuilders.termQuery("city", "杭州"));
- // 3、添加filter条件
- boolQuery.filter(QueryBuilders.rangeQuery("price").lte(250))
- //4、添加到查询中去:
- request.source().query(boolQuery);


request.source()相当于整个JSON对象,通过下图可知 query和 sort、from 、size都是同级,所以像request.source()。query()一样去调用即可


前面我们已经说过,解析数据是得到source部分,但是heighLight和source同级,所以需要特殊处理:

-
-
- @Test
- void testMatchAll() throws IOException {
- // 1.准备Request
- SearchRequest request = new SearchRequest("hotel");
- // 2.组织DSL参数
- request.source()
- .query(QueryBuilders.matchAllQuery());
- // 3.发送请求,得到响应结果
- SearchResponse response = client.search(request, RequestOptions.DEFAULT);
-
- // 4.解析结果
- SearchHits searchHits = response.getHits();
- // 4.1.查询的总条数
- long total = searchHits.getTotalHits().value;
- // 4.2.查询的结果数组
- SearchHit[] hits = searchHits.getHits();
- for (SearchHit hit : hits) {
- // 4.3.得到source
- String json = hit.getSourceAsString();
-
- // 获取source
- HotelDoc hotelDoc = JSON.parseObject(hit.getSourceAsString(), HotelDoc.class);
- // 处理高亮
- Map
highlightFields = hit.getHighlightFields(); - if (!CollectionUtils.isEmpty(highlightFields)) {
- // 获取高亮字段结果
- HighlightField highlightField = highlightFields.get("name");
- if (highlightField != null) {
- // 取出高亮结果数组中的第一个,就是酒店名称
- String name = highlightField.getFragments()[0].string();
- //覆盖高亮的值
- hotelDoc.setName(name);
- }
- }
- }
- }
实现对文档数据的统计、分析、运算。与MySQL中的聚合函数sum、avg相似。
1、桶聚合:用来对文档做分组。
2、度量聚合:用以计算一些值,比如:最大值、最小值、平均值等。
例:统计不同酒店评分的平均值:
1、通过桶聚合将酒店进行分类
2、通过度量聚合对已分类的酒店进行平均值求解
仍然与之前一样,修改第二步的DSL部分:
解析聚合结果:
