SpringBoot-ElasticSearch(ElasticSearch 倒排索引 es的一些概念 环境安装 索引库操作 RestAPI ElasticSearch查询 案例(含页面))

目录

一、ElasticSearch

二、倒排索引 

1. 正向索引

2. 倒排索引

3. 正向和倒排

三、es的一些概念

1. 文档和字段

2. 索引和映射

3. mysql与elasticsearch 

四、环境安装

1. IK分词器

2. 安装

 五、索引库操作

1. mapping映射属性

2. 索引库的CRUD 

2.1 创建索引库和映射 

2.2 查询索引库

2.3 查询所有索引 

2.4 修改索引库

2.5 删除索引库

 总结

3. 文档操作

3.1 新增文档

3.2 查询文档

3.3 删除文档

3.4 修改文档

全量修改

增量修改

总结

六、RestAPI

数据结构如下(数据库字段)

mapping映射分析

整体结构图

1. 初始化RestClient

2. 创建索引库

3. 配置yml

4. 测试-索引库操作

总结

RestClient操作文档

pojo

三层

测试类

小结

七、ElasticSearch查询 

1. DSL查询文档

2. RestClient查询文档  

八、案例(含页面)


一、ElasticSearch

         Elasticsearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口 Elasticsearch 是用 Java 语言开发的,并作为 Apache 许可条款下的开放源码发布,是一种流行的企业级搜索引擎。Elasticsearch 用于 云计算 中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。官方客户端在Java .NET C# )、 PHP Python Apache Groovy Ruby 和许多其他语言中都是可用的。根据DB-Engines 的排名显示, Elasticsearch 是最受欢迎的企业搜索引擎,其 次是Apache Solr ,也是基于 Lucene
演示:京东,淘宝
Lucene 是一个 Java 语言的搜索引擎类库 , Apache 公司的顶级项目,由 DougCutting 1999 年研发。
官网地址 : https:// lucene.apache.org/
重要特性:
  • 1、分布式的实时文件存储,每个字段都被索引并可被搜索
  • 2、实时分析的分布式搜索引擎
  • 3、可以扩展到上百台服务器,处理PB级结构化或非结构化数据

二、倒排索引 

倒排索引的概念是基于 MySQL 这样的正向索引而言的。

1. 正向索引

那么什么是正向索引呢?例如给下表(tb_goods)中的id创建索引:

如果是根据 id 查询,那么直接走索引,查询速度非常快。
但如果是基于title做模糊查询,只能是逐行扫描数据,流程如下
  • 1)用户搜索数据,条件是title符合 "%手机%"
  • 2)逐行获取数据,比如id1的数据
  • 3)判断数据中的title是否符合用户搜索条件
  • 4)如果符合则放入结果集,不符合则丢弃。回到步骤1
逐行扫描,也就是全表扫描,随着数据量增加,其查询效率也会越来越低。当数据量达到数百万时,就是一场灾难。

2. 倒排索引

倒排索引中有两个非常重要的概念:
文档( Document ):用来搜索的数据, 其中的每一条数据就是一个文档。例如一个网页、一个商品信息
词条( Term ):对文档数据或用户搜索数据 ,利用某种算法分词,得到的具备含义的词语就是词条。例如:我是中国人,就可以分为:我、是、中国人、中国、国人这样的几个词条
创建倒排索引 是对正向索引的一种特殊处理,流程如下:
  1. 将每一个文档的数据利用算法分词,得到一个个词条
  2. 创建表,每行数据包括词条、词条所在文档id、位置等信息
  3. 因为词条唯一性,可以给词条创建索引,例如hash表结构索引
如图:

倒排索引的 搜索流程 如下(以搜索 " 华为手机 " 为例):
  • 1)用户输入条件 "华为手机" 进行搜索。
  • 2)对用户输入内容分词,得到词条: 华为 手机
  • 3)拿着词条在倒排索引中查找,可以得到包含词条的文档id123
  • 4)拿着文档id到正向索引中查找具体文档。
如图:
虽然要先查询倒排索引,再查询倒排索引,但是无论是词条、还是文档 id 都建立了索引,查询速度非常快!无需全表扫描。

3. 正向和倒排

那么为什么一个叫做正向索引,一个叫做倒排索引呢?
  • 正向索引是最传统的,根据id索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档找词条的过程
  • 倒排索引则相反,是先找到用户要搜索的词条,根据词条得到保护词条的文档的id,然后根据id获取文档。是根据词条找文档的过程
正向索引
优点:
  • 可以给多个字段创建索引
  • 根据索引字段搜索、排序速度非常快
缺点:
  • 根据非索引字段,或者索引字段中的部分词条查找时,只能全表扫描。
倒排索引
优点:
  • 根据词条搜索、模糊搜索时,速度非常快
