资讯

精准传达 • 有效沟通

从品牌网站建设到网络营销策划,从策略到执行的一站式服务

一、Elasticsearch原理与基本使用

[TOC]

创新互联公司是一家网站设计公司,集创意、互联网应用、软件技术为一体的创意网站建设服务商,主营产品:成都响应式网站建设公司成都品牌网站建设全网整合营销推广。我们专注企业品牌在网站中的整体树立,网络互动的体验,以及在手机等移动端的优质呈现。成都网站建设、成都做网站、移动互联产品、网络运营、VI设计、云产品.运维为核心业务。为用户提供一站式解决方案,我们深知市场的竞争激烈,认真对待每位客户,为客户提供赏析悦目的作品,网站的价值服务。

一、Elasticsearch概述

1.1 什么是搜索

​ 搜索,就是在任何场景下,找寻想要的信息。通过关键字检索出与此关键字有关的信息。这和查询还不太一样,查询通常是在表格类型的数据中查找,字段的内容的长度往往不大。

1.2 使用传统数据库实现搜索

​ 传统数据库的情况下,如果要查询某个字段是否包含某些关键字的话,需要使用到like关键字来进行字段匹配,很大概率导致全表扫描,本身来说性能就不算好。如果再加上查询的字段非常长,那么使用like匹配的工作量是很大的,另外如果表的行数也很多,那么性能就更差了。

1.3 全文检索与倒排索引

​ 全文检索是指计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。全文搜索引擎数据库中的数据。而全文检索用到的关键技术就是倒排索引。什么是倒排索引?看看例子就知道了

数据库中有如下数据
id  员工描述
1   优秀论文
2   优秀员工称号
3   优秀项目
4   优秀团队

建立倒排索引的步骤:
1、每行切词, 怎么切都可以,看实际需要
1 优秀    论文
2 优秀    员工  称号
3 优秀    项目
4 优秀    团队

2、建立倒排索引
优秀  1,2,3,4
论文  1
员工  2
称号  2
项目  3
团队  4

3、检索
倒排索引意思简单就是指定的词出现在哪些行中,这些行都用唯一id进行标识。
所以这就是为什么倒排索引用到全文检索中,因为可以直接查询到包含相关关键字的内容有哪些。
比如搜索优秀,可以看到优秀这个词在1234中都有出现,然后根据id查询原始数据。

​ 有了倒排索引,当我们需要从很多端很长的内容中检索包含指定关键字的内容时,直接根据倒排索引就知道有没有指定关键字了。而如果使用传统数据库,那么必须扫描全部内容,如果数据有1000行,那工作量就很恐怖了。而倒排索引只是查询个关键字而已,无需扫描全部内容。

1.4 Lucene和Elasticsearch

​ Lucene就是一个jar包,里面包含了封装好的各种建立倒排索引,以及进行搜索的代码,包括各种算法。我们就用java开发的时候,引入lucene jar,然后基于lucene的api进行去进行开发就可以了。但是它只是根据文本做出索引,然后保存下来,但是本身并不提供搜索功能。
​ 由于Lucene使用比较复杂,繁琐,所以基于Lucene开发了一个新的项目,也就是Elasticsearch(简称ES)。

1.5 ES的特点与适用场景

特点:

1)可以作为一个大型分布式集群(数百台服务器)技术,处理PB级数据,服务大公司;也可以运行在单机上,服务小公司;
2)Elasticsearch不是什么新技术,主要是将全文检索、数据分析以及分布式技术,合并在了一起,才形成了独一无二的ES;lucene(全文检索),商用的数据分析软件(也是有的),分布式数据库(mycat);
3)对用户而言,是开箱即用的,非常简单,作为中小型的应用,直接3分钟部署一下ES,就可以作为生产环境的系统来使用了,数据量不大,操作不是太复杂;
4)数据库的功能面对很多领域是不够用的(事务,还有各种联机事务型的操作);特殊的功能,比如全文检索,同义词处理,相关度排名,复杂数据分析,海量数据的近实时处理;Elasticsearch作为传统数据库的一个补充,提供了数据库所不能提供的很多功能。

适用场景:

1)维基百科,类似百度百科,牙膏,牙膏的维基百科,全文检索,高亮,搜索推荐。
2)The Guardian(国外新闻网站),类似搜狐新闻,用户行为日志(点击,浏览,收藏,评论)+ 社交网络数据(对某某新闻的相关看法),数据分析,给到每篇新闻文章的作者,让他知道他的文章的公众反馈(好,坏,热门,垃圾,鄙视,崇拜)。
3)Stack Overflow(国外的程序异常讨论论坛),IT问题,程序的报错,提交上去,有人会跟你讨论和回答,全文检索,搜索相关问题和答案,程序报错了,就会将报错信息粘贴到里面去,搜索有没有对应的答案。
4)GitHub(开源代码管理),搜索上千亿行代码。
5)国内:站内搜索(电商,招聘,门户,等等),IT系统搜索(OA,CRM,ERP,等等),数据分析(ES热门的一个使用场景)。

1.6 ES中的相关概念

近实时

两个意思,从写入数据到数据可以被搜索到有一个小延迟(大概1秒);基于es执行搜索和分析可以达到秒级。

集群cluster

ES集群可以有多个节点,但是每个节点属于哪个ES集群中是通过配置集群名称来指定的。当然一个集群只有一个节点也是OK的

节点node

集群中的一个节点,节点也有一个名称(默认是随机分配的),节点名称很重要(在执行运维管理操作的时候),默认节点会去加入一个名称为“elasticsearch”的集群,如果直接启动一堆节点,那么它们会自动组成一个elasticsearch集群,当然一个节点也可以组成一个elasticsearch集群。

index--database

索引包含一堆有相似结构的文档数据,比如可以有一个客户索引,商品分类索引,订单索引,索引有一个名称。一个index包含很多document,一个index就代表了一类类似的或者相同的document。比如说建立一个product index,商品索引,里面可能就存放了所有的商品数据,所有的商品document。类似于传统数据库中的库的概念

type--table

    每个索引里都可以有一个或多个type,type是index中的一个逻辑数据分类,一个type下的document,都有相同的field,比如博客系统,有一个索引,可以定义用户数据type,博客数据type,评论数据type。类似于传统数据库中的表的概念。
    要注意:es逐渐抛弃掉这个概念了,到6.x版本中,已经只允许一个index只有一个type了。

