ElasticSearch

315 阅读11分钟

ElasticSearch是什么

todo

部署

压缩包部署

略,大概就是copy 相同的es文件 修改elasticsearch.yml内的node_name

docker-compose部署

version: '3'
services:
  elasticsearch:
    ##image:tag image为hub.docker.com上镜像名称  tag为版本号  不写tag时默认拉取最新镜像
    image: elasticsearch:7.9.3
    container_name: es
    ##加载的环境变量
    environment: 
         ##集群名称
      - cluster.name=my-application
      - node.name=node
      - bootstrap.memory_lock=true
      - http.cors.enabled=true
      - http.cors.allow-origin=*
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
      - "cluster.initial_master_nodes=node,node1,node2" ##集群初始化master节点 
      - "discovery.seed_hosts=elasticsearch,elasticsearch1,elasticsearch2" 
    ulimits:
      memlock:
        soft: -1
        hard: -1
    ##宿主机与容器的端口映射,端口需要暴露出来
    ports:
      - 9200:9200 
      - 9300:9300
    networks:
      - esnet
    ##数据卷映射可以多个映射   左侧es_data为别名  右侧为容器内地址
    ## es_data具体通过 docker volume inspect ${user}_es_data  
    ## ${user}为创建容器时账户 eg:docker volume inspect root_es_data
    volumes: 
      - es_data:/usr/share/elasticsearch/data
      - es_plugin:/usr/share/elasticsearch/plugins
  elasticsearch1:
    image: elasticsearch:7.9.3
    container_name: es1
    environment:
      - cluster.name=my-application
      - node.name=node1
      - bootstrap.memory_lock=true
      - http.cors.enabled=true
      - http.cors.allow-origin=*
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
      - "cluster.initial_master_nodes=node,node1,node2"
      - "discovery.seed_hosts=elasticsearch,elasticsearch1,elasticsearch2"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    ports:
      - 9201:9200
      - 9301:9300
    networks:
      - esnet
    volumes:
      - es1_data:/usr/share/elasticsearch/data    
      - es_plugin:/usr/share/elasticsearch/plugins
  elasticsearch2:
    image: elasticsearch:7.9.3
    container_name: es2
    environment:
      - cluster.name=my-application
      - node.name=node2
      - bootstrap.memory_lock=true
      - http.cors.enabled=true
      - http.cors.allow-origin=*
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
      - "cluster.initial_master_nodes=node,node1,node2"
      - "discovery.seed_hosts=elasticsearch,elasticsearch1,elasticsearch2"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    ports:
      - 9202:9200
      - 9302:9300
    networks:
      - esnet
    volumes:
      - es2_data:/usr/share/elasticsearch/data
      - es_plugin:/usr/share/elasticsearch/plugins
  kibana:
    image: kibana:7.9.3
    container_name: kibana
    environment:
      - ELASTICSEARCH_URL=http://elasticsearch:9200
    ports:
      - "5601:5601"
    networks:
      - esnet
volumes:
  es_data:
    driver: local
  es1_data:
    driver: local
  es2_data:
    driver: local
  es_plugin:
    driver: local
networks: ##容器之间通信网络 局域网的概念
  esnet:

ElasticSearch架构原理

基本概念

索引

  • 将同一类数据放到一起的集合 相当于传统关系型数据库中的库

类型

  • 7.0已废弃 默认type为_doc 8.0版本正式剔除 相当于传统关系型数据库中的表

文档/doc

  • 即一行数据为一个doc

字段/field

  • doc中某个字段

shard

索引被分成若干primary shard 即数据被几等分 分布在不同node
每个primary shard有几个副本即replica shard;
primary shard与其replica shard不会出现在同一个node 为了满足分区容错性
数据会被存入哪一个shard 根据hash(routing) % number_of_primary_shards  routing默认是_id
由于一致性hash问题所以shard个数定好之后不可能增加修改

读写流程

写请求:

  • 请求任意节点(协调节点) 协调节点根据id路由到指定primary shard-->primary shard写完后分发给自己的replica shard->replica shard写完后回传信息给自己的primary shard->primary shard收到所有写完信号后通知协调节点

读请求:

  • 请求任意节点(协调节点) 无id可以路由时分发请求到所有shard,将所有shard返回的数据放到一起(结果集仅仅包含docId和所有排序的字段值) 而后确定需要返回给client的docId 再去这些docId对应的shard上取回真正的数据返回

倒排索引

将一句话利用分词器分成若干字或词,每个字词分布在哪些doc

