springcloud篇7-ElasticSearch分布式搜索

一、初识elasticsearch

elasticsearch是一款非常强大的开源搜索引擎,可以帮助我们从海量数据中快速找到需要的内容。

例如,搜索商品:

google浏览器搜索:

打车软件搜索附近车辆:

1.1 了解ES

elasticsearch是elastic stack(ELK)的核心,elastic stack还包括kibana、Logstash、Beats,被广泛应用于 日志数据分析、实时监控 等领域。

(1)elasticsearch 负责存储、搜索和分析数据。

(2)logstash和beats 负责数据抓取(可替代,可以写java代码进行搜索)。

(3)kibana 负责数据可视化(可替代,可视化的工具有很多,不一定要用kibana)。

ElasticSearch的底层实现是Lucene技术,Lucene是一个java语言的搜索引擎类库 ,是Apache公司的顶级项目。

官方网址:https://lucene.apache.org/

Lucene的优势:

(1)易扩展

(2)高性能(基于倒排索引)

缺点:

(1)只限于Java语言开发

(2)学习曲线陡峭(API设计复杂)

(3)不支持水平扩展(类库只考虑如何实现搜索,面对高并发场景不支持集群扩展,要想实现需要进行二次开发)
ElasticSearch解决了Lucene的问题,支持分布式,可水平扩展;提供Restful接口,可被任何语言调用。

1.2 倒排索引

正向索引和倒排索引

传统数据库(如MySQL)采用正向索引,例如给tb_goods表的字段id创建索引,而对于没加索引的字段title,根据该字段查询时需要遍历所有记录。

倒排索引:

倒排索引创建时会建立一个新的表,包括文档(document)和词条(term)。
文档:每条数据就是一个文档
词条:文档按照语义分成的词语


词条字段不能重复,给词条创建索引,根据词条查找id,再在原表里根据id查找,速度很快

1.3 ES与MySQL对比

(1)文档

elasticsearch是面向文档存储的,文档数据会被序列化为json格式后存储在elasticsearch中。

(2)索引

索引是相同类型的文档的集合

映射:索引中文档的字段约束信息,类似表的结构约束(约束索引中文档的数据格式相同)。

对比:

(1)MySQL擅长事务类型操作,可以确保数据的安全和一致性。

(2)Elasticsearch:擅长海量数据的搜索、分析和计算。

1.4 安装ES和Kibana

1.4.1 创建网络

因为还需要部署kibana容器,因此需要让es和kibana容器互联,先创建一个网络:

bash 复制代码
docker network create es-net

1.4.2 加载镜像

将资料中的两个jar包导入(镜像太大,docker pull时间会很长)。

bash 复制代码
docker load -i es.tar
docker load -i kibana.tar

1.4.3 运行(创建容器)

(1)运行elasticsearch

bash 复制代码
docker run -d \
	--name es \
    -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
    -e "discovery.type=single-node" \
    -v es-data:/usr/share/elasticsearch/data \
    -v es-plugins:/usr/share/elasticsearch/plugins \
    --privileged \
    --network es-net \
    -p 9200:9200 \
    -p 9300:9300 \
elasticsearch:7.12.1

命令解释:

  • -e "cluster.name=es-docker-cluster":设置集群名称
  • -e "http.host=0.0.0.0":监听的地址,可以外网访问
  • -e "ES_JAVA_OPTS=-Xms512m -Xmx512m":内存大小
  • -e "discovery.type=single-node":非集群模式
  • -v es-data:/usr/share/elasticsearch/data:挂载逻辑卷,绑定es的数据目录
  • -v es-logs:/usr/share/elasticsearch/logs:挂载逻辑卷,绑定es的日志目录
  • -v es-plugins:/usr/share/elasticsearch/plugins:挂载逻辑卷,绑定es的插件目录
  • --privileged:授予逻辑卷访问权
  • --network es-net :加入一个名为es-net的网络中
  • -p 9200:9200:端口映射配置


    (2)运行kibana