document--行

    文档是es中的最小数据单元,一个document可以是一条客户数据,一条商品分类数据,一条订单数据,通常用JSON数据结构表示,每个index下的type中,都可以去存储多个document。相当于行

field--字段

Field是Elasticsearch的最小单位。一个document里面有多个field,每个field就是一个数据字段。
如:
product document
{
  "product_id": "1",
  "product_name": "高露洁牙膏",
  "product_desc": "高效美白",
  "category_id": "2",
  "category_name": "日化用品"  这些就是字段
}

mapping--映射约束

    数据如何存放到索引对象上,需要有一个映射配置,包括:数据类型、是否存储、是否分词等。所谓映射是对type的存储的一些限制。
例子:
    这样就创建了一个名为blog的Index。Type不用单独创建,在创建Mapping 时指定就可以。Mapping用来定义Document中每个字段的类型,即所使用的 analyzer、是否索引等属性。创建Mapping 的代码示例如下:
client.indices.putMapping({
    index : 'blog',
    type : 'article',
    这里还可以设置type的一些工作属性,比如_source等,后面会讲
    body : {
        article: {
            properties: {
                id: {
                    type: 'string',
                    analyzer: 'ik',
                    store: 'yes',
                },
                title: {
                    type: 'string',
                    analyzer: 'ik',
                    store: 'no',
                },
                content: {
                    type: 'string',
                    analyzer: 'ik',
                    store: 'yes',
                }
            }
        }
    }
});

1.7 ES读写数据的机制

写流程:

1、客户端根据提供的es节点,选择一个node作为协调节点,并发送写请求
2、协调节点对写入的document进行路由,将document进行分片。每个分片单独进行写,每个分片默认都是双备份,写在不同的节点上。
3、分片写入时,主备份由协调节点写入,副备份则是从主备份所在节点同步数据过去。
4、当分片都写完后,由协调节点返回写入完成给客户端

读流程:

读流程就很简单了,如果通过docid来读取,直接根据docid进行hash。判断出该doc存储在哪个节点上,然后到相应节点上读取数据即可。

1.8 ES数据存储结构

一、Elasticsearch原理与基本使用

图1.1 ES存储结构

首先分为两个区域,一个是索引区域,一个是数据区域。前者用来存储生成的倒排索引,后者用来存储原始的document(可以选择不存,后面有说)。

1)索引对象(index):存储数据的表结构 ,任何搜索数据,存放在索引对象上 。
2)映射(mapping):数据如何存放到索引对象上,需要有一个映射配置, 包括:数据类型、是否存储、是否分词等。
3)文档(document):一条数据记录,存在索引对象上 。es会给每个document生成一个唯一的documentID,用于标识该document。当然也可以手动指定docid
4)文档类型(type):一个索引对象,存放多种类型数据,数据用文档类型进行标识。

二、ES部署

使用的es版本为:6.6.2
下载地址:https://www.elastic.co/products/elasticsearch

2.1 单节点部署

解压程序到指定目录:

tar zxf elasticsearch-6.6.2.tar.gz -C /opt/modules/

修改配置文件:

cd /opt/modules/elasticsearch-6.6.2/
vim config/elasticsearch.yml 
修改如下内容:
# ---------------------------------- Cluster -------------------------------------
# 集群名称
cluster.name: my-application
# ------------------------------------ Node --------------------------------------
# 节点名称,需要保证全局唯一
node.name: bigdata121
# ----------------------------------- Paths ---------------------------------------
# 配置es数据目录,以及日志目录
path.data: /opt/modules/elasticsearch-6.6.2/data
path.logs: /opt/modules/elasticsearch-6.6.2/logs
# ----------------------------------- Memory -----------------------------------
# 配置es不检查内存限制,内存不够时启动会检查报错
bootstrap.memory_lock: false
bootstrap.system_call_filter: false
# ---------------------------------- Network ------------------------------------
# 绑定ip
network.host: 192.168.50.121
# --------------------------------- Discovery ------------------------------------
# 初始发现节点,用来给新添加的节点进行询问加入集群
discovery.zen.ping.unicast.hosts: ["bigdata121"]

修改Linux一些内核参数

vim /etc/security/limits.conf
添加如下内容:
Es硬性要求打开最小数目最小为65536,进程数最小为4096,否则无法启动
* soft nofile 65536
* hard nofile 131072
* soft nproc 4096
* hard nproc 4096

vim /etc/security/limits.d/20-nproc.conf 
* soft nproc 1024
#修改为
* soft nproc 4096
这些内核参数需要重启才生效

vim /etc/sysctl.conf 
添加下面配置:
vm.max_map_count=655360
并执行命令:
sysctl -p

创建es的数据目录以及日志目录

mkdir /opt/modules/elasticsearch-6.6.2/{logs,data}   

启动es服务

bin/elasticsearch -d
-d 表示以后台进程服务的方式启动,不加此选项就以前台进程方式启动

测试es

es会启动两个对外端口:
9200:restful api的端口
9300:java api端口

可以直接使用curl访问9200端口
curl http://bigdata121:9200
{
  "name" : "bigdata121",
  "cluster_name" : "my-application",
  "cluster_uuid" : "DM6wmLzsQv2xVDkLMBJzOQ",
  "version" : {
    "number" : "6.6.2",
    "build_flavor" : "default",
    "build_type" : "tar",
    "build_hash" : "3bd3e59",
    "build_date" : "2019-03-06T15:16:26.864148Z",
    "build_snapshot" : false,
    "lucene_version" : "7.6.0",
    "minimum_wire_compatibility_version" : "5.6.0",
    "minimum_index_compatibility_version" : "5.0.0"
  },
  "tagline" : "You Know, for Search"
}
这样就正常了

2.2 多节点部署

2.2.1 es集群节点类型

master node:master 节点主要用于元数据(metadata)的处理,比如索引的新增、删除、分片分配等。
data node:data 节点上保存了数据分片。它负责数据相关操作,比如分片的 CRUD,以及搜索和整合操作。这些操作都比较消耗 CPU、内存和 I/O 资源;
client node:client 节点起到路由请求的作用,实际上可以看做负载均衡器。

那么这三种节点该如何配置,例子:

# 配置文件中给出了三种配置高性能集群拓扑结构的模式,如下: 
# 1. 如果你想让节点从不选举为主节点,只用来存储数据,可作为负载器 
# node.master: false 
# node.data: true 