正排索引是指文档ID为key,表中记录每个关键词出现的次数,查找时扫描表中的每个文档中字的信息,直到找到所有包含查询关键字的文档

docA 内容 智器云大数据,大数据时代的福尔摩斯
docb 内容 大数据中心
此时的索引为 
docA_id  智器云 1(出现次数)
         大数据 2
         ...
docB_id  大数据 1
         中心 1
检索 关键字 智器云时   查询所有doc找到含有智器云的文档docA
检索 关键字 大数据时   查询所有doc找到含有智器云的文档docA,docB
docA 内容 智器云大数据,大数据时代的福尔摩斯
docb 内容 大数据中心
此时的索引为 
智器云 docA_id,docB_id
大数据 docA_id,docB_id
中心  docB_id
...
检索 关键字 智器云时   直接返回docA_id
检索 关键字 大数据时  直接返回docA_id,docB_id
检索 关键字 中心时   直接返回docB_id

分词器

索引时使用 ik_max_word(细粒度),在搜索时用ik_smart(粗粒度)

ik_max_word会将文本做最细粒度的拆分,比如会将“中华人民共和国人民大会堂”拆分为“中华人民共和国、中华人民、中华、华人、人民共和国、人民、共和国、大会堂、大会、会堂等词语。

ik_smart会做最粗粒度的拆分,比如会将“中华人民共和国人民大会堂”拆分为中华人民共和国、人民大会堂。

pinyin

keep_first_letter启用此选项时,例如:刘德华> ldh,默认值:true
keep_separate_first_letter启用该选项时,将保留第一个字母分开,例如:刘德华> l,d,h,默认:false
limit_first_letter_length 设置first_letter结果的最大长度,默认值:16
keep_full_pinyin当启用该选项,例如:刘德华> [ liu,de,hua],默认值:true
keep_joined_full_pinyin当启用此选项时,例如:刘德华> [ liudehua],默认值:false
keep_none_chinese 在结果中保留非中文字母或数字,默认值:true
keep_none_chinese_together保持非中国信一起,默认值:true,如:DJ音乐家- > DJ,yin,yue,jia,当设置为false,例如:DJ音乐家- > D,J,yin,yue,jia,注意:keep_none_chinese必须先启动
keep_none_chinese_in_first_letter第一个字母保持非中文字母,例如:刘德华AT2016- > ldhat2016,默认值:true
keep_none_chinese_in_joined_full_pinyin保留非中文字母加入完整拼音,例如:刘德华2016- > liudehua2016,默认:false
none_chinese_pinyin_tokenize打破非中国信成单独的拼音项,如果他们拼音,默认值:true,如:liudehuaalibaba13zhuanghan- > liu,de,hua,a,li,ba,ba,13,zhuang,han,注意:keep_none_chinese和keep_none_chinese_together应首先启用
keep_original 当启用此选项时,也会保留原始输入,默认值:false
lowercase 小写非中文字母,默认值:true
trim_whitespace 默认值:true
remove_duplicated_term当启用此选项时,将删除重复项以保存索引,例如:de的> de,默认值:false,注意:位置相关查询可能受影响

setting