缺点:
  • 只能给词条创建索引,而不是字段
  • 无法根据字段做排序

三、es的一些概念

elasticsearch 中有很多独有的概念,与 mysql 中略有差别,但也有相似之处。

1. 文档和字段

        elasticsearch是面向 文档( Document 存储的,可以是数据库中的一条商品数据,一个订单信息。 文档数据会被序列化为json格式后存储在elasticsearch中: Json 文档中往往包含很多的 字段( Field ,类似于数据库中的列。

2. 索引和映射

索引(Index),就是相同类型的文档的集合。类似于数据库中的表
例如:
  • 所有用户文档,就可以组织在一起,称为用户的索引;
  • 所有商品的文档,可以组织在一起,称为商品的索引;
  • 所有订单的文档,可以组织在一起,称为订单的索引;

因此,我们可以把索引当做是数据库中的表。
        数据库的表会有约束信息,用来定义表的结构、字段的名称、类型等信息。因此, 索引库中就有映射 (mapping),是索引中文档的字段约束信息,类似表的结构约束。

3. mysqlelasticsearch 

我们统一的把 mysql与elasticsearch的概念做一下对比:
MySQLElasticsearch说明
TableIndex索引(index),就是文档的集合,类似数据库的表(table)
RowDocument文档(Document),就是一条条的数据,类似数据库中的行 (Row),文档都是JSON格式
ColumnField字段(Field),就是JSON文档中的字段,类似数据库中的列 (Column)
SchemaMappingMapping(映射)是索引中文档的约束,例如字段类型约束。类
SQLDSLDSL是elasticsearc
是不是说,我们学习了 elasticsearch 就不再需要 mysql 了呢?
并不是如此,两者各自有自己的擅长支出:
  • Mysql:擅长事务类型操作,可以确保数据的安全和一致性
  • Elasticsearch:擅长海量数据的搜索、分析、计算

因此在企业中,往往是两者结合使用:

  • 对安全性要求较高的写操作,使用mysql实现
  • 对查询性能要求较高的搜索需求,使用elasticsearch实现
  • 两者再基于某种方式,实现数据的同步,保证一致性

四、环境安装

Windows ES 下载
https://www.elastic.co/cn/downloads/elasticsearch
Windows ES 安装与启动 运行 elasticsearch.bat
访问localhost:9200能看到json代表启动成功

1. IK分词器

分词器的作用是什么?
  • 创建倒排索引时对文档分词
  • 用户搜索时,对输入的内容分词
IK 分词器有几种模式?
  • ik_smart:智能切分,粗粒度
  • ik_max_word:最细切分,细粒度

2. 安装

下载: https://github.com/medcl/elasticsearch-analysis-ik/releases
  • ES安装目录下找到plugins目录创建ik文件夹
  • ik分词器解压缩在此目录并重启ES即可
解压即安装

 五、索引库操作

索引库就类似数据库表, mapping 映射就类似表的结构。
我们要向 es 中存储数据,必须先创建

1. mapping映射属性

mapping 是对索引库中文档的约束,常见的 mapping 属性包括:
  • type:字段数据类型,常见的简单类型有:
    • 字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)
    • 数值:long、integer、short、byte、double、float、
    • 布尔:boolean
    • 日期:date
    • 对象:object
  • index:是否创建索引,默认为true
  • analyzer:使用哪种分词器
  • properties:该字段的子字段

例如下面的json文档:

{
    "age": 18,
    "weight": 70.2,
    "isMarried": false,
    "info": "java老师",
    "email": "123@163.com",
    "score": [99.1, 99.5, 98.9],
    "name": {
        "firstName": "师傅",
        "lastName": "王"
    }
}

对应的每个字段映射(mapping):
age:类型为 integer;参与搜索,因此需要index为true;无需分词器
weight:类型为float;参与搜索,因此需要index为true;无需分词器
isMarried:类型为boolean;参与搜索,因此需要index为true;无需分词器
info:类型为字符串,需要分词,因此是text;参与搜索,因此需要index为true;分词器可以用
ik_smart
email:类型为字符串,但是不需要分词,因此是keyword;不参与搜索,因此需要index为
false;无需分词器
score:虽然是数组,但是我们只看元素的类型,类型为float;参与搜索,因此需要index为
true;无需分词器
name:类型为object,需要定义多个子属性
name.firstName;类型为字符串,但是不需要分词,因此是keyword;参与搜索,因此需要
index为true;无需分词器
name.lastName;类型为字符串,但是不需要分词,因此是keyword;参与搜索,因此需要
index为true;无需分

2. 索引库的CRUD 

这里我们统一使用 postMan 编写 DSL 的方式来演示。
启动:
在安装目录下 bin-》elasticsearch.bat打开这个文件
访问http://localhost:9200/ 即打开成功

2.1 创建索引库和映射 

基本语法:
  • 请求方式:PUT
  • 请求路径:/索引库名,可以自定义
  • 请求参数:mapping映射
格式:
PUT /索引库名称
{
    "mappings": {
        "properties": {
            "字段名":{
                "type": "text",
                "analyzer": "ik_smart"
            },
            "字段名2":{
                "type": "keyword",
                "index": "false"
            },
            "字段名3":{
                "properties": {
                    "子字段": {
                        "type": "keyword"
                    }
                }
            },
            // ...略
        }
    }
}

示例:

创建索引等同于创建一个数据库 ,请求类型:put,请求地址http://localhost:9200/teacher
teacher表示索引名称

{
	"mappings":{
		"properties":{
			"age":{
				"type":"integer",
				"index":"false"
			},
			"weight":{
				"type":"float",
				"index":"false"
			},
			"isMarried":{
				"type":"boolean",
				"index":"false"
			},
			"info":{
				"type":"text",
				"analyzer":"ik_smart"
			},
			"email":{
				"type":"keyword",
				"index":"false"
			},
			"name":{
				"properties":{
					"firstName":{
						"type":"keyword"
					},
					"lastName":{
						"type":"keyword"
					}
				}
			}
		}
	}
}

2.2 查询索引库

基本语法
  • 请求方式:GET
  • 请求路径:/索引库名
  • 请求参数:无
格式
GET /索引库名

 示例:请求类型:get,请求地址为: http://localhost:9200/teacher

2.3 查询所有索引 

请求类型:get,请求地址:http://localhost:9200/_cat/indices?v

2.4 修改索引库

        倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引, 这简直是灾难。因此索引库一旦创建,无法修改mapping
        虽然无法修改mapping 中已有的字段,但是却允许添加新的字段到 mapping 中,因为不会对倒排索引产生影响。
语法说明

PUT /索引库名/_mapping
{
    "properties": {
        "新字段名":{
            "type": "integer"
        }
    }
}

这里就不修改了

2.5 删除索引库

语法:
  • 请求方式:DELETE
  • 请求路径:/索引库名
  • 请求参数:无

格式:

DELETE /索引库名

 示例:请求类型:delete,请求地址为: http://localhost:9200/teacher

 总结

  • 创建索引库:PUT /索引库名
  • 查询索引库:GET /索引库名
  • 删除索引库:DELETE /索引库名
  • 添加字段:PUT /索引库名/_mapping

3. 文档操作

3.1 新增文档

语法:
POST /索引库名/_doc/文档id
{
    "字段1": "值1",
    "字段2": "值2",
    "字段3": {
        "子属性1": "值3",
        "子属性2": "值4"
    },
// ...
}

示例:

添加数据也就是所谓的在teacher索引中添加字段(就如同在mysql中有张teacher表中添加字段)
请求方式:post,请求地址:http://localhost:9200/teacher/_doc
在请求时一定要添加请求体

{
    "age":18,
    "weight":70.2,
    "isMarried":false,
    "info":"一个javaee老师",
    "email":"123@163.com",
    "name":{
        "firstName":"师傅",
        "lastName":"王"
    }
}
响应:
result:created

3.2 查询文档

         根据rest风格,新增是post,查询应该是get,不过查询一般都需要条件,这里我们把文档id带上。
语法:
GET /{索引库名称}/_doc/{id}
id 根据上面新增的结果获取

3.3 删除文档

删除使用 DELETE 请求,同样,需要根据 id 进行删除:
语法:
DELETE /{索引库名}/_doc/id值
示例:
# 根据id删除数据
http://localhost:9200/teacher/_doc/Nd-f5ZABetgWOWEtpiq7
结果:
result:deleted

3.4 修改文档

修改有两种方式:
  • 全量修改:直接覆盖原来的文档
  • 增量修改:修改文档中的部分字段
全量修改

全量修改是覆盖原来的文档,其本质是:

  • 根据指定的id删除文档
  • 新增一个相同id的文档
注意:如果根据id删除时,id不存在,第二步的新增也会执行,也就从修改变成了新增操作了。
语法:
PUT /{索引库名}/_doc/文档id
{
    "字段1": "值1",
    "字段2": "值2",
    // ... 略
}

这里基本逻辑为:就是把新数据用body传输替换
请求方式:post,请求地址:http://localhost:9200/teacher/_doc/Nt-n5ZABetgWOWEtrSrQ

{
    "info": "java程序员高级Java讲师",
    "email": "zkt@163.com",
    "name": {
        "firstName": "kt",
        "lastName": "zz"
    }
}

“result”: “updated” 更新成功

重新查询

增量修改
增量修改是只修改指定 id 匹配的文档中的部分字段。
语法:
POST /{索引库名}/_update/文档id
{
    "doc": {
        "字段名": "新的值",
    }
}

示例:

请求方式:post,请求地址:http://localhost:9200/teacher/_update/Nt-n5ZABetgWOWEtrSrQ
“result”: “updated” 更新成功

{
    "doc": {
        "email": "zkt77@qq.com"
    }
}

重新查看

总结

  • 创建文档:POST /{索引库名}/_doc/文档id { json文档 }
  • 查询文档:GET /{索引库名}/_doc/文档id
  • 删除文档:DELETE /{索引库名}/_doc/文档id
  • 修改文档:
    • 全量修改:PUT /{索引库名}/_doc/文档id { json文档 }
    • 增量修改:POST /{索引库名}/_update/文档id { "doc": {字段}}

六、RestAPI

        ES官方提供了各种不同语言的客户端,用来操作 ES 。这些客户端的本质就是组装 DSL 语句,通过 http 请 求发送给ES 。官方文档地址: https://www.elastic.co/guide/en/elasticsearch/client/index.html
其中的 Java Rest Client 又包括两种:
  • Java Low Level Rest Client
  • Java High Level Rest Client

我们学习的是Java HighLevel Rest Client客户端API

数据结构如下(数据库字段)

CREATE TABLE `tb_hotel` (
`id` bigint(20) NOT NULL COMMENT '酒店id',
`name` varchar(255) NOT NULL COMMENT '酒店名称;例:7天酒店',
`address` varchar(255) NOT NULL COMMENT '酒店地址;例:航头路',
`price` int(10) NOT NULL COMMENT '酒店价格;例:329',
`score` int(2) NOT NULL COMMENT '酒店评分;例:45,就是4.5分',
`brand` varchar(32) NOT NULL COMMENT '酒店品牌;例:如家',
`city` varchar(32) NOT NULL COMMENT '所在城市;例:上海',
`star_name` varchar(16) DEFAULT NULL COMMENT '酒店星级,从低到高分别是:1星到5星,1
钻到5钻',
`business` varchar(255) DEFAULT NULL COMMENT '商圈;例:虹桥',
`latitude` varchar(32) NOT NULL COMMENT '纬度;例:31.2497',
`longitude` varchar(32) NOT NULL COMMENT '经度;例:120.3925',
`pic` varchar(255) DEFAULT NULL COMMENT '酒店图片;例:/img/1.jpg',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

mapping映射分析

来看下酒店数据的索引库结构 :
PUT /hotel
{
  "mappings": {
    "properties": {
      "id": {
        "type": "keyword"
      },
      "name": {
        "type": "text",
        "analyzer": "ik_max_word",
        "copy_to": "all"
      },
      "address": {
        "type": "keyword",
        "index": false,
        "copy_to": "all"
      },
      "price": {
        "type": "integer"
      },
      "score": {
        "type": "integer"
      },
      "brand": {
        "type": "keyword",
        "copy_to": "all"
      },
      "city": {
        "type": "keyword",
        "copy_to": "all"
      },
      "starName": {
        "type": "keyword"
      },
      "business": {
        "type": "keyword"
      },
      "location": {
        "type": "geo_point"
      },
      "pic": {
        "type": "keyword",
        "index": false
      },
      "all": {
        "type": "text",
        "analyzer": "ik_max_word"
      }
    }
  }
}
几个特殊字段说明:
  • location:地理坐标,里面包含精度、纬度
  • all:一个组合字段,其目的是将多字段的值 利用copy_to合并,提供给用户搜索

地理坐标说明:

copy_to说明:

整体结构图

1. 初始化RestClient

        在elasticsearch提供的API中,与elasticsearch一切交互都封装在一个名为RestHighLevelClient的类中,必须先完成这个对象的初始化,建立与elasticsearch的连接。

分为三步:

1)引入es的RestHighLevelClient依赖:

         <dependency>
            <groupId>org.elasticsearch.client</groupId>
            <artifactId>elasticsearch-rest-high-level-client</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.28</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>    
     <!-- FastJson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.71</version>
        </dependency>
        <!--
                提供工具类
                    字符串的处理类(StringUtils)
                    随机数生成类(RandomStringUtils)
                    数字类NumberUtils
                    数组类ArrayUtils
                    日期类DateUtils
                 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

2)因为SpringBoot默认的ES版本是7.6.2,所以我们需要覆盖默认的ES版本:

<properties>
<java.version>1.8</java.version>
<elasticsearch.version>7.12.0</elasticsearch.version>
</properties>

3)初始化RestHighLevelClient:

初始化的代码如下:
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://localhost:9200")
));
这里为了单元测试方便,我们创建一个测试类 HotelIndexTest ,然后将初始化的代码编写在
@BeforeEach 方法中:
public class HotelIndexTest {
    private RestHighLevelClient client;

    @Before
    void setUp() {
        this.client = new RestHighLevelClient(
            RestClient.builder(
                HttpHost.create("http://localhost:9200")
            )
        );
    }

    @After
    void tearDown() throws IOException {
        this.client.close();
    }
}

2. 创建索引库

创建索引库的API如下:
创建一个类,定义mapping映射的JSON字符串常量:

 common 

{
    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" +
            "        \"copy_to\": \"all\"\n" +
            "      },\n" +
            "      \"starName\":{\n" +
            "        \"type\": \"keyword\"\n" +
            "      },\n" +
            "      \"business\":{\n" +
            "        \"type\": \"keyword\"\n" +
            "      },\n" +
            "      \"location\":{\n" +
            "        \"type\": \"geo_point\"\n" +
            "      },\n" +
            "      \"pic\":{\n" +
            "        \"type\": \"keyword\",\n" +
            "        \"index\": false\n" +
            "      },\n" +
            "      \"all\":{\n" +
            "        \"type\": \"text\",\n" +
            "        \"analyzer\": \"ik_max_word\"\n" +
            "      }\n" +
            "    }\n" +
            "  }\n" +
            "}";
}