# 2. 如果想让节点成为主节点,且不存储任何数据,并保有空闲资源,可作为协调器
# node.master: true
# node.data: false

# 3. 如果想让节点既不成为主节点,又不成为数据节点,那么可将他作为搜索器,从节点中获取数据,生成搜索结果等 
# node.master: false 
# node.data: false

# 4. 节点是数据节点,也是master节点,这是默认配置
# node.master: true
# node.data: true

2.2.2 es集群常用部署方案

1、默认情况下,一个节点是数据节点,也是master节点。对于3-5个节点的小集群来讲,通常让所有节点存储数据和具有获得主节点的资格。你可以将任何请求发送给任何节点,并且由于所有节点都具有集群状态的副本,它们知道如何路由请求。多个master的元数据也会同步,不用担心不一致。要注意,master节点的数量最好最少为3,且为单数

2、当集群节点数量比较大时,那么通常就会将主节点、数据节点分开,专门部署在对应的节点上,然后主节点是多个都可用的,形成HA的结构。要注意,master节点的数量最好最少为3,且为单数

实际部署其实和单节点差不多,主要看部署的方案选哪个,master有几个,数据节点有几个,设置下角色即可,这里不多说

2.3 安装head插件

用qq浏览器或者chrome,直接到应用商店搜索elasticsearch-head,直接安装插件即可

三、java api操作ES

3.1 基本操作

3.1.1 maven依赖准备



            org.elasticsearch
            elasticsearch
            6.6.2
        

        
            org.elasticsearch.client
            transport
            6.6.2
        

        
            org.apache.logging.log4j
            log4j-core
            2.9.0
        
        
            junit
            junit
            4.12
        


另外需要自己添加一个log4j2的日志格式配置文件,添加到resource目录下
log4j2.xml


    
        
            
        
    
    
        
            
        
    


下面代码中使用 junit进行运行测试,不会用的自己百度

3.1.2 创建ES连接操作对象

public class ESDemo1 {
    private TransportClient client;

    @Before
    public void getClient() throws UnknownHostException {
        //1、创建es配置对象
        Settings settings = Settings.builder().put("cluster.name", "my-application").build();

        //2、连接es集群
        client = new PreBuiltTransportClient(settings);
        //配置es集群地址
        client.addTransportAddress(new TransportAddress(
                InetAddress.getByName("192.168.50.121"),
                9300
        ));

        System.out.println(client.toString());

    }
}

3.1.3 索引操作

// .get() 表示触发操作
@Test
    public void createBlog() {
        //创建索引blog
        //创建index需要admin用户
        client.admin().indices().prepareCreate("blog").get();
        client.close();
    }

//删除索引
    @Test
    public void deleteIndex() {
        client.admin().indices().prepareDelete("blog").get();
        client.close();
    }

3.1.4 添加doc

@Test
    public void addDocument() {
        //1、json方式添加document
        String d = "{\"id\":1, \"name\":\"山海经\"}";

        //导入document,并指定源的格式为 json.
        IndexResponse indexResponse = client.prepareIndex("blog", "article").setSource(d, XContentType.JSON).execute().actionGet();

        System.out.println(indexResponse.getId());
        client.close();
    }

    @Test
    public void addDocument2() throws IOException {
        //2、另外一种方式添加document
        IndexResponse indexResponse = client.prepareIndex("blog3", "article")
                .setSource(XContentFactory.jsonBuilder()
                        .startObject()
                        .field("name","静夜思")
                        .field("id",4)
                        .endObject()
                ).execute().actionGet();
        System.out.println(indexResponse.getResult());
        client.close();
    }

    @Test
    public void addDocument3() throws IOException {
        //3、通过hashmap组织数据
        HashMap json = new HashMap<>();
        json.put("name","spark从入门到放弃");
        json.put("id","6");

        IndexResponse indexResponse = client.prepareIndex("blog", "article")
                .setSource(json).execute().actionGet();
        System.out.println(indexResponse.getResult());
        client.close();
    }

    @Test
    public void addMoreDocument() throws IOException {
        //4、一次请求内部添加多个document
        BulkRequestBuilder bulkRequestBuilder = client.prepareBulk();
        bulkRequestBuilder.add(
                client.prepareIndex("blog2", "comment").setSource(
                    XContentFactory.jsonBuilder()
                    .startObject()
                    .field("name", "山海经")
                    .field("id",1)
                    .field("commentValue","这是一部很好的作品")
                    .endObject())
        );

        bulkRequestBuilder.add(
                client.prepareIndex("blog2", "comment").setSource(
                    XContentFactory.jsonBuilder()
                    .startObject()
                    .field("name", "骆驼祥子")
                    .field("id",2)
                    .field("commentValue","这是讲一个人的故事")
                    .endObject())
        );
        BulkResponse bulkItemResponses = bulkRequestBuilder.get();
        System.out.println(bulkItemResponses);
        client.close();
    }

要注意的是,从6.x版本开始,一个index中只能有一个type了,如果创建多个type会有以下报错

Rejecting mapping update to [blog] as the final mapping would have more than 1

3.1.5 搜索doc

根据docid搜索document
//搜索单个document
    @Test
    public void getType() {
        GetResponse documentFields = client.prepareGet().setIndex("blog3").setType("article").setId("2OlH9WwBaToKuF8JhwB5").get();
        System.out.println(documentFields.getSourceAsString());
        client.close();
    }

//查询多个doc
    @Test
    public void getDocFromMoreIndex() {
        MultiGetResponse multiGetResponse = client.prepareMultiGet()
                .add("blog", "article", "1")
                .add("blog", "article", "2")
                .get();
        //结果打印
        for (MultiGetItemResponse itemResponse : multiGetResponse) {
            System.out.println( itemResponse.getResponse().getSourceAsString());
        }
        client.close();
    }

3.1.6 更新doc