bash 复制代码
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=es-net \
-p 5601:5601  \
kibana:7.12.1

命令解释:

  • --network es-net :加入一个名为es-net的网络中,与elasticsearch在同一个网络中
  • -e ELASTICSEARCH_HOSTS=http://es:9200":设置elasticsearch的地址,因为kibana已经与elasticsearch在一个网络,因此可以用容器名直接访问elasticsearch
  • -p 5601:5601:端口映射配置

注意:kibana启动速度较慢,虽然通过docker ps可以查看到,但启动速度很慢,会产生各种数据和日志。可以通过docker logs -f kibana查看。

bash 复制代码
docker logs -f kibana

通过浏览器访问:

点击"Explore on my own":


DSL语句的本质就是发送一个Restful请求到ES中。

1.5 安装KI分词器

es在创建倒排索引时 需要对文档分词;在搜索时,需要对用户输入内容分词 。但默认的分词规则对中文处理并不友好。

处理分词时,一般会使用IK分词器。

bash 复制代码
docker volume inspect es-plugins

将解压的ik文件夹里的东西上传到查询到的插件目录/var/lib/docker/volumes/es-plugins/_data。直接上传可能打不开这个目录,可以先上传到一个目录再用mv指令。

重启docker服务的es:

bash 复制代码
docker restart es

IK分词器包含两种模式:

  • ik_smart:最少切分
  • ik_max_word:最细切分

测试:

GET /_analyze { "analyzer": "ik_max_word", "text": "黑马程序员学习java太棒了" }

补充:IK分词器的拓展和停用字典

IK分词器的分词是基于一个词库,词库里有的词就会切分,但有些词词库里没有,无法准确切分,此时需要拓展。

例如:

最新的网络用语"白嫖"、"欧力给"没办法切割出来。

要拓展IK分词器的词库,只需要修改一个IK分词器目录中的config目录中的IKAnalyzer.cfg.xml文件:

也可以加一些停用词(比如"的","地","了"等):

文件里新增地ext.dic和stopword.dic是文件,需要新建并在文件中添加词。




测试:

二、索引库操作

mapping映射是对索引库中文档的约束。

可以查看官方文档:https://www.elastic.co/

2.1 mapping属性

(一)type(字段数据类型)

(1)字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、IP地址)

(2)数值:long、Integer、short、byte、double、float

(3)布尔:boolean

(4)日期:date

(5)对象:object

(二)index(是否创建索引,默认为true)

(三)analyzer(使用哪种分词器)

结合text类型用,值为ik_smart、ik_max_word

(四)properties:该字段的子字段

2.2 创建索引库

ES通过Restful请求操作索引库、文档。请求内容用DSL语句来表示,创建索引库和mapping的DSL语法如下:

一个示例:

操作实例:

创建索引库

bash 复制代码
PUT /heima
{
  "mappings": {
    "properties": {
      "info":{
        "type":"text",
        "analyzer": "ik_smart"
      },
      "email":{
        "type":"keyword",
        "index":false
      },
      "name":{
        "type":"object",
        "properties":{
          "firstName":{
            "type":"keyword"
          },
          "lastName":{
            "type":"keyword"
          }
        }
      }
    }
  }
}

2.3 查看、删除索引库

查看索引库语法:
GET /索引库名

删除索引库语法:
DELETE /索引库名

注意:ES中索引库是不允许修改的,但是可以新增新字段。

bash 复制代码
# 查询
GET /heima
bash 复制代码
# 修改索引库
PUT /heima/_mapping
{
  "properties":{
    "age":{
      "type":"integer"
    }
  }
}
bash 复制代码
# 删除
DELETE /heima

三、文档操作

3.1 新增文档

一个例子:

bash 复制代码
# 插入一个文档
POST /heima/_doc/1
{
  "info":"黑马程序员Java讲师",
  "email":"zy@itcast.cn",
  "name":{
    "firstName":"云",
    "lastName":"赵"
  }
}

3.2 查询、删除文档




3.3 修改文档

3.3.1 方式一、全量修改