3. 配置yml

server:
  port: 8089
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/es?serverTimezone=GMT
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
logging:
  level:
    com.zkt: debug
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
  type-aliases-package: com.zkt.hotel.pojo

4. 测试-索引库操作

  • 判断索引库是否存在
  • 编写单元测试,实现创建索引:
  • 编写单元测试,实现删除索引: 

@SpringBootTest
public class HotelDemoApplicationTests {
    private RestHighLevelClient client;

    @BeforeEach
    void setUp() {
        this.client = new RestHighLevelClient(RestClient.builder(
                HttpHost.create("http://localhost:9200")
        ));
    }

    @AfterEach
    void tearDown() throws IOException {
        this.client.close();
    }


    @Test
    void testExistsHotelIndex() throws IOException {
        // 1.创建Request对象
        GetIndexRequest request = new GetIndexRequest("hotels");
        // 2.发送请求
        boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
        // 3.输出
        System.err.println(exists ? "索引库已经存在!" : "索引库不存在!");
    }

    //创建索引库
    @Test
    void createHotelIndex() throws IOException {
        // 1.创建Request对象
        CreateIndexRequest request = new CreateIndexRequest("hotels");
        // 2.准备请求的参数:DSL语句
        request.source(HotelConstants.MAPPING_TEMPLATE, XContentType.JSON);
        // 3.发送请求
        client.indices().create(request, RequestOptions.DEFAULT);
    }