@Test
    public void updateData() throws IOException {
        //更新数据方式1:通过 prepareupdate方法
        UpdateResponse updateResponse = client.prepareUpdate("blog", "article", "4")
                .setDoc(XContentFactory.jsonBuilder()
                        .startObject()
                        .field("name", "天黑")
                        .field("id", "5")
                        .endObject()
                ).get();
        System.out.println(updateResponse.getResult());
    }

    @Test
    public void updateData2() throws IOException, ExecutionException, InterruptedException {
        //更新数据方式2:通过update方法
        UpdateRequest updateRequest = new UpdateRequest().index("blog").type("article").id("4");
        updateRequest.doc(XContentFactory.jsonBuilder()
                .startObject()
                .field("name", "亚瑟")
                .field("id", "7")
                .endObject());
        UpdateResponse updateResponse = client.update(updateRequest).get();
        System.out.println(updateResponse.getResult());
    }

    @Test
    public void upsertData() throws IOException, ExecutionException, InterruptedException {
        //指定doc不存在时就插入,存在就修改
        //不存在就插入这个
        IndexRequest indexRequest = new IndexRequest("blog","article","6").source(
                XContentFactory.jsonBuilder().startObject()
                .field("name","wang")
                .field("id","10")
                .endObject()
        );

        //存在就更新这个,注意最后的那个 upsert操作,意思就是不存在就插入上面的 indexrequest
        UpdateRequest updateRequest = new UpdateRequest().index("blog").type("article").id("6");
        updateRequest.doc(XContentFactory.jsonBuilder()
                .startObject()
                .field("name", "king")
                .field("id", "7")
                .endObject()).upsert(indexRequest);

        UpdateResponse updateResponse = client.update(updateRequest).get();
        System.out.println(updateResponse.getResult());
        client.close();
    }

3.1.7 删除doc

@Test
    public void deleteDocument() {
        //删除document
        DeleteResponse deleteResponse = client.prepareDelete("blog", "article", "6").get();
        System.out.println(deleteResponse.getResult());
        client.close();
    }

3.2 条件查询doc

关键性一个类是 org.elasticsearch.index.query.QueryBuilders;

3.2.1 查询指定index所有doc

@Test
    public void matchAll() {
    //构建全部查询
        SearchResponse searchResponse = client.prepareSearch("blog").setQuery(QueryBuilders.matchAllQuery()).get();
        //从返回结构中解析doc
        SearchHits hits = searchResponse.getHits();
        for (SearchHit hit:hits){
            System.out.println(hit.getSourceAsString());
        }
        client.close();
    }

3.2.2 全部字段进行全文检索

搜索全部字段中包含指定字符的document
@Test
    public void matchSome() {
        //直接全文检索指定字符
        SearchResponse searchResponse = client.prepareSearch("blog3").setQuery(QueryBuilders.queryStringQuery("思")).get();

        SearchHits hits = searchResponse.getHits();
        for(SearchHit hit:hits) {
            System.out.println(hit.getId());
            System.out.println();
        }
    }

3.2.3 通配符字段全文检索

@Test
    public void wildMatch() {
        //通配符查询,*表示0或者多个字符,?表示单个字符
        SearchResponse searchResponse = client.prepareSearch("blog").setTypes("article").setQuery(QueryBuilders.wildcardQuery("name", "wa*")).get();
        SearchHits hits = searchResponse.getHits();

        for(SearchHit h:hits) {
            System.out.println(h.getSourceAsString());
        }

    }

这个方法用于匹配某个字段的整个内容,类似like操作

3.2.4 对指定字段进行分词搜索

@Test
    public void matchField() {
        //这是对分词结果进行等值操作的方法,不是对整个字段,而是对字段的分词结果
        SearchResponse searchResponse = client.prepareSearch("blog").setQuery(QueryBuilders.termQuery("name", "山")).get();
        SearchHits hits = searchResponse.getHits();
        for(SearchHit hit:hits) {
            System.out.println(hit.getSourceAsString());
        }
        client.close();
    }

这个方法一定要注意:
比如有一个字段内容如下: 我爱中国
假设分词如下: 我 爱  中国
如果使用 QueryBuilders.termQuery("name", "中") 也就是搜索“中”这个字时,实际上没有结果返回的。因为分词中并没有含有单独的“中”。
所以这个方法是用于完整匹配分词结果中的某个分词的。
由此,可以得出,即便是用整个字段的内容来搜索,这个方法也不会返回任何结果的,因为分词结果不包含。

3.2.5对指定字段进行模糊检索

@Test
public void fuzzy() {

// 1 模糊查询
SearchResponse searchResponse = client.prepareSearch("blog").setTypes("article")
.setQuery(QueryBuilders.fuzzyQuery("title", "lucene")).get();

// 2 打印查询结果
SearchHits hits = searchResponse.getHits(); // 获取命中次数,查询结果有多少对象

for(SearchHit hit:hits) {
System.out.println(hit.getSourceAsString());
}

// 3 关闭连接
client.close();
}

这个方法和 termQuery很类似,但是有区别。感兴趣的话可以自己查找资料。这个方法比较少用

3.3 映射mapping

3.3.1 mapping的定义

​ 映射是规定index中的一些属性,以及各自type下的字段的属性(再强调一遍,现在6.x版本一个index下只能有一个type,其实就是变相地去除掉了type)。Elasticsearch映射虽然有idnex和type两层关系,但是实际索引时是以index为基础的。如果同一个index下不同type的字段出现mapping不一致的情况,虽然数据依然可以成功写入并生成各自的mapping,但实际上fielddata中的索引结果却依然是以index内第一个mapping类型来生成的

3.3.2 mapping的写法

定义mapping时,依旧是使用json格式定义定义。一般格式如下:

{
    元数据属性字段,如:
    _type:是哪个type的mapping,还是那句话,type基本不怎么提了
    _index:属于哪个index
    。。。。。。。
    properties:{
        "field1":{
            字段属性字段,如:
            type:字段数据类型
        }
         "field2":{
            字段属性字段,如:
            type:字段数据类型
        }
         。。。。。。。。。
    }

}

基本格式就是这样,分为两大部分,一个是整个index 的元数据信息,一个是针对具体type中的字段信息。

3.3.3 数据类型

核心数据类型
字符串:text,keyword
数字:long, integer, short, byte, double, float, half_float, scaled_float
布尔值:boolean
时间:date
二进制:binary
范围:integer_range, float_range, long_range, double_range, date_range

复杂数据类型
数组:array
对象:object
堆叠/嵌套对象: nested
地理:geo_point,geo_point
IP: ip
字符个数:token_count(输入一个字符串,保存的是它的长度)

3.3.4mapping属性字段

元数据字段:

_all : 它是文档中所有字段的值整合成的一个大字符串,用空格分割。它进行了索引但没有存储,所以我们只能对他进行搜索不能获取。如果我们没有指定搜索的字段,就默认是在_all字段上进行搜索。

_source :文档信息 
包含在文档在创建时的实际主体,它会被存储但不会被索引,用于get或search是返回主体。如果你并不关系数据的主体,只注重数量,那可以将此字段禁用

_routing :路由字段 
es会使用下面的计算公式计算数据应保存在哪个分片,索引指定一个路由字段可以自己来控制哪些值放在一起。

shard_num = hash(_routing) % num_primary_shards

_meta 自定义的元数据 ,因为元数据是每个文档都会带的,索引如果你想要在每个文档上标注一些信息,就可以使用此属性,自定义一些元数据。

_field_names :保存着非空值得属性名集合,可以通过它查询包含某个字段非空值的文档

_id :主键
_index :索引
_type :类型
_uid :类型和id的组合 uid字段的值可以在查询、聚合、脚本和排序中访问:
_parent :父类,可用于关联两个索引

字段属性:

type 数据类型 
改属性用来指定字段的数据类型,一但指点后就不能再修改,如果数据不是以设置的数据类型传入,es会去转换数据,装换不成功则报错。具体可配置的参数,可看前面的数据类型说明。

analyzer 分析器 
用于指定索引创建时使用的分析器是什么,即对同一段内容,不同的分析器会用不同的方式分词,最后在倒排索引上的值是不同的。

index 是否索引
索引选项控制字段值是否被索引。它接受true或false,默认为true。没有索引的字段不是可查询的。

store 
属性值是否被存储,默认情况下字段是可以被搜索但是内容不存储的,值一般都是保存在_source中。但比如一篇文章你有它的内容和原网址,现在需要对内容进行检索,但查看是跳转到它原网址的,那这时就不需要存储内容了。

fielddata 现场数据 
如果你要对一个text类型进行聚合操作,你必须设置这个参数为true。

doc_values 文档数据 
建立一个文档对应字段的“正排索引”,其实就是把文档的字段按列存储了,它不会保存分析的字段。方便聚合排序时访问。
format 默认格式 
一般用于时间格式的数据,指定默认的数据格式, “yyyy-MM-dd HH:mm:ss”

search_analyzer 搜索分析器 
指定搜索时使用的分析器,一般不设置在搜索时就会使用创建索引时使用的分析器,如果要自己指定不同的也只要配置即可。

boost 分值 
指定字段的相关性评分默认是1.0,数值越大,搜索时排序时使用。也可以直接通过查询时指定分值的方式

coerce 是否转换 
在插入数据时,在插入数据类型和映射类型不一致的情况下是否强制转换数据类型。默认是开启的

normalizer 转换器 
因为keyword类型的字段是不进行分析的,但是我们又想要将其统一成一个规则,比如都是小写,比如用ASCILL进行编码,其实就是个给keyword用的分析器。可以在setting下的analysis下定义自己的normalizer使用

copy_to 同步复制 
在插入值是,会把值一同放到另一个字段中。主要用于自己定义一个类似于_all字段的字端。

dynamic 动态映射控制 
该字段是用来控制动态映射的,它有三个值 
-true-自动添加映射 
-false-新值不索引,不能被搜索,但返回的命中源字段中会存在这个值 
-strict-遇到新值抛出异常

enabled 是否启动 
这个值是否要用于搜索

ignore_above 忽视上限 
一个字符串超过指定长度后就不会索引了

ignore_malformed 忽视错误数据 
比如一个文档数据传过来,只用一个字段的数据时不能被存储,ES会抛出异常并且不会存储此数据。我们就可以配置此属性保证数据被存储

include_in_all 是否保存在_all字段中

fields 多字段配置 
比如出现标题既要索引,又有不用索引的情景。我们不能对一个字段设置两个类型,又不想再建一个不同类型的相同字段。我们可以使用多字段的方式,在保存数据时,我们只需保存一个字段,ES会默认将数据保存到这个字段下的多字段上。

null_value 空值 
假如你插入的数据为空,或者数据中没有这个字段的值。那这个文档的这个字段就不参与搜索了。我们可以通过指定一个显示的空值来让他能够参与搜索

norms 规范 
如果一个字段只用于聚合,可以设置为false

3.3.5 _all,_sources,store的区别

背景:
    首先,我们要知道一点,当doc传入es时,es会根据配置给doc的每个字段生成索引,并且会将生成的索引保存到es中。但是至于doc的原始数据是否保存到es中,是可以选择的。这点要先搞清楚,并一定非得把doc的原始数据保存在es中的,es非保存不可的是生成的索引,而不是原始数据

========================
_all:
这是一个特殊字段,是把所有其它字段中的值,以空格为分隔符组成一个大字符串,然后被分析和索引,但是不存储原始数据,也就是说它能被查询,但不能被取回显示。注意这个字段是可以被索引的。默认情况下,如果要进行全文检索,需要指定在哪个字段上检索,如果不知道在哪个字段上,那么_all就起到作用了。_all能让你在不知道要查找的内容是属于哪个具体字段的情况下进行搜索

======================
_source: true/false,默认为true
保存的是doc的本来的原数数据,也就是是json格式的doc。他和_all不同,他是json格式的字符串。而且这个字段不会被索引。
当我们执行检索操作时,是到倒排索引中查询,然后获得含有指定关键字的doc的id,
当 _source 设置为 true时
可以根据上面查询到的docid,返回对应id的document的原始数据。
当  _source 设置为 false时
就只能返回对应的document的id,无法回显对应document的原始数据
这种情况下,一般是使用额外的方式来保存document的原始数据的,比如hbase。而es就单纯保存索引而已

=======================
store:true/false,默认为false
这个属性用于指定是否保存document中对应字段的value,这个的概念和上面的source有点类似了,只不过这里store是针对某个field的原始数据,source是针对整个document的原始数据。

当执行想获取一个document的数据时,
1、采用source方式时:
只需产生一次磁盘IO,因为_source存储的时候,直接把整个doc当做一个字段来存储。当我们需要doc中的某个字段时,是先从source读取数据,然后再解析成json,获取到指定字段内容
2、采用store方式时,
因为每个字段都单独存储了,当需要获得整个doc的数据时,就需要单独每个字段进行取值,有多少个字段就产生多少次磁盘IO。
3、store和source混合使用时
如果操作是获取整个doc的数据,那么es会优先从source读取数据。
如果操作是获取某些字段的数据,那么es会优先从store存储中读取数据。因为这样读取的数据量相对较少,无需读取整个doc的数据再解析。
但是注意的是,这两个属性都是单独自己保存数据的,所以如果两个启用的话,相当于数据存储了两次,挺浪费存储空间的,增大了索引的体积