注意:改方式如果文档id没有对应的就文档,则相当于执行"新增"操作。

例如:

bash 复制代码
PUT /heima/_doc/1
{
  "info":"黑马程序员Java讲师",
  "email":"ZhaoYun@itcast.cn",
  "name":{
    "firstName":"云",
    "lastName":"赵"
  }
}

3.3.2 局部修改(修改指定字段)



四、RestClient

ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过http请求发送给ES。

地址:

https://www.elastic.co/guide/en/elasticsearch/client/index.html

通过一个案例学习。

4.1 导入代码和SQL



注意修改数据库连接。

4.2 分析数据结构

mappging要考虑的问题:

字段名、数据类型、是否参与搜索、是否分词、如果分词分词器是什么。

表结构如下:

新增酒店索引的DSL语句如下:

bash 复制代码
# 酒店的mapping
PUT /hotel
{
  "mapping":{
    "properties":{
      "id":{
        "type":"keyword" # id一般是字符串类型
      },
      "name":{
        "type":"text",
        "analyzer":"ik_max_word"
      },
      "address":{
        "type":"keyword",
        "index":false
      },
      "price":{
        "type":"integer"
      },
      "score":{
        "type":"integer"
      },
      "brand":{
        "type":"keyword"
      },
      "city":{
        "type":"keyword"
      },
      "starName":{
        "type":"keyword"
      },
      "business":{
        "type":"keyword"
      },
      "location":{
        "type":"geo_point"
      },
      "pic":{
        "type":"keyword",
        "index":false
      }
    }
  }
}

另外,需要实现多个字段(包括酒店名称、酒店品牌、酒店星级、酒店商圈等)都能搜索的功能,但是分别创建 索引和分词比较麻烦,ES可以把这些字段的值汇总到一个字段,使用copy_to属性。

bash 复制代码
# 酒店的mapping
PUT /hotel
{
  "mapping":{
    "properties":{
      "id":{
        "type":"keyword"
      },
      "name":{
        "type":"text",
        "analyzer":"ik_max_word",
        "copy_to":"all"
      },
      "address":{
        "type":"keyword",
        "index":false
      },
      "price":{
        "type":"integer"
      },
      "score":{
        "type":"integer"
      },
      "brand":{
        "type":"keyword",
        "copy_to":"all"
      },
      "city":{
        "type":"keyword"
      },
      "starName":{
        "type":"keyword"
      },
      "business":{
        "type":"keyword",
        "copy_to":"all"
      },
      "location":{
        "type":"geo_point"
      },
      "pic":{
        "type":"keyword",
        "index":false
      },
      "all":{
        "type":"text",
        "analyzer":"ik_max_word"
      }
    }
  }
}

4.3 初始化JavaRestClient


1.引入es的RestHighLevelClient依赖

2.覆盖springboot默认的es版本

见上图。
3.初始化RestHighLevelClient

java 复制代码
public class HotelIndexTest {
    private RestHighLevelClient client;

    @BeforeEach
    void setUp() {
        this.client = new RestHighLevelClient(RestClient.builder(
                HttpHost.create("http://10.38.48.12:9200")));
    }
    @AfterEach
    void tearDown() throws IOException {
        this.client.close();
    }

    @Test
    void testInit(){
        System.out.println(client);
    }
}

测试结果:

4.4 RestClient操作索引库

4.4.1 创建索引库

就是写java代码来组装之前写的DSL语句。

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

注意上图中的MAPPING_TEMPLATEZ字段,它是另一个类的静态字段。

java 复制代码
public class HotelConstants {
    public static final String MAPPING_TEMPLATE="{\n" +
            "  \"mapping\":{\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" +
            "      \"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" +
            "}";
}


4.4.2 删除和判断索引


注意区分删除和判断是否存在于创建的区别。

java 复制代码
    @Test
    void deleteHotelIndex() throws IOException {
        //1.创建Request对象
        DeleteIndexRequest request=new DeleteIndexRequest("hotel");
        //2.发送请求
        client.indices().delete(request, RequestOptions.DEFAULT);
    }

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