    @Test
    void testDeleteHotelIndex() throws IOException {
        // 1.创建Request对象
        DeleteIndexRequest request = new DeleteIndexRequest("hotels");
        // 2.发送请求
        client.indices().delete(request, RequestOptions.DEFAULT);
    }

}

总结

        JavaRestClient操作 elasticsearch 的流程基本类似。核心是 client.indices() 方法来获取索引库的操作对 象。

索引库操作的基本步骤:
  • 初始化RestHighLevelClient
  • 创建XxxIndexRequest。XXX是Create、Get、Delete
  • 准备DSL( Create时需要,其它是无参)
  • 发送请求。调用RestHighLevelClient#indices().xxx()方法,xxx是create、exists、delete

RestClient操作文档

pojo

与数据库相关的实体类

/**
 * @author zkt
 * @Version 1.0
 * @since 2024/7/23
 */
@Data
@TableName("tb_hotel")
public class Hotel {
    @TableId(type = IdType.INPUT)
    private Long id;
    private String name;
    private String address;
    private Integer price;
    private Integer score;
    private String brand;
    private String city;
    private String starName;
    private String business;
    private String longitude;//经度
    private String latitude;//纬度
    private String pic;
}

我们需要定义一个新的类型,与索引库结构吻合:
为ES索引库设计实体类
  • longitude和latitude需要合并为location
/**
 * @author zkt
 * @Version 1.0
 * @since 2024/7/23
 */
@Data
@NoArgsConstructor
public class HotelDoc {
    private Long id;
    private String name;
    private String address;
    private Integer price;
    private Integer score;
    private String brand;
    private String city;
    private String starName;
    private String business;
    private String location;
    private String pic;

    public HotelDoc(Hotel hotel) {
        this.id = hotel.getId();
        this.name = hotel.getName();
        this.address = hotel.getAddress();
        this.price = hotel.getPrice();
        this.score = hotel.getScore();
        this.brand = hotel.getBrand();
        this.city = hotel.getCity();
        this.starName = hotel.getStarName();
        this.business = hotel.getBusiness();
        this.location = hotel.getLatitude() + ", " + hotel.getLongitude();
        this.pic = hotel.getPic();
    }
}