3.3.6 api操作mapping

创建mapping,要注意,mapping创建之后不能更改

@Test
    public void createMapping() throws Exception {

        // 1设置mapping,使用jsonbuilder构建mapping
        XContentBuilder builder = XContentFactory.jsonBuilder()
                .startObject()
                    .startObject("article")
                        .startObject("properties")
                            .startObject("id1")
                                .field("type", "string")
                                .field("store", "yes")
                            .endObject()
                            .startObject("title2")
                                .field("type", "string")
                                .field("store", "no")
                            .endObject()
                            .startObject("content")
                                .field("type", "string")
                                .field("store", "yes")
                            .endObject()
                        .endObject()
                    .endObject()
                .endObject();

        // 2 添加mapping
        PutMappingRequest mapping = Requests.putMappingRequest("blog4").type("article").source(builder);

        client.admin().indices().putMapping(mapping).get();

        // 3 关闭资源
        client.close();
    }

查看map

@Test
    public void getIndexMapping() throws ExecutionException, InterruptedException {
       //构建查看mapping的请求,查看blog3这个index的mapping
        GetMappingsResponse mappingsResponse = client.admin().indices().getMappings(new GetMappingsRequest().indices("blog3")).get();
        //获取mapping
        ImmutableOpenMap> mappings = mappingsResponse.getMappings();
        //迭代打印mapping数据
        for (ObjectObjectCursor> mapping : mappings) {
            if (mapping.value.isEmpty()) {
                continue;
            }
            //最外层的key是index的名称
            System.out.println("index key:" + mapping.key);
            //value包裹的是每个type的mapping,里面以type为key,mapping为value
            for (ObjectObjectCursor mapValue : mapping.value) {
                System.out.println("type key:" + mapValue.key);
                System.out.println("type value:" + mapValue.value.sourceAsMap());

            }
        }
        client.close();
    }

/*
结果如下:
index key:blog3
type key:article
type value:{_source={enabled=false}, properties={id={type=long}, name={type=text, fields={keyword={type=keyword, ignore_above=256}}}}}
*/

3.4 spark操作es时的报错

在spark.2.1和es6.6项目中混合使用,报错:

java.lang.NoSuchMethodError: io.netty.buffer.ByteBuf.retainedSlice(II)Lio/netty/buffer/ByteBuf;

这种问题,一般都是使用的某个依赖包的版本问题。使用mvn dependency:tree 看了下,原来spark和es各自依赖的版本不一致,spark使用的是3.x版本,es使用的是4.1.32.Final版本。但是因为spark的依赖在pom.xml中写在前面,迫使es使用的是3.x版本的依赖,导致有些方法不存在,就报错。解决方式很简答,直接指定使用新版本的就好,如下:


            io.netty
            netty-all
            4.1.32.Final
        

四、分词器

4.1 默认分词器

我们知道,建立索引过程中,最重要的一个步骤就是分词,分词的策略有很多,我们看看es默认的中文分词器的效果

[root@bigdata121 elasticsearch-6.6.2]# curl -H "Content-Type:application/json" -XGET 'http://bigdata121:9200/_analyze?pretty' -d '{"analyzer":"standard","text":"中华人民共和国"}' 
{
  "tokens" : [
    {
      "token" : "中",
      "start_offset" : 0,
      "end_offset" : 1,
      "type" : "",
      "position" : 0
    },
    {
      "token" : "华",
      "start_offset" : 1,
      "end_offset" : 2,
      "type" : "",
      "position" : 1
    },
    {
      "token" : "人",
      "start_offset" : 2,
      "end_offset" : 3,
      "type" : "",
      "position" : 2
    },
    {
      "token" : "民",
      "start_offset" : 3,
      "end_offset" : 4,
      "type" : "",
      "position" : 3
    },
    {
      "token" : "共",
      "start_offset" : 4,
      "end_offset" : 5,
      "type" : "",
      "position" : 4
    },
    {
      "token" : "和",
      "start_offset" : 5,
      "end_offset" : 6,
      "type" : "",
      "position" : 5
    },
    {
      "token" : "国",
      "start_offset" : 6,
      "end_offset" : 7,
      "type" : "",
      "position" : 6
    }
  ]
}

可以看到,标准的中文分词器只是单纯将字分开,其实并不智能,没有词语考虑进去。所以需要更加强大的分词器。常用的有ik分词器

4.2 安装ik分词器

cd /opt/modules/elasticsearch-6.6.2
执行下面的命令安装,需要联网
bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.6.2/elasticsearch-analysis-ik-6.6.2.zip

注意要根据ES的版本安装对应版本的ik

4.3 命令行下IK分词器的使用

分两种模式:ik_smart 和 ik_max_word

1、 ik_smart  模式,智能解析词语结构
curl -H "Content-Type:application/json" -XGET 'http://bigdata121:9200/_analyze?pretty' -d '{"analyzer":"ik_smart","text":"中华人民共和国"}'

{
  "tokens" : [
    {
      "token" : "中华人民共和国",
      "start_offset" : 0,
      "end_offset" : 7,
      "type" : "CN_WORD",
      "position" : 0
    }
  ]
}

2、ik_max_word 模式,智能解析字和词语
curl -H "Content-Type:application/json" -XGET 'http://192.168.109.133:9200/_analyze?pretty' -d '{"analyzer":"ik_max_word","text":"中华人民共和国"}'