{
    "settings":{
        "analysis":{
            "char_filter": {},//分词前,字符过滤器
            "tokenizer": {},//自定义分词器,对已有分词器进行属性参数上的更改
            "filter": {},//分词后,分词过滤器
            "analyzer":{}//定制化分析器,结合上述3个组装出自己的分词器
        }
    }
}
{
	"settings": {
		"index.max_ngram_diff": 20,
		"number_of_shards": "5",
		"number_of_replicas": "2",
		"analysis": {
			"analyzer": {
				"pinyin_analyzer": {
					"tokenizer": "standard_pinyin"
				},
				"first_py_letter_analyzer": {
					"tokenizer": "first_py_letter"
				},
				"full_pinyin_letter_analyzer": {
					"tokenizer": "full_pinyin_letter"
				}
			},
			"tokenizer": {
				"standard_pinyin": {
					"keep_joined_full_pinyin": "true",
					"keep_first_letter": "true",
					"keep_separate_first_letter": "false",
					"lowercase": "true",
					"type": "pinyin",
					"limit_first_letter_length": "16",
					"keep_original": "true",
					"keep_full_pinyin": "true",
					"keep_none_chinese_in_joined_full_pinyin": "true"
				},
				"first_py_letter": {
					"type": "pinyin",
					"keep_first_letter": true,
					"keep_full_pinyin": false,
					"keep_original": false,
					"limit_first_letter_length": 16,
					"lowercase": true,
					"trim_whitespace": true,
					"keep_none_chinese_in_first_letter": false,
					"none_chinese_pinyin_tokenize": false,
					"keep_none_chinese": true,
					"keep_none_chinese_in_joined_full_pinyin": true
				},
				"full_pinyin_letter": {
					"type": "pinyin",
					"keep_separate_first_letter": false,
					"keep_full_pinyin": false,
					"keep_original": false,
					"limit_first_letter_length": 16,
					"lowercase": true,
					"keep_first_letter": false,
					"keep_none_chinese_in_first_letter": false,
					"none_chinese_pinyin_tokenize": false,
					"keep_none_chinese": true,
					"keep_joined_full_pinyin": true,
					"keep_none_chinese_in_joined_full_pinyin": true
				}
			}
		}
	}
}
索引静态配置
△△△△index.number_of_shards 索引分片的数量。在ES层面可以通过es.index.max_number_of_shards属性设置索引最大的分片数,默认为1024,index.number_of_shards的默认值为Math.min(es.index.max_number_of_shards,5),故通常默认值为5。
index.shard.check_on_startup 分片在打开之前是否应该检查该分片是否损坏。当检测到损坏时,它将阻止分片被打开。可选值:false:不检测;checksum:只检查物理结构;true:检查物理和逻辑损坏,相对比较耗CPU;fix:类同与false,7.0版本后将废弃。默认值:false。
index.codec 数据存储的压缩算法,默认值为LZ4,可选择值best_compression ,比LZ4可以获得更好的压缩比(即占据较小的磁盘空间,但存储性能比LZ4低)。
index.routing_partition_size 路由分区数,如果设置了该参数,其路由算法为:(hash(_routing) + hash(_id) % index.routing_parttion_size ) % number_of_shards。如果该值不设置,则路由算法为 hash(_routing) % number_of_shardings,_routing默认值为_id。
索引动态配置
△△△△index.number_of_replicas 索引复制分片的个数,默认值1,该值必须大于等于0,索引创建后该值可以变更。
index.auto_expand_replicas 副本数是否自动扩展,可设置(e.g0-5)或(0-all)。
index.refresh_interval 执行刷新操作的频率,该操作使对索引的最新更改对搜索可见。默认为1s。可以设置为-1以禁用刷新。
index.max_result_window 控制分页搜索总记录数,from + size的大小不能超过该值,默认为10000。
index.max_inner_result_window 从from+ size的最大值,用于控制top aggregations,默认为100。内部命中和顶部命中聚合占用堆内存,并且时间与 from + size成正比,这限制了内存。
index.max_rescore_window 在rescore的搜索中,rescore请求的window_size的最大值。
index.max_docvalue_fields_search 一次查询最多包含开启doc_values字段的个数,默认为100。
index.max_script_fields 查询中允许的最大script_fields数量。默认为32。
index.max_ngram_diff NGramTokenizer和NGramTokenFilter的min_gram和max_gram之间允许的最大差异。默认为1。
index.max_shingle_diff 对于ShingleTokenFilter, max_shingle_size和min_shingle_size之间允许的最大差异。默认为3。
index.blocks.read_only 索引数据、索引元数据是否只读,如果设置为true,则不能修改索引数据,也不能修改索引元数据。
index.blocks.read_only_allow_delete 与index.blocks.read_only基本类似,唯一的区别是允许删除动作。
index.blocks.read 设置为true以禁用对索引数据的读取操作。
index.blocks.write 设置为true以禁用对索引数据的写操作。(针对索引数据,而不是索引元数据)
index.blocks.metadata 设置为true,表示不允许对索引元数据进行读与写。
index.max_refresh_listeners 索引的每个分片上当刷新索引时最大的可用监听器数量。这些侦听器用于实现refresh=wait_for。
index.highlight.max_analyzed_offset 高亮显示请求分析的最大字符数。此设置仅适用于在没有偏移量或term vectors的文本字段时。默认情况下,该设置在6中未设置。x,默认值为-1。
index.max_terms_count 可以在terms查询中使用的术语的最大数量。默认为65536。
index.routing.allocation.enable Allocation机制,其主要解决的是如何将索引在ES集群中在哪些节点上分配分片(例如在Node1是创建的主分片,在其他节点上创建复制分片)。举个例子,如果集群中新增加了一个节点,集群的节点由原来的3个变成了4 可选值:
all 所有类型的分片都可以重新分配,默认。
primaries 只允许分配主分片。
new_primaries 只允许分配新创建的主分片。
none 所有的分片都不允许分配。
index.routing.rebalance.enable 索引的分片重新平衡机制。可选值如下:
all 默认值,允许对所有分片进行再平衡。
primaries 只允许对主分片进行再平衡。
replicas 只允许对复制分片进行再平衡。
none 不允许对任何分片进行再平衡
index.gc_deletes 文档删除后(删除后版本号)还可以存活的周期,默认为60s。
index.max_regex_length 用于正在表达式查询(regex query)正在表达式长度,默认为1000。
index.default_pipeline 默认的管道聚合器。