三层

这里不写controller 直接测试就行

mapper

/**
 * @author zkt
 * @Version 1.0
 * @since 2024/7/23
 */
public interface HotelMapper extends BaseMapper<Hotel> {
}

service

/**
 * @author zkt
 * @Version 1.0
 * @since 2024/7/23
 */
public interface IHotelService extends IService<Hotel> {
}
/**
 * @author zkt
 * @Version 1.0
 * @since 2024/7/23
 */
@Service
public class HotelService  extends ServiceImpl<HotelMapper, Hotel> implements IHotelService {
}

启动类

@MapperScan("com.zkt.hotel.mapper")
@SpringBootApplication
public class HotelDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(HotelDemoApplication.class, args);
    }
}

测试类

/**
 * @author zkt
 * @Version 1.0
 * @since 2024/7/24
 */

@SpringBootTest
public class HotelDocumentTests {
    //ctrl+alt+m 抽取公共方法
    private RestHighLevelClient client;

    @Autowired
    HotelService hotelService;

    @BeforeEach
    void setUp() {
        this.client = new RestHighLevelClient(RestClient.builder(
                HttpHost.create("http://localhost:9200")
        ));
    }

    @AfterEach
    void tearDown() throws IOException {
        this.client.close();
    }

    //解析响应对象
    public void show(SearchResponse response) {
        // 4.解析响应
        SearchHits searchHits = response.getHits();
        // 4.1.获取总条数
        long total = searchHits.getTotalHits().value;
        System.out.println("共搜索到" + total + "条数据");
        // 4.2.文档数组
        SearchHit[] hits = searchHits.getHits();
        // 4.3.遍历
        for (SearchHit hit : hits) {
            // 获取文档source
            String json = hit.getSourceAsString();
            // 反序列化
            HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
            System.out.println("hotelDoc = " + hotelDoc);
        }
    }


    /*************************文档增,删,修,查*********************************/
    @Test
    void testAddDocument() throws IOException {
        // 1.根据id查询酒店数据
        Hotel hotel = hotelService.getById(197837109);
        // 2.转换为文档类型
        HotelDoc hotelDoc = new HotelDoc(hotel);
        // 3.将HotelDoc转json
        String json = JSON.toJSONString(hotelDoc);
        // 1.准备Request对象
        IndexRequest request = new IndexRequest("hotels").id(hotelDoc.getId().toString());
        // 2.准备Json文档
        request.source(json, XContentType.JSON);
        // 3.发送请求
        client.index(request, RequestOptions.DEFAULT);
    }

    @Test
    void testDeleteDocument() throws IOException {
        // 1.准备Request
        DeleteRequest request = new DeleteRequest("hotels", "197837109");
        // 2.发送请求
        client.delete(request, RequestOptions.DEFAULT);
    }

    @Test
    void testUpdateDocument() throws IOException {
        // 1.准备Request
        UpdateRequest request = new UpdateRequest("hotels", "197837109");
        // 2.准备请求参数
        request.doc(
                "name", "W酒店",
                "city", "西安",
                "price", "2000",
                "starName", "五星级"
        );
        // 3.发送请求
        client.update(request, RequestOptions.DEFAULT);
    }

    @Test
    void testGetDocumentById() throws IOException {
        // 1.准备Request
        GetRequest request = new GetRequest("hotels", "197837109");
        // 2.发送请求,得到响应
        GetResponse response = client.get(request, RequestOptions.DEFAULT);
        // 3.解析响应结果
        String json = response.getSourceAsString();

        HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
        System.out.println(hotelDoc);
    }

    //批量新增
    @Test
    void testBulkRequest() throws IOException {
        // 批量查询酒店数据
        List<Hotel> hotels = hotelService.list();

        // 1.创建Request
        BulkRequest request = new BulkRequest();
        // 2.准备参数,添加多个新增的Request
        for (Hotel hotel : hotels) {
            // 2.1.转换为文档类型HotelDoc
            HotelDoc hotelDoc = new HotelDoc(hotel);
            // 2.2.创建新增文档的Request对象
            request.add(new IndexRequest("hotels")
                    .id(hotelDoc.getId().toString())
                    .source(JSON.toJSONString(hotelDoc), XContentType.JSON));
        }
        // 3.发送请求
        client.bulk(request, RequestOptions.DEFAULT);
    }

    /*****************************全文检索*********************************/

//查询所有
    @Test
    void testMatchAll() throws IOException {
        // 1.准备Request
        SearchRequest request = new SearchRequest("hotels");
        // 2.准备DSL,QueryBuilders构造查询条件
        request.source()
                .query(QueryBuilders.matchAllQuery());
        //3.发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        show(response);
    }

