ElasticSearch

ElasticSearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口

优势

安装

使用 docker

docker run elasticsearch:7.3.1
docker network create somenetwork;
docker run -d --name elasticsearch --net somenetwork -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" elasticsearch:7.3.1

9300端口: ES节点之间通讯使用 9200端口: ES节点 和 外部 通讯使用

图形化管理界面

概念

批注 2020-03-19 081056

索引结构

批注 2019-10-18 145410

操作

创建索引

PUT /blog
{
    "settings": {
        "number_of_shards": 3,
        "number_of_replicas": 2
      }
}

GET /blog

DELETE /blog

添加映射

PUT /索引库名/_mapping/类型名称
{
  "properties": {
    "字段名": {
      "type": "类型",
      "index": true,
      "store": true,
      "analyzer": "分词器"
    }
  }
}

数据类型

text:该类型被用来索引长文本,在创建索引前会将这些文本进行分词,转化为词的组合,建立索引;允许es来检索这些词,text类型不能用来排序和聚合。 keyword:该类型不需要进行分词,可以被用来检索过滤、排序和聚合,keyword类型自读那只能用本身来进行检索(不可用text分词后的模糊检索) 数值型:long、integer、short、byte、double、float 日期型:date 布尔型:boolean 二进制型:binary

GET /索引库名/_mapping

POST http://my-pc:9200/blog/{indexName}

添加文档

POST /索引库名/类型名
{
    "key":"value"
}
POST /索引库名/类型/id值
{...}

删除文档

DELETE http://my-pc:9200/blog/hello/1

修改文档

UPDATE http://my-pc:9200/blog/hello/1

查询

基本查询

GET /索引库名/_search
{
    "query":{
        "查询类型":{
            "查询条件":"查询条件值"
        }
    }
}

GET http://my-pc:9200/blog/hello/1

Term Query为精确查询,在搜索时会整体匹配关键字,不再将关键字分词。

GET /shop/_search
{
  "_source": ["title","price"],
  "query": {
    "term": {
      "price": 2699
    }
  }
}
{
    "query":{
        "query_string":{
            "default_field":"content",
            "query":"内容"
        }
    }
}

过滤

GET /shop/_search
{
  "_source": {
    "includes":["title","price"]
  },
  "query": {
    "term": {
      "price": 2699
    }
  }
}

排序

GET /shop/_search
{
  ...
  "sort": [
    {
      "price": {
        "order": "desc"
      }
    }
  ]
}

模糊查询

GET /heima/_search
{
  "query": {
    "fuzzy": {
        "title": {
            "value":"appla",
            "fuzziness":1
        }
    }
  }
}

分词

内置的分词器

测试分词

GET /_analyze

{
  "analyzer": "standard",
  "text": "中文测试分词"
}

中文分词器

下载

docker run --name elasticsearch --net somenetwork -v /root/plugin:/usr/share/elasticsearch/plugins -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" elasticsearch:7.3.1

GET http://my-pc:9200/_analyze

{
  "analyzer": "ik_max_word",
  "text": "中文测试分词"
}

ik 的两种模式:

聚合

ES集群

采用ES集群,将单个索引的分片到多个不同分布式物理机器上存储,从而可以实现高可用、容错性

架构

es 集群多个节点,会自动选举一个节点为 master 节点 master 节点宕机了,那么会重新选举一个节点为 master 节点

非 master节点宕机了,那么会由 master 节点,让那个宕机节点上的 primary shard 的身份转移到其他机器上的 replica shard

批注 2020-03-19 081559

可以使用三个节点,将索引分成三份,每个节点存放一份primary shard,两份replica,这样就算只剩下一台节点,也能保证服务可用

搭建

# 集群名称,必须保持一致
cluster.name:  elasticsearch
# 节点的名称
node.name: node-1
# 监听网段
network.host: 0.0.0.0
# 本节点rest服务端口
http.port: 9201
# 本节点数据传输端口
transport.tcp.port: 9301
# 集群节点信息
discovery.seed_hosts: ["127.0.0.1:9301","127.0.0.1:9302","127.0.0.1:9303"]
cluster.initial_master_nodes: ["node-1","node-2","node-3"]

另外两个节点配置省略...

JAVA客户端

<dependency>
    <groupId>org.elasticsearch</groupId>
    <artifactId>elasticsearch</artifactId>
    <version>7.3.1</version>
</dependency>

<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>transport</artifactId>
    <version>7.3.1</version>
</dependency>
Settings settings = Settings.builder()
                .put("cluster.name","docker-cluster")
                .build();