mapping

{
	"mappings": {
		"properties": {
			"field1": {
				"type": "keyword"
			},
			"field2": {
				"type": "text"
			},
			"field3": {
				"type": "text",
				"analyzer": "ik_max_word",
				"search_analyzer": "ik_smart"
			}
		}
	}
}

选举

使用zk选举

言简意赅就是有资格成为master的节点去zk上创建指定路径,成功当选master失败则watch这个路径

自带的选举

脑裂:可能出现单个node多次选举多个有效master致使集群存在多个master 以下选举为单个node选择出自己的master

ElasticSearch.class=>Command.class: main方法启动
Command.class=>EnvironmentAwareCommand.class: execute()
EnvironmentAwareCommand.class=>ElasticSearch.class: execute()
ElasticSearch.class=>Bootstrap.class: init()
Bootstrap.class=>Node.class: start()
// Node.class
public Node start() throws NodeValidationException {
        ...
        ...
        discovery.start(); 
        ...
        ...
        discovery.startInitialJoin();
        ...
        ...

discovery.start()

protected void doStart() {
       DiscoveryNode localNode = transportService.getLocalNode();
       assert localNode != null;
       synchronized (stateMutex) {
           // set initial state
           assert committedState.get() == null;
           assert localNode != null;
           ClusterState.Builder builder = ClusterState.builder(clusterName);
           ClusterState initialState = builder
               .blocks(ClusterBlocks.builder()
                   .addGlobalBlock(STATE_NOT_RECOVERED_BLOCK)
                   .addGlobalBlock(noMasterBlockService.getNoMasterBlock()))
               .nodes(DiscoveryNodes.builder().add(localNode).localNodeId(localNode.getId()))
               .build();
           committedState.set(initialState);
           clusterApplier.setInitialState(initialState);
           nodesFD.setLocalNode(localNode);
           joinThreadControl.start();//**running.set(true)**
       }
       zenPing.start();
   }

discovery.startInitialJoin();

//ZenDiscovery.class
public void startNewThreadIfNotRunning() {
           assert Thread.holdsLock(stateMutex);
           if (joinThreadActive()) {
               return;
           }
           threadPool.generic().execute(new Runnable() {
               @Override
               public void run() {
                   Thread currentThread = Thread.currentThread();
                   if (!currentJoinThread.compareAndSet(null, currentThread)) {
                       return;
                   }
                   //自旋直到上面设置成true
                   while (running.get() && joinThreadActive(currentThread)) {
                       try {
                           innerJoinCluster();
                           return;
                       } catch (Exception e) {
                           logger.error("unexpected error while joining cluster, trying again", e);
                           // Because we catch any exception here, we want to know in
                           // tests if an uncaught exception got to this point and the test infra uncaught exception
                           // leak detection can catch this. In practise no uncaught exception should leak
                           assert ExceptionsHelper.reThrowIfNotNull(e);
                       }
                   }
                   // cleaning the current thread from currentJoinThread is done by explicit calls.
               }
           });
       }

innerJoinCluster();

private void innerJoinCluster() {
        DiscoveryNode masterNode = null;
        final Thread currentThread = Thread.currentThread();
        nodeJoinController.startElectionContext();
        while (masterNode == null && joinThreadControl.joinThreadActive(currentThread)) {
            masterNode = findMaster();
        }

        if (!joinThreadControl.joinThreadActive(currentThread)) {
            logger.trace("thread is no longer in currentJoinThread. Stopping.");
            return;
        }
        //如果选举结果是自己
        if (transportService.getLocalNode().equals(masterNode)) {
        //半数以上结果时即可成为集群master requiredJoins为还需几个节点join
            final int requiredJoins = Math.max(0, electMaster.minimumMasterNodes() - 1); // we count as one
            logger.debug("elected as master, waiting for incoming joins ([{}] needed)", requiredJoins);
            //等待其他node返回选举结果
            nodeJoinController.waitToBeElectedAsMaster(requiredJoins, masterElectionWaitForJoinsTimeout,
                    new NodeJoinController.ElectionCallback() {
                        @Override
                        public void onElectedAsMaster(ClusterState state) {
                            synchronized (stateMutex) {
                                joinThreadControl.markThreadAsDone(currentThread);
                            }
                        }

                        @Override
                        public void onFailure(Throwable t) {
                            logger.trace("failed while waiting for nodes to join, rejoining", t);
                            synchronized (stateMutex) {
                                joinThreadControl.markThreadAsDoneAndStartNew(currentThread);
                            }
                        }
                    }

            );
           
        } else {//选举的master不是自己时  向选举的node发送join投靠信息
            // process any incoming joins (they will fail because we are not the master)
            nodeJoinController.stopElectionContext(masterNode + " elected");

            // send join request
            final boolean success = joinElectedMaster(masterNode);

            synchronized (stateMutex) {
                if (success) {
                    DiscoveryNode currentMasterNode = this.clusterState().getNodes().getMasterNode();
                    if (currentMasterNode == null) {
                        // Post 1.3.0, the master should publish a new cluster state before acking our join request. we now should have
                        // a valid master.
                        logger.debug("no master node is set, despite of join request completing. retrying pings.");
                        joinThreadControl.markThreadAsDoneAndStartNew(currentThread);
                    } else if (currentMasterNode.equals(masterNode) == false) {
                        // update cluster state
                        joinThreadControl.stopRunningThreadAndRejoin("master_switched_while_finalizing_join");
                    }
                    joinThreadControl.markThreadAsDone(currentThread);
                } else {
                    // 如果当前节点所选择的master节点,无法成为集群master时(自己没选举自己,选择其他node当选master)
                    //重新选举master
                    joinThreadControl.markThreadAsDoneAndStartNew(currentThread);
                }
            }
        }
    }

findMaster();

//ZenDiscovery.class
private DiscoveryNode findMaster() {
        //通过ping 其他节点来判定本节点能够连接上的节点的个数,yml配置的discovery.seed_hosts获取ping列表
        List<ZenPing.PingResponse> fullPingResponses = pingAndWait(pingTimeout).toList();
        if (fullPingResponses == null) {
            return null;
        }
        //当前节点
        final DiscoveryNode localNode = transportService.getLocalNode();
        fullPingResponses.add(new ZenPing.PingResponse(localNode, null, this.clusterState()));
        //根据配置是否剔除非master节点过滤   配置文件true|false
        final List<ZenPing.PingResponse> pingResponses = filterPingResponses(fullPingResponses, masterElectionIgnoreNonMasters, logger);
        //集群中已存在的master节点,activeMasters中去掉自己 因为ping自己无意义
        List<DiscoveryNode> activeMasters = new ArrayList<>();
        for (ZenPing.PingResponse pingResponse : pingResponses) {
            if (pingResponse.master() != null && !localNode.equals(pingResponse.master())) {
                activeMasters.add(pingResponse.master());
            }
        }
        //配置 node.matser=true 即有资格成为master   备胎
        List<ElectMasterService.MasterCandidate> masterCandidates = new ArrayList<>();
        for (ZenPing.PingResponse pingResponse : pingResponses) {
            if (pingResponse.node().isMasterNode()) {
                masterCandidates.add(new ElectMasterService.MasterCandidate(pingResponse.node(), pingResponse.getClusterStateVersion()));
            }
        }
        //master节点为空 则让备胎竞选
        if (activeMasters.isEmpty()) {
        	//是否需要满足min_master_nodes配置 最少得几个master
            if (electMaster.hasEnoughCandidates(masterCandidates)) {
                final ElectMasterService.MasterCandidate winner = electMaster.electMaster(masterCandidates);
                //此时master还可能是自己 因为是其他node的master
                return winner.getNode();
            } else {
                return null;
            }
        } else {
        //不然则选择activeMasters中nodeID最小的master,此时master不可能有自己
            return electMaster.tieBreakActiveMasters(activeMasters);
        }
    }

与传统关系型数据对比

todo

使用场景

todo

怎么使用

sql

局限性 只能返回数据,像score,heightlight都没有 跟真正的关系型数据返回无异 image.png

dsl

  • match 将搜索词分词查询
  • match_phrase 搜索词顺序一致,间隔为slop
  • match_phrase_prefix
  • operator 将分词条件用and连接 默认为or
  • boost 检索权重 跟分数相关
  • minmum_should_match 搜索词相关度
  • slop 查询词条能够相隔多远时仍然将文档视为匹配 比如查询ab slop=1时a@b也能满足条件返回 todo

spring-data-elasticsearch

todo

highRestClient

todo