    //查询all字段内容中有如家的(or拼接多条件)
    @Test
    void testMatch() throws IOException {
        // 1.准备Request
        SearchRequest request = new SearchRequest("hotels");
        // 2.准备DSL 参数1:字段  参数2:数据
        request.source()
                .query(QueryBuilders.matchQuery("all", "如家"));
        // 3.发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        // 4.解析响应
        show(response);
    }

    //查询name,business字段内容中有如家的
    @Test
    void testMultiMatch() throws IOException {
        // 1.准备Request
        SearchRequest request = new SearchRequest("hotels");
        // 2.准备DSL 参数1:字段  参数2:数据
        request.source()
                .query(QueryBuilders.multiMatchQuery("如家", "name","business"));
        // 3.发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        // 4.解析响应
        show(response);
    }
    /***************************精确检索*********************************/
    //词条查询
    @Test
    void testTermQuery() throws IOException {
        // 1.准备Request
        SearchRequest request = new SearchRequest("hotels");
        // 2.准备DSL,QueryBuilders构造查询条件
        request.source()
                .query(QueryBuilders.termQuery("city","上海"));
        // 3.发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        show(response);
    }
    //范围查询
    @Test
    void testRangeQuery() throws IOException {
        // 1.准备Request
        SearchRequest request = new SearchRequest("hotels");
        // 2.准备DSL,QueryBuilders构造查询条件
        request.source()
                .query(QueryBuilders.rangeQuery("price").gte(100).lte(150));
        // 3.发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        show(response);
    }

    @Test
    void testBool() throws IOException {
        // 1.准备request
        SearchRequest request = new SearchRequest("hotels");
//      布尔查询是一个或多个查询子句的组合,子查询的组合方式有:
//      must:必须匹配每个子查询,类似“与”
//      should:选择性匹配子查询,类似“或”
//      must_not:必须不匹配,不参与算分,类似“非”
//      filter:必须匹配,类似“与”,不参与算分
//        一般搜索框用must,选择条件使用filter

        // 2.准备请求参数(and拼接)
//        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
//        // 2.1.must
//        boolQuery.must(QueryBuilders.termQuery("city", "上海"));
//        // 2.2.filter小于等于
//        boolQuery.filter(QueryBuilders.rangeQuery("price").lte(260));
//
//        request.source().query(boolQuery);

        //方式2
        request.source().query(
                QueryBuilders.boolQuery()
                        .must(QueryBuilders.termQuery("city", "上海"))
                        .filter(QueryBuilders.rangeQuery("price").lte(260))
        );
        // 3.发送请求,得到响应
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        // 4.结果解析
        show(response);
    }
    @Test
    void testPageAndSort() throws IOException {
        // 页码,每页大小
        int page = 1, size = 5;
        // 查询条件
        String searchName = "如家";
//        String searchName = null;

        // 1.准备Request
        SearchRequest request = new SearchRequest("hotels");
        // 2.准备DSL
        // 2.1.query
        if(searchName == null){
            request.source().query(QueryBuilders.matchAllQuery());
        }else{
            request.source().query(QueryBuilders.matchQuery("name", searchName));
        }
        // 2.2.分页 from、size
        request.source().from((page - 1) * size).size(size);
        //2.3.排序
        request.source().sort("price",SortOrder.DESC);

        // 3.发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        // 4.解析响应
        show(response);

    }
}

 id查询

查询所有 因为自动实现了分页 所以只显示10条数据查询name,business字段内容中有如家的词条查询范围查询

小结

文档操作的基本步骤:
  • 初始化RestHighLevelClient
  • 创建XxxRequestXXXIndexGetUpdateDeleteBulk
  • 准备参数(IndexUpdateBulk时需要)
  • 发送请求。调用RestHighLevelClient#.xxx()方法,xxxindexgetupdatedeletebulk
  • 解析结果(Get时需要)

七、ElasticSearch查询 

1. DSL查询文档

elasticsearch 的查询依然是基于 JSON 风格的 DSL 来实现的。
1.1 DSL 查询分类
        Elasticsearch提供了基于 JSON DSL Domain Specific Language )来定义查询。常见的查询类型包括:
  • 查询所有:查询出所有数据,一般测试用(不会显示出所有,自带分页功能)。例如:match_all
  • 全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。例如:
    • match_query:单字段查询
    • multi_match_query:多字段查询,任意一个字段符合条件就算符合查询条件
  • 精确查询:根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型字段。 例如:
    • ids
    • range根据值的范围查询2.RestClient查询文档
    • term根据词条精确值查询
  • 地理(geo)查询:根据经纬度查询。例如:
    • geo_distance
    • geo_bounding_box
  • 复合(compound)查询:复合查询可以将上述各种查询条件组合起来,合并查询条件。例如:
    • bool
    • function_score