4.5 RestClient操作文档

4.5.1 初始化JavaRestClient


4.5.2 添加酒店数据到数据库


还需要注意一个问题,前面在elastic中创建Hotel索引库时,里面的字段和MySQL数据库中表的字段不完全一致,longitude和latitude合成了location。为了方便,可以再建议一个HotelDoc.java来进行转换:


java 复制代码
@SpringBootTest
public class HotelDocumentTest {
    @Autowired
    private IHotelService  hotelService;

    private RestHighLevelClient client;
    @BeforeEach
    void setUp() {
        this.client = new RestHighLevelClient(RestClient.builder(
                HttpHost.create("http://10.38.48.12:9200")));
    }
    @AfterEach
    void tearDown() throws IOException {
        this.client.close();
    }

    @Test
    void addDocument() throws IOException {
        //根据id查询酒店数据
        Hotel hotel = hotelService.getById(61083L);
        //转为文档类型
        HotelDoc hotelDoc = new HotelDoc(hotel);
        //1.创建Request对象
        IndexRequest request=new IndexRequest("hotel").id(hotel.getId().toString());
        //2.准备json文档
        request.source(JSON.toJSONString(hotelDoc),XContentType.JSON);
        //3.发送请求
        client.index(request,RequestOptions.DEFAULT);
    }
}


4.5.3 根据ID查询酒店文档

注意,如上图,从elastic中查出的数据除了包括原始值"_source"字段的值,还有其他一些字段。需要使用getSourceAsString()方法获取数据。

java 复制代码
@Test
    void getDocumentById() throws IOException {

        //1.创建Request对象
        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);
    }

4.5.4 根据id修改酒店数据

全量更新和新增代码一样。

局部更新代码:

java 复制代码
    @Test
    void updateDocumentById() throws IOException {

        //1.创建Request对象
        UpdateRequest request=new UpdateRequest("hotel","61083");
        //2.准备请求参数
        request.doc(
                "price","952",
                "starName","四钻"
        );
        //3.发送请求
        client.update(request,RequestOptions.DEFAULT);
    }


4.5.5 根据id删除酒店数据


java 复制代码
    @Test
    void deleteDocumentById() throws IOException {

        //1.创建Request对象
        DeleteRequest request=new DeleteRequest("hotel","61083");
        //2.发送请求
        client.delete(request,RequestOptions.DEFAULT);
    }


4.5.6 批量导入文档

一条一条记录新增效率很慢,需要批量导入。

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

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


五、DSL查询和RestClient进阶

相关推荐
鸽鸽程序猿1 小时前
【JavaEE】【SpringCloud】注册中心_nacos
java·spring cloud·java-ee
递归尽头是星辰4 小时前
Spring Cloud Alibaba 核心理论体系:Nacos、Sentinel、Seata深度解析
spring cloud·nacos·sentinel·seata·微服务治理
lpfasd1234 小时前
springcloud docker 部署问题排查与解决方案
spring·spring cloud·docker
Elastic 中国社区官方博客4 小时前
使用 Elasticsearch 管理 agentic 记忆
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai·全文检索
蓝眸少年CY5 小时前
(第七篇)spring cloud之Hystrix断路器
spring·spring cloud·hystrix
蓝眸少年CY7 小时前
(第八篇)spring cloud之zuul路由网关
后端·spring·spring cloud
七夜zippoe9 小时前
Elasticsearch核心概念与Java客户端实战 构建高性能搜索服务
java·大数据·elasticsearch·集群·索引·分片
码出财富19 小时前
SpringBoot 内置的 20 个高效工具类
java·spring boot·spring cloud·java-ee
daladongba1 天前
Spring Cloud Gateway
java·spring cloud·gateway
忍冬行者1 天前
Elasticsearch 超大日志流量集群搭建(网关 + 独立 Master + 独立 Data 纯生产架构,角色完全分离,百万级日志吞吐)
大数据·elasticsearch·云原生·架构·云计算