{
  "tokens" : [
    {
      "token" : "中华人民共和国",
      "start_offset" : 0,
      "end_offset" : 7,
      "type" : "CN_WORD",
      "position" : 0
    },
    {
      "token" : "中华人民",
      "start_offset" : 0,
      "end_offset" : 4,
      "type" : "CN_WORD",
      "position" : 1
    },
    {
      "token" : "中华",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "CN_WORD",
      "position" : 2
    },
    {
      "token" : "华人",
      "start_offset" : 1,
      "end_offset" : 3,
      "type" : "CN_WORD",
      "position" : 3
    },
    {
      "token" : "人民共和国",
      "start_offset" : 2,
      "end_offset" : 7,
      "type" : "CN_WORD",
      "position" : 4
    },
    {
      "token" : "人民",
      "start_offset" : 2,
      "end_offset" : 4,
      "type" : "CN_WORD",
      "position" : 5
    },
    {
      "token" : "共和国",
      "start_offset" : 4,
      "end_offset" : 7,
      "type" : "CN_WORD",
      "position" : 6
    },
    {
      "token" : "共和",
      "start_offset" : 4,
      "end_offset" : 6,
      "type" : "CN_WORD",
      "position" : 7
    },
    {
      "token" : "国",
      "start_offset" : 6,
      "end_offset" : 7,
      "type" : "CN_CHAR",
      "position" : 8
    }
  ]
}

4.4 java api使用ik分词器

这里其实和mapping的使用差不多,只是在mapping的字段属性中添加一个 “analyzer” 属性,指定使用的分词器而已。其他都没有区别,这里不重复

五、优化

5.1 背景

ES在数十亿级别的数据如何提高检索效率?
​ 这个问题说白了,就是看你有没有实际用过 ES,因为啥?其实 ES 性能并没有你想象中那么好的。很多时候数据量大了,特别是有几亿条数据的时候,可能你会懵逼的发现,跑个搜索怎么一下 5~10s,坑爹了。第一次搜索的时候,是 5~10s,后面反而就快了,可能就几百毫秒。
​ 然后你就很懵,每个用户第一次访问都会比较慢,比较卡么?所以你要是没玩儿过 ES,或者就是自己玩玩儿 Demo,被问到这个问题容易懵逼,显示出你对 ES 确实玩的不怎么样?说实话,ES 性能优化是没有银弹的。啥意思呢?就是不要期待着随手调一个参数,就可以万能的应对所有的性能慢的场景。也许有的场景是你换个参数,或者调整一下语法,就可以搞定,但是绝对不是所有场景都可以这样。
​ 下面看看几个优化的手段

5.2 优化1--filesystemCache

5.2.1 基本原理

一、Elasticsearch原理与基本使用

图5.1 ES filesytem cache

​ 你往 ES 里写的数据,实际上都写到磁盘文件里去了,查询的时候,操作系统会将磁盘文件里的数据自动缓存到 Filesystem Cache 里面去。ES 的搜索引擎严重依赖于底层的 Filesystem Cache,你如果给 Filesystem Cache 更多的内存,尽量让内存可以容纳所有的 IDX Segment File 索引数据文件,那么你搜索的时候就基本都是走内存的,性能会非常高。

问题:直接读取硬盘数据和从缓存读取数据,性能差距究竟可以有多大?
回答:
我们之前很多的测试和压测,如果走磁盘一般肯定上秒,搜索性能绝对是秒级别的,1 秒、5 秒、10 秒。但如果是走 Filesystem Cache,是走纯内存的,那么一般来说性能比走磁盘要高一个数量级,基本上就是毫秒级的,从几毫秒到几百毫秒不等。

案例:
​ 来看一个真实的案例:某个公司 ES 节点有 3 台机器,每台机器看起来内存很多 64G,总内存就是 64 3 = 192G。每台机器给 ES JVM Heap 是 32G,那么剩下来留给 Filesystem Cache 的就是每台机器才 32G,总共集群里给 Filesystem Cache 的就是 32 3 = 96G 内存。
​ 而此时,整个磁盘上索引数据文件,在 3 台机器上一共占用了 1T 的磁盘容量,ES 数据量是 1T,那么每台机器的数据量是 300G。这样性能会好吗?
​ Filesystem Cache 的内存才 100G,十分之一的数据可以放内存,其他的都在磁盘,然后你执行搜索操作,大部分操作都是走磁盘,性能肯定差。

5.2.2 优化具体方式

​ 首先要知道一点:归根结底,你要让 ES 性能好,最佳的情况下,就是你的机器的内存,至少可以容纳你的总数据量的一半。当然如果内存能容纳全部数据,自然是最好,然而基本生产中没有那么多钱的啦。走内存可以满足秒级以内的查询要求

1、去掉写入ES的doc中不必要的字段
如果一个doc中有很多字段,但是有些字段压根是没用的(也就是说该字段不会用于搜索),但是读取的时候仍旧会将这些字段都读取,然后缓存到filesytem cache中,占据了大量空间,导致后面的数据只能重新从硬盘中读取。这个时候就要想着取消一些没怎么用的字段了。减小索引的体积。从而节省filesytem cache空间

2、采用 ES+HBase架构
​ 之前也说到,es可以只存储索引,不存储原始doc数据;或者只存储某些字段的原始数据。通常完整的原始数据都保存在hbase中,然后通过rowkey作为docid导入到es中,最终通过这个rowkey进行唯一性关联。为什么要采用这种架构呢?
​ 比如说你现在有一行数据:id,name,age .... 30 个字段。但是你现在搜索,只需要根据 id,name,age 三个字段来搜索。如果你傻乎乎往 ES 里写入一行数据所有的字段,就会导致 90% 的数据是不用来搜索的。但是呢,这些数据硬是占据了 ES 机器上的 Filesystem Cache 的空间,单条数据的数据量越大,就会导致 Filesystem Cahce 能缓存的数据就越少。其实,仅仅写入 ES 中要用来检索的少数几个字段就可以了,比如说就写入 es id,name,age 三个字段。然后你可以把其他的字段数据存在 MySQL/HBase 里,我们一般是建议用 ES + HBase 这么一个架构(官方建议的方案)。
​ HBase是列式数据库,其特点是适用于海量数据的在线存储,就是对 HBase 可以写入海量数据,但是不要做复杂的搜索,做很简单的一些根据 id 或者范围进行查询的这么一个操作就可以了。hbase非常适合这种简单通过key直接获取数据的应用场景。
​ 例如:从 ES 中根据 name 和 age 去搜索,拿到的结果可能就 20 个 doc id,然后根据 doc id 到 HBase 里去查询每个 doc id 对应的完整的数据,给查出来,再返回给前端。而写入 ES 的数据最好小于等于,或者是略微大于 ES 的 Filesystem Cache 的内存容量。然后你从 ES 检索可能就花费 20ms,然后再根据 ES 返回的 id 去 HBase 里查询,查 20 条数据,可能也就耗费个 30ms。如果你像原来那么玩儿,1T 数据都放 ES,可能会每次查询都是 5~10s,而现在性能就会很高,每次查询就是 50ms。