2. RestClient查询文档  

基本步骤包括:
  • 1)准备Request对象
  • 2)准备请求参数
  • 3)发起请求
  • 4)解析响应

测试见上面

八、案例(含页面)

总体结构图

在上面的基础上 新增两个实体类

@Data
public class RequestParams {
    private String key;//搜索关键字
    private Integer page;//当前页
    private Integer size;//每页记录数
    private String sortBy;//排序字段

    /*****组合查询服务属性*****/
    private String brand;//品牌
    private String city;//城市
    private String starName;//星级
    private Integer minPrice;
    private Integer maxPrice;

}
@Data
public class PageResult {
    private Long total;
    private List<HotelDoc> hotels;

    public PageResult() {
    }

    public PageResult(Long total, List<HotelDoc> hotels) {
        this.total = total;
        this.hotels = hotels;
    }
}

service

public interface IHotelService extends IService<Hotel> {

    PageResult search(RequestParams params);

}
@Service
public class HotelService extends ServiceImpl<HotelMapper, Hotel> implements IHotelService {



    @Autowired
    private RestHighLevelClient restHighLevelClient;

    /***********************基础查询***************************/
//    @Override
//    public PageResult search(RequestParams params) {
//        try {
//            // 1.准备Request
//            SearchRequest request = new SearchRequest("hotels");
//            // 2.准备请求参数
//            // 2.1.查询条件
//            String key = params.getKey();
//            if (key == null || "".equals(key)) {
//                request.source().query(QueryBuilders.matchAllQuery());
//            } else {
//                request.source().query(QueryBuilders.matchQuery("all",key));
//            }
//            // 2.2.分页
//            int page = params.getPage();
//            int size = params.getSize();
//            request.source().from((page - 1) * size).size(size);
//            // 3.发送请求
//            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
//            // 4.解析响应
//            return handleResponse(response);
//        } catch (IOException e) {
//            throw new RuntimeException("搜索数据失败", e);
//        }
//    }

    /***********************条件查询***************************/
    @Override
    public PageResult search(RequestParams params) {
        try {
            // 1.准备Request
            SearchRequest request = new SearchRequest("hotels");
            // 1.准备Boolean查询
            BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();

            // 1.1.关键字搜索,match查询,放到must中
            String key = params.getKey();
            if (key == null || "".equals(key)) {
                // 为空,查询所有
                boolQuery.must(QueryBuilders.matchAllQuery());
            } else {
                // 不为空,根据关键字查询
                boolQuery.must(QueryBuilders.matchQuery("all", key));
            }

            // 1.2.品牌
            String brand = params.getBrand();
            if (brand != null) {
                boolQuery.filter(QueryBuilders.termQuery("brand", brand));
            }
            // 1.3.城市
            String city = params.getCity();
            if (city!=null) {
                boolQuery.filter(QueryBuilders.termQuery("city", city));
            }
            // 1.4.星级
            String starName = params.getStarName();
            if (starName!=null) {
                boolQuery.filter(QueryBuilders.termQuery("starName", starName));
            }
            // 1.5.价格范围
            Integer minPrice = params.getMinPrice();
            Integer maxPrice = params.getMaxPrice();
            if (minPrice != null && maxPrice != null) {
                boolQuery.filter(QueryBuilders.rangeQuery("price").gte(minPrice).lte(maxPrice));
            }
            // 3.设置查询条件
            request.source().query(boolQuery);
            // 2.2.分页
            int page = params.getPage();
            int size = params.getSize();
            request.source().from((page - 1) * size).size(size);
            // 3.发送请求
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            // 4.解析响应
            PageResult pageResult = handleResponse(response);
            System.out.println(pageResult);
            return pageResult;
        } catch (IOException e) {
            throw new RuntimeException("搜索数据失败", e);
        }
    }




    //处理结果集
    private PageResult handleResponse(SearchResponse response) {
        SearchHits searchHits = response.getHits();
        // 4.1.总条数
        long total = searchHits.getTotalHits().value;
        // 4.2.获取文档数组
        SearchHit[] hits = searchHits.getHits();
        // 4.3.遍历
        List<HotelDoc> hotels = new ArrayList<>(hits.length);
        for (SearchHit hit : hits) {
            // 4.4.获取source
            String json = hit.getSourceAsString();
            // 4.5.反序列化,非高亮的
            HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
            // 4.9.放入集合
            hotels.add(hotelDoc);
            System.out.println(hotelDoc);
        }
        return new PageResult(total, hotels);
    }

}

前端页面

index.html

banner.css

index.css

amap.min.js

axios.min.js

vue.js

已上传资源

测试结果 

  • 24
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冯诺依曼转世

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值