TransportClient client = new PreBuiltTransportClient(settings);
client.addTransportAddress(
            new TransportAddress(InetAddress.getByName("my-pc"),9300));
client.admin().indices().prepareCreate("index").get();
XContentBuilder builder = XContentFactory.jsonBuilder()
                .startObject()
                .startObject("article")
                .startObject("properties")
                .startObject("id")
                .field("type", "long")
                .field("store", true)
                .endObject()
                .startObject("title")
                .field("type", "text")
                .field("store", true)
                .field("analyzer", "ik_smart")
                .endObject()
                .startObject("content")
                .field("type", "text")
                .field("store", true)
                .field("analyzer", "ik_smart")
                .endObject()
                .endObject()
                .endObject()
                .endObject();
  client.admin().indices().preparePutMapping("index")
          .setType("article")
          .setSource(builder)
          .get();
XContentBuilder builder = XContentFactory.jsonBuilder()
                .startObject()
                    .field("id",1L)
                    .field("title","央视快评:勇做敢于斗争善于斗争的战士")
                    .field("content","9月3日,习近平总书记在中央党校(国家行政学院)中青年干部培训班开班式上发表重要讲话强调,广大干部特别是年轻干部要经受严格的思想淬炼、政治历练、实践锻炼,发扬斗争精神,增强斗争本领,为实现“两个一百年”奋斗目标、实现中华民族伟大复兴的中国梦而顽强奋斗。")
                .endObject();
client.prepareIndex("index","article","1")
        .setSource(builder)
        .get();
Article article = new Article();
        article.setId(3L);
        article.setTitle("3央视快评:勇做敢于斗争善于斗争的战士");
        article.setContent("9月3日3333,(国家行政学院)中青年干部培训班开班式上发表重要讲话强调,广大干部特别是年");
        String json = new ObjectMapper().writeValueAsString(article);

client.prepareIndex("index","article","3")
        .setSource(json, XContentType.JSON)
        .get();

查询

QueryBuilder queryBuilder = QueryBuilders.idsQuery().addIds("1","2");
SearchResponse response = client.prepareSearch("index")
        .setTypes("article")
        .setQuery(queryBuilder)
        .get();
SearchHits hits = response.getHits();

System.out.println("总记录:"+hits);
SearchHit[] ret = hits.getHits();

for (SearchHit documentFields : ret) {
    Map<String, Object> map = documentFields.getSourceAsMap();
    System.out.println("id:"+map.get("id"));
    System.out.println("title:"+map.get("title"));
    System.out.println("content:"+map.get("content"));
    System.out.println("-------------------");
}
QueryBuilder queryBuilder = QueryBuilders.termQuery("title","斗争");
QueryBuilder queryBuilder = QueryBuilders.queryStringQuery("青年强调")
                .defaultField("content");
SearchResponse response = client.prepareSearch("index")
                .setTypes("article")
                .setQuery(queryBuilder)
                .setFrom(10)
                .setSize(5)
                .get();
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field(highlight);
highlightBuilder.preTags("<em>");
highlightBuilder.postTags("</em>");

SearchResponse response = client.prepareSearch("index")
        .setTypes("article")
        .setQuery(queryBuilder)
        .highlighter(highlightBuilder)
        .get();
SearchHits hits = response.getHits();

System.out.println("总记录:"+hits.getTotalHits());
SearchHit[] ret = hits.getHits();

for (SearchHit documentFields : ret) {
    Map<String, Object> map = documentFields.getSourceAsMap();

    System.out.println("id:"+map.get("id"));
    System.out.println("content:"+map.get("content"));
    Map<String, HighlightField> highlightFields = documentFields.getHighlightFields();
    System.out.println(highlightFields.get(highlight).getFragments()[0]);
    System.out.println("-------------------");

}

es操作过程

写过程

客户端选择一个协调节点(coordinating node)发送请求,协调节点将请求转发给对应的node 对应的node在primary shard上处理请求,并同步到replica shard上

批注 2020-03-19 082208

写过程原理

批注 2020-03-19 083304

数据先写入内存 buffer,然后每隔 1s,将数据 refresh 到 os cache,到了 os cache 数据就能被搜索到

每隔 5s,将数据写入 translog 文件(这样如果机器宕机,内存数据全没,最多会有 5s 的数据丢失),translog 大到一定程度,或者默认每隔 30mins,会触发 commit 操作,将缓冲区的数据都 flush 到 segment file 磁盘文件中

读过程