5.3 优化2--数据预热

​ 从概率上来说,大部分的访问量往往集中小部分的数据上,也就是我们所说的数据热点的情况。数据预热通常就是事先将一些可能有大量访问的数据先通过手动访问让它们提前缓存到cache中,然而后面的用户访问这些数据时,就直接走cache查询了,非常快。而且这些数据因为访问量多,所以还需要保证这些热点数据不要被其他非热点数据加载到cache时,被覆盖掉了。这就需要时常手动访问,加载数据到cache中。
​ 例子:
​ 比如电商,你可以将平时查看最多的一些商品,比如说 iPhone 8,热数据提前后台搞个程序,每隔 1 分钟自己主动访问一次,刷到 Filesystem Cache 里去。
​ 总之,就是对于那些你觉得比较热的、经常会有人访问的数据,最好做一个专门的缓存预热子系统。然后对热数据每隔一段时间,就提前访问一下,让数据进入 Filesystem Cache 里面去。这样下次别人访问的时候,性能一定会好很多。

5.4 优化3--冷热分离

​ 这个也是数据热点的问题。ES 可以做类似于 MySQL 的水平拆分,就是说将大量的访问很少、频率很低的数据,单独写一个索引,然后将访问很频繁的热数据单独写一个索引。最好是将冷数据写入一个索引中,然后热数据写入另外一个索引中,这样可以确保热数据在被预热之后,尽量都让他们留在 Filesystem OS Cache 里,别让冷数据给冲刷掉。
​ 还是来一个例子,假设你有 6 台机器,2 个索引,一个放冷数据,一个放热数据,每个索引 3 个 Shard。3 台机器放热数据 Index,另外 3 台机器放冷数据 Index。这样的话,你大量的时间是在访问热数据 Index,热数据可能就占总数据量的 10%,此时数据量很少,几乎全都保留在 Filesystem Cache 里面了,就可以确保热数据的访问性能是很高的。
​ 但是对于冷数据而言,是在别的 Index 里的,跟热数据 Index 不在相同的机器上,大家互相之间都没什么联系了。如果有人访问冷数据,可能大量数据是在磁盘上的,此时性能差点,就 10% 的人去访问冷数据,90% 的人在访问热数据,也无所谓了。

5.5 优化4--避免关联查询

​ 对于 MySQL,我们经常有一些复杂的关联查询,在 ES 里该怎么玩儿?ES 里面的复杂的关联查询尽量别用,一旦用了性能一般都不太好。最好是先在 Java 系统里就完成关联,将关联好的数据直接写入 ES 中。搜索的时候,就不需要利用 ES 的搜索语法来完成 Join 之类的关联搜索了。

5.6 优化5--document模型设计

​ Document 模型设计是非常重要的,很多操作,不要在搜索的时候才想去执行各种复杂的乱七八糟的操作。
​ ES 能支持的操作就那么多,不要考虑用 ES 做一些它不好操作的事情。如果真的有那种操作,尽量在 Document 模型设计的时候,写入的时候就完成。另外对于一些太复杂的操作,比如 join/nested/parent-child 搜索都要尽量避免,性能都很差的。
​ 总结一句就是说,ES不适合执行复杂查询操作

5.7 优化6--分页性能优化

背景:

    ES 的分页是较坑的,为啥呢?举个例子吧,假如你每页是 10 条数据,你现在要查询第 100 页,实际上是会把每个 Shard 上存储的前 1000 条数据都查到一个协调节点上。如果你有 5 个 Shard,那么就有 5000 条数据,接着协调节点对这 5000 条数据进行一些合并、处理,再获取到最终第 100 页的 10 条数据。
    由于是分布式的,你要查第 100 页的 10 条数据,不可能说从 5 个 Shard,每个 Shard 就查 2 条数据,最后到协调节点合并成 10 条数据吧?你必须得从每个 Shard 都查 1000 条数据过来,然后根据你的需求进行排序、筛选等等操作,最后再次分页,拿到里面第 100 页的数据。
    也就是说,你翻页的时候,翻的越深,每个 Shard 返回的数据就越多,而且协调节点处理的时间越长,非常坑爹。所以用 ES 做分页的时候,你会发现越翻到后面,就越是慢。
    我们之前也是遇到过这个问题,用 ES 作分页,前几页就几十毫秒,翻到 10 页或者几十页的时候,基本上就要 5~10 秒才能查出来一页数据了。

解决方案:

1、不允许深度分页(默认深度分页性能很差)。跟产品经理说,你系统不允许翻那么深的页,默认翻的越深,性能就越差。

2、类似于 App 里的推荐商品不断下拉出来一页一页的;类似于微博中,下拉刷微博,刷出来一页一页的,你可以用 Scroll API,关于如何使用,大家可以自行上网搜索学习一下。
    Scroll是如何做的呢?它会一次性给你生成所有数据的一个快照,然后每次滑动向后翻页就是通过游标 scroll_id 移动,获取下一页、下一页这样子,性能会比上面说的那种分页性能要高很多很多,基本上都是毫秒级的。
    但是,唯一的一点就是,这个适合于那种类似微博下拉翻页的,不能随意跳到任何一页的场景。也就是说,你不能先进入第 10 页,然后去第 120 页,然后又回到第 58 页,不能随意乱跳页。所以现在很多产品,都是不允许你随意翻页的,你只能往下拉,一页一页的翻。
    使用时需要注意,初始化必须指定 Scroll 参数,告诉 ES 要保存此次搜索的上下文多长时间。你需要确保用户不会持续不断翻页翻几个小时,否则可能因为超时而失败。
    除了用 Scroll API,你也可以用 search_after 来做。search_after 的思想是使用前一页的结果来帮助检索下一页的数据。
    显然,这种方式也不允许你随意翻页,你只能一页页往后翻。初始化时,需要使用一个唯一值的字段作为 Sort 字段。

当前文章:一、Elasticsearch原理与基本使用
转载来于:http://www.cdkjz.cn/article/pcgepc.html
多年建站经验

多一份参考,总有益处

联系快上网,免费获得专属《策划方案》及报价

咨询相关问题或预约面谈,可以通过以下方式与我们联系

大客户专线   成都:13518219792   座机:028-86922220