客户端选择一个协调节点(coordinating node)发送根据ID查询请求,协调节点会根据id进行哈希,得到doc所在的分片,将请求转发到对应的node 这个node然后会在primary shard与replica中使用随机轮询,进行负载均衡,返回document给协调节点 协调节点再把document返回给客户端

搜索过程

客户端发送搜索请求给协调节点,协调节点将这个请求发送给所有的shard 每个shard将自己的搜索结构返回给协调节点 由协调节点进行数据的合并、排序、分页等操作,产出最终结果 接着协调节点根据id再去查询对应的document的数据,返回给客户端

删除/更新过程

删除操作,会生成一个对应document id的.del文件,标识这个document被删除 如果是更新操作,就是将原来的 doc 标识为 deleted 状态,然后新写入一条数据

每refresh一次,会生成一个segment file,系统会定期合并这些文件,合并这些文件的时候,会物理删除标记.del的document

性能优化

杀手锏:filesystem cache

批注 2020-03-19 085001

在es中,doc的字段尽量只存储要被搜索的字段,这样可以节省内存,存放更多数据,做缓存效果更好

数据预热

对于一些热点数据,也要通过一些方式让它在缓存中

冷热分离

保证热点数据都在缓存里,提高系统性能

doc模型设计

对于一些复杂的关联,最好在应用层面就做好,对于一些太复杂的操作,比如 join/nested/parent-child 搜索都要尽量避免,性能都很差的

分页性能优化

由于分页操作是由协调节点来完成的,所以翻页越深,性能越差 解决:

kibana

Kibana是一个基于Node.js的Elasticsearch索引库数据统计工具,可以利用Elasticsearch的聚合功能,生成各种图表,如柱形图,线状图,饼图等。

而且还提供了操作Elasticsearch索引数据的控制台,并且提供了一定的API提示,非常有利于我们学习Elasticsearch的语法。

docker pull kibana:5.6.8 # 拉取镜像
docker run -d --name kibana --net somenetwork -p 5601:5601 kibana:5.6.8 # 启动

SpringDataElasticSearch

配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:jpa="http://www.springframework.org/schema/data/jpa"
       xmlns:elasticsearch="http://www.springframework.org/schema/data/elasticsearch"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
     https://www.springframework.org/schema/beans/spring-beans.xsd
     http://www.springframework.org/schema/data/jpa
     https://www.springframework.org/schema/data/jpa/spring-jpa.xsd http://www.springframework.org/schema/data/elasticsearch http://www.springframework.org/schema/data/elasticsearch/spring-elasticsearch.xsd">

    <elasticsearch:transport-client id="esClient" cluster-name="docker-elasticsearch"
                                    cluster-nodes="my-pc:9300"/>
    <elasticsearch:repositories base-package="wang.ismy.es"/>
    <bean id="elasticsearchTemplate" class="org.springframework.data.elasticsearch.core.ElasticsearchTemplate">
        <constructor-arg name="client" ref="esClient"/>
    </bean>

</beans>
@Document(indexName = "index1",type = "article")
@Data
public class Article {

    @Id
    @Field(type = FieldType.Long,store = true)
    private long id;

    @Field(type = FieldType.Text,store = true)
    private String title;

    @Field(type = FieldType.Text,store = true)
    private String content;

}
@Repository
public interface ArticleDao extends ElasticsearchRepository<Article,Long> { }

创建索引

ElasticsearchTemplate template = context.getBean(ElasticsearchTemplate.class);
template.createIndex(Article.class);

添加文档

Article article = new Article();
article.setId(1L);
article.setTitle("【中国稳健前行】“中国之治”的政治保证");
article.setContent("新中国成立70年来,在中国共产党的坚强领导下,...");
articleDao.save(article);

删除文档

articleDao.deleteById(1L);
articleDao.deleteAll(); // 全部删除

修改文档

同添加文档

查询

articleDao.findAll().forEach(System.out::println);
System.out.println(articleDao.findById(2L).get());

自定义查询

@Repository
public interface ArticleDao extends ElasticsearchRepository<Article,Long> {

    List<Article> findAllByTitle(String title);
}
List<Article> findAllByTitle(String title, Pageable pageable);
articleDao.findAllByTitle("中", PageRequest.of(0,5)).forEach(System.out::println);
NativeSearchQuery query = new NativeSearchQueryBuilder()
        .withQuery(QueryBuilders.queryStringQuery("中国").defaultField("title"))
        .withPageable(PageRequest.of(0,5))
        .build();
template.queryForList(query,Article.class).forEach(System.out::println);