【数据库篇】Elasticsearch知识总结

在公司需要用到es,github.com/olivere/elastic 的sdk,这里做下知识点的记录吧

es好文 https://mp.weixin.qq.com/s/j7YdtmyuzBFRK1BViDtp2w

基本概念

集群解决的问题

基本上所有的单机问题都有

  • 并发压力
  • 响应时间
  • 磁盘空间问题
  • 宕机

Elasticsearch 信息存储机制

  • 分片
    Elasticsearch 中一个索引(Index)相当于是一个数据库,它是被分片存储的,Elasticsearch 默认会把一个索引分成五个分片,当然这个数字是可以自定义的。

    分片是数据的容器,数据保存在分片内,分片又被分配到集群内的各个节点里。当你的集群规模扩大或者缩小时, Elasticsearch 会自动的在各节点中迁移分片,使得数据仍然均匀分布在集群里,所以相当于一份数据被分成了多份并保存在不同的主机上。

  • 数据备份
    Elasticsearch 默认会生成默认五个原分片和五个分片副本(可以自定义),相当于一份数据存了两份,并分了十个分片。

    只需要将某个分片的副本存在另外一台主机上,这样当某台主机宕机了,可以从另外一台主机的副本中找到对应的数据。

  • 返回

    ES会将搜索在各个节点的结果聚合返回 如下图
    在这里插入图片描述
    一旦有多个节点挂掉,只剩一台就不会提供服务

健康状态

针对一个索引,Elasticsearch 中其实有专门的衡量索引健康状况的标志,分为三个等级:

green,绿色。这代表所有的主分片和副本分片都已分配。你的集群是 100% 可用的。

yellow,黄色。所有的主分片已经分片了,但至少还有一个副本是缺失的。不会有数据丢失,所以搜索结果依然是完整的。不过,你的高可用性在某种程度上被弱化。如果更多的分片消失,你就会丢数据了。所以可把 yellow 想象成一个需要及时调查的警告。

red,红色。至少一个主分片以及它的全部副本都在缺失中。这意味着你在缺少数据:搜索只能返回部分数据,而分配到这个分片上的写入请求会返回一个异常。

index 、type、Document

从 ES 7.0 开始,Type 被废弃
在 7.0 以及之后的版本中 Type 被废弃了。一个 index 中只有一个默认的 type,即 _doc。

ES 的Type 被废弃后,库表合一,Index 既可以被认为对应 MySQL 的 Database,也可以认为对应 table。

也可以这样理解:

ES 实例:对应 MySQL 实例中的一个 Database。
Index 对应 MySQL 中的 Table 。
Document 对应 MySQL 中表的记录。

查看es版本

GET /

{
  "name": "Xfu4P",
  "cluster_name": "docker-cluster",
  "cluster_uuid": "PjAbaLnqRwSozfDoLxb",
  "version": {
    "number": "6.3.2",
    "build_flavor": "oss",
    "build_type": "tar",
    "build_hash": "053779d",
    "build_date": "2018-07-20",
    "build_snapshot": false,
    "lucene_version": "7.3.1",
    "minimum_wire_compatibility_version": "5.6.0",
    "minimum_index_compatibility_version": "5.0.0"
  },
  "tagline": "You Know, for Search"
}

字符串类型 keyword 、text

在 ElasticSearch 5.0 之前,字符串类型是 string。从 5.0 版本开始,string 类型被废弃,引入了 keyword 、text 两种类型。

两者的主要区别是:

  • keyword 不支持全文搜索。所以,只能是使用精确匹配进行查询,比如 term 查询。
  • text 默认支持全文搜索。

倒排索引的数据结构是怎样的?底层如何实现?

倒排索引的数据结构主要包括倒排列表(Inverted List)和词典(Lexicon)。

倒排列表(Inverted List)

倒排列表(Inverted List):倒排列表是倒排索引的核心数据结构之一,它存储了每个词项(term)在文档集合中的出现位置信息。每个词项对应一个倒排列表,该列表中包含了包含该词项的所有文档及其在文档中的位置。倒排列表通常由一个有序的文档ID列表和对应的位置信息构成。

例如,假设有词项 “cat” 出现在文档 1 的第 5 和第 10 个位置,出现在文档 3 的第 2 个位置,则 “cat” 的倒排列表可能如下所示:

cat: { 
  doc1: [5, 10],
  doc3: [2]
}

词典(Lexicon)

词典(Lexicon):词典用于将词项映射到倒排列表,提供了从词项到倒排列表的索引。它可以是一个简单的键值对结构,其中键是词项,值是对应的倒排列表在倒排索引中的位置。词典还可以提供额外的元信息,例如词项的文档频率(DF)和位置频率(TF)等。

Lexicon:
cat: { 
  df: 2,   // 文档频率
  position: 12345  // 倒排列表的起始位置
}

实现倒排索引的关键步骤包括分词、构建倒排列表、维护词典等。具体实现方法可以用常见的数据结构和算法来完成,例如使用哈希表或树结构来实现词典,使用数组或链表来实现倒排列表。

倒排索引的构建及查找过程

倒排索引的构建过程一般分为两个阶段:
1.首先是扫描文档集合进行分词并构建倒排列表,
2.然后是建立词典并对倒排列表进行排序和压缩等优化操作。

在搜索时,根据查询词在词典中查找对应的倒排列表,然后通过倒排列表中的文档ID找到相应的文档。

关键词查询时,文档的排名如何确认?

ES(Elasticsearch)是一个分布式搜索引擎,它使用倒排索引等技术来实现快速的全文搜索。在进行搜索时,ES会根据一系列因素来确定文档的排名,其中包括与查询的相关性、文档的分数以及其他因素。

当你用关键字进行查询时,有些文档排在第一个,而有些排在第二个的情况可能是由以下原因导致的:

  1. 相关性评分(Relevance Score): ES会根据文档与查询的相关性对文档进行打分。如果一个文档与查询更相关,它往往会被赋予更高的分数,从而排名更靠前。

  2. 字段匹配: ES可以针对文档的不同字段进行查询。如果某个文档的关键字匹配了查询的字段,并且匹配更多的字段,那么它可能会被认为更相关,因此排名更靠前。

  3. 文档的其他属性: ES也可能考虑文档的其他属性,比如文档的权重、更新时间等。这些属性可能会影响文档的排名。

  4. 查询的复杂度: 如果查询比较复杂,包含多个条件或者多个关键字,ES可能会根据查询的复杂度对文档进行更加细致的评分,导致不同文档的排名不同。

  5. 索引设置: ES的索引可以配置不同的分析器、评分算法等,这些配置可能会影响文档的相关性评分,从而影响文档的排名。

es操作

一、基础操作

使用工具

阿里云es提供查询工具,包括kibana,用docker搭也可以
es信息 推荐es-head 用Chrome插件 或者 GitHub

在这里插入图片描述

查询index结构

GET user_login_log/message/_mapping

GET 方式获取所有索引类型下的所有文档

GET 索引名/类型名/_search

查找_id 为1的文档

_search ? 后边为要查询的条件 q query 的意思 例如查询__id 为1 则 q=_id:1 查询名字为小明 q=name:小明

GET /lei/one/_search?q=_id:1

添加字段

POST /文档名/类型名
{
    "properties": {
       "添加的字段名": {
           "type": "字段的类型"
       }
   }
}
POST /lei/one/2/_update
{
 "doc":{
   "hobby":["篮球","击剑"]
 }
}
POST /lei/one/7ErnHHMBjoLvVc_LTjx_/_update
{
 "doc":{
   "hobby":["足球","爬山"]
 }
}

重建索引

何为重建索引
在原index的基础上copy一份数据在新index(字段相同,类型可不相同)。

为什么有重建索引的需求
当分词插件变更,数据类型改变等等。(当然也可以直接重跑一份数据)

如何重建索引
请借步参阅官网文档https://www.elastic.co/guide/en/elasticsearch/reference/6.5/docs-reindex.html

生产案例
需求
需要对现有数据内的一个字段进行排序。

问题
需要排序的字段存储类型是字符串,业务上值是数字 且并未开启fielddata1。

解决方案
新建index mapping 使对应字段数据类型为long,其他字段保持不变。使用reindex进行数据重建。需要重新建立新的索引

扩展
重建后的新索引,如何不停机的迁移到生产环境。在生产环境上建议使用 索引别名(Index Aliases2) 而不是直接使用真实的index name。(下文示例会说明)

使用reindex API

需要重新建立新的索引hot-search2

POST _reindex?wait_for_completion=false//直接返回结果
{
  "source": {
    "index": "hot-search1" //目标索引
  },
  "dest": {
    "index": "hot-search2"//新索引
  }
}

API返回结果

{
  "task": "dOlIdAkxQEOpXmHOjb3e4A:385537"//任务结果
}

tasks API

查询任务详情 官网文档https://www.elastic.co/guide/en/elasticsearch/reference/6.5/tasks.html

GET _tasks/dOlIdAkxQEOpXmHOjb3e4A:385537

修改别名 新索引平滑迁移到生产环境

POST _aliases
{
  "actions": [{"add": {//新增别名
    "index": "hot-search2",
    "alias": "hot-search"
  }}, {"remove": {//移除别名
    "index": "hot-search1",
    "alias": "hot-search"
  }}]
}

别名hot-search 指向了新的索引hot-search2 ,生产环境使用别名hot-search,这样索引变更就不会影响到生产环境。

冷热分离

冷热数据分离的前提
1、ES 的索引已经按照天或者月的时间维度生成索引。

2、历史数据相对于近期数据来说没有高频度的查询需求。

冷热数据分离的实现策略

本文实现策略:最新的天和月索引均为热数据,其他索引根据查询周期不同,调整为冷数据。当然业务不同策略不同,具体实现策略还是需要根据实际的业务场景来决定。

冷热数据分离的目的
1、ES集群异构,机器硬件资源配置不一,有高性能CPU和SSD存储集群,也有大容量的机械磁盘集群,比如我们的场景就是存放冷数据的集群,服务器都是多年前买的一批满配的4T Dell R70,但是新扩容的热节点机器均为DELL 高性能SSD磁盘和CPU的R740机器。

2、对于时间型数据来说,一般是当前的数据,写入和查询较为集中,所以高性能的资源应该优先提供给这些数据使用。

3、集群的搜索和写入性能,取决于最慢节点的性能。

冷热数据分离的前提
1、ES 的索引已经按照天或者月的时间维度生成索引。

2、历史数据相对于近期数据来说没有高频度的查询需求。

前置条件
需要修改ES 集群配置文件,对节点进行打标签操作
热数据实例:

node.tag: "hot"

冷数据实例:

node.tag: "cold"

二、增删改查

filter must 区别

全文搜索、评分排序,使用query;
是非过滤,精确匹配,使用filter。

过滤器(filter)通常用于过滤文档的范围,比如某个字段是否属于某个类型,或者是属于哪个时间区间。

must, 返回的文档必须满足must子句的条件,并且参与计算分值
filter, 返回的文档必须满足filter子句的条件。但是跟Must不一样的是,不会计算分值, 并且可以使用缓存

matchQuery和termQuery的区别

termQuery
不带分析器,比如说你搜索“中国”,没有分析器你就搜索不到,而

matchQuery
就带了分析器,你搜索“中国”的时候他会自动使用自带的中文分析器帮你去检索,那么你就能搜索到关于“中国”的信息。搜索.term是字段的检索,检索时会按照你输入的内容按照完全匹配的模式检索,而match是全文检索,会模糊按照匹配相关度给你找出结果按分值排列。某种意义上来说,term相当于“match_phrase”。

返回指定字段

fsc := elastic.
		NewFetchSourceContext(true).
		Include("jump_id", "message_type", "group_id", "result")

	result, err := client.Search(messageIndex).Type("message").
		Query(query).
		FetchSourceContext(fsc).
		Size(limit).
		Sort("addtime", false).
		Do(context.Background())

ES 读取过程分为GET和Search两种操作。

GET/MGET(批量GET):
	需要指定_index、_type、_id。也就是根据id从正排索引中获取内容。
Search:
	Search不指定_id,根据关键词从哪个倒排索引中获取内容

如何改es中的数据结构(字段)?

相同字段不同类型也是可以的

各种term query的 QueryBuild 构建

官方文档地址:https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-terms-query.html

elastic.matchQuery与termQuery区别

matchQuery:会将搜索词分词,再与目标查询字段进行匹配,若分词中的任意一个词与目标字段匹配上,则可查询到。

termQuery:不会对搜索词进行分词处理,而是作为一个整体与目标字段进行匹配,若完全匹配,则可查询到。

elastic.NewTermsQuery NewTermQuery区别

TermsQueryBuilder:

1、term query 分词精确查询,查询hotelName 分词后包含 hotel的term的文档

QueryBuilders.termQuery(“hotelName”,“hotel”)

2、terms Query 多term查询,查询hotelName 包含 hotel 或test 中的任何一个或多个的文档

词条查询(Term Query)允许匹配单个未经分析的词条,多词条查询(Terms Query)可以用来匹配多个这样的词条。只要指定字段包含任一我们给定的词条,就可以查询到该文档。

QueryBuilders.termsQuery(“hotelName”,“hotel”,“test”)

matchQuery、matchPhraseQuery、termQuery的区别

区别1:matchPhraseQuery和matchQuery等的区别,

matchQuery,在执行查询时,搜索的词会被分词器分词,而使用matchPhraseQuery,不会被分词器分词,而是直接以一个短语的形式查询,而如果你在创建索引所使用的field的value中没有这么一个短语(顺序无差,且连接在一起),那么将查询不出任何结果。

区别2:

matchQuery:会将搜索词分词,再与目标查询字段进行匹配,若分词中的任意一个词与目标字段匹配上,则可查询到。

termQuery:不会对搜索词进行分词处理,而是作为一个整体与目标字段进行匹配,若完全匹配,则可查询到。

multiMatchQuery 多字段匹配

query = query.Must(elastic.NewMultiMatchQuery(search_nickname, "nickname", "user_id"))

range query 范围查询

1.查询当天数据,客户端返回的是当天的日期,我用了elastic.NewRangeQuery(“date”).Lt(date), 一直查不出当天的数据, Lte才是小于等于

	ctx := context.Background()
	client, _ := models.ESClient()
	query := elastic.NewBoolQuery()
	query = query.Filter(elastic.NewTermQuery("receiver_uid", uid))
	if len(date) > 0 {
		query = query.Filter(elastic.NewRangeQuery("date").Lt(date))
	}
	if len(add_time) > 0 {
		query = query.Filter(elastic.NewRangeQuery("addtime").Lt(add_time))
	}

2.搜索浏览量在30~60之间的帖子

GET /forum/article/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "range": {
          "view_cnt": {
            "gt": 30,
            "lt": 60
          }
        }
      }
    }
  }
}

gte 大等于
lte 小等于

4、 exist query 查询字段不为null的文档 查询字段address 不为null的数据

QueryBuilders.existsQuery(“address”)

删除(更新)一条数据

//删除Es
//根据时间查到es对应的id并标记为HasRevoked Deleted(4),HasRevoked(6)
			// Update with script
			updRes, err := client.Update().Index(index).Type("message").Id(v).
				Script(
					elastic.NewScriptInline("ctx._source['Status']=6"),
				).
				Do(context.Background())

			fmt.Println("updRes: ", updRes)
			if err != nil {
				failIds = append(failIds, v)
				fmt.Printf("ES删除消息失败id:%v : err: %v", v, err)
				continue
			}

批量添加数据

//oss回调 consumer
func CronConsume() {
	//触发每次的消费能力 来自配置文件
	count := config.GetGlobal().MQ_CONSUME_COUNT
	gw := sync.WaitGroup{}
	gw.Add(count)

	for i := 0; i < count; i++ {
		go func(i int) {
			defer gw.Done()
			e, resData := models.ConsumeContent(config.GetGlobal().TOPIC_OSS_CALLBACK)
			if e == true && len(resData) > 0 {
				client, _ := models.ESClient()
				//es 批量add
				bulkService := client.Bulk()
				for _, item := range resData {
					var ossContent OssContent
					json.Unmarshal([]byte(item), &ossContent)
					if len(ossContent.Bucket) > 0 {
						r := elastic.NewBulkIndexRequest().Index("oss_illegal").Type("message").Doc(ossContent)
						bulkService = bulkService.Add(r)
					}
				}
				_, e := bulkService.Do(context.Background())
				if e != nil {
					fmt.Println("bulkService: " + fmt.Sprint(e))
				}
			}
		}(i)
	}
	gw.Wait()
}

三、es elastic sdk常用的方法

初始化一个通用的es连接

package models

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"os"
	"savetoes/pkg/setting"
	"strconv"
	"strings"

	pool "github.com/jolestar/go-commons-pool"
	"github.com/olivere/elastic"
)

var ctx = context.Background()
var pCommonPool *pool.ObjectPool

func init() {
	// 初始化连接池配置项
	PoolConfig := pool.NewDefaultPoolConfig()
	// 连接池最大容量设置
	PoolConfig.MaxTotal = 1000
	WithAbandonedConfig := pool.NewDefaultAbandonedConfig()
	// 注册连接池初始化链接方式
	pCommonPool = pool.NewObjectPoolWithAbandonedConfig(ctx, pool.NewPooledObjectFactorySimple(
		func(context.Context) (interface{}, error) {
			return Link()
		}), PoolConfig, WithAbandonedConfig)
}

// 初始化链接类
func Link() (*elastic.Client, error) {
	path, _ := os.Getwd()
	logPath := path + "/" + setting.LogPath
	file := logPath + "/eslog.log"
	logFile, _ := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0766) //TODO 判断error
	cfg := []elastic.ClientOptionFunc{
		elastic.SetURL(setting.EsUrl),
		elastic.SetBasicAuth(setting.EsUser, setting.EsPwd),
		elastic.SetSniff(false),
		elastic.SetInfoLog(log.New(logFile, "ES-INFO: ", 0)),
		elastic.SetTraceLog(log.New(logFile, "ES-TRACE: ", 0)),
		elastic.SetErrorLog(log.New(logFile, "ES-ERROR: ", 0)),
	}
	newClient, e := elastic.NewClient(cfg...)

	//fmt.Printf("esUrl: %v  , User: %v", setting.EsUrl, setting.EsUser)
	//fmt.Printf("esPwd: %v", setting.EsPwd)
	//return &PoolTest{}, nil
	return newClient, e
}


func SaveToEs(data []byte, index string) error {
	// 从连接池中获取一个实例
	obj, err := pCommonPool.BorrowObject(ctx)
	// 转换为对应实体
	if err != nil {
		fmt.Printf("pCommonPool.BorrowObject:%v", err)
		return err
	}
	client := obj.(*elastic.Client)
	println(client)

	//归还pool
	defer pCommonPool.ReturnObject(ctx, client)

	// _, err := client.Index().Index(index).Type("message").BodyJson(c2c_mes).Do(context.Background())
}

批量操作bulk:

数据库都要支持批量执行的操作,如批量写入。否则设想有一亿条数据,如果一个一个插入并发满了效率太低,并发高了数据库负载扛不住。作为开发者好的习惯是在需要的时候应该一次性的写入一批数据,减少对数据库写入频率。在es里面也支持批量操作:这个「批量」定义要更泛化,不止是指一次多写,还可以删除更新等!

//data直接插入es
func ESInsertData(data []interface{}, index string) {
	client, _ := models.ESClient()
	bulkRequest := client.Bulk()
	if len(data) > 0 {
		for _, v := range data {
			r := elastic.NewBulkIndexRequest().Index(index).Type("message").Doc(v)
			bulkRequest = bulkRequest.Add(r)
		}
		result, err := bulkRequest.Do(context.Background())
		if err != nil {
			ffmt.Puts(err)
		}
		ffmt.Puts(result)

	}
}

https://www.cnblogs.com/gwyy/p/13356345.html

筛选字段、排序

func GetUserPkScore(uid, group_id string, lifecycle_round, start, limit int) (list []GetUserPkScoreStruct, count int) {
	ctx := context.Background()
	client, _ := models.ESClient()
	query := elastic.NewBoolQuery()
	query = query.Filter(elastic.NewTermQuery("uid", uid))
	if len(group_id) > 0 {
		query = query.Filter(elastic.NewTermQuery("group_id", group_id))
	}
	if lifecycle_round >= 0 {
		query = query.Filter(elastic.NewTermQuery("lifecycle_round", lifecycle_round))
	}

	fsc := elastic.
		NewFetchSourceContext(true).
		Include("uid",
			"score",
			"group_id",
			"team_id",
			"msg_score",
			"invite_score",
			"be_like_score",
			"prop_score",
			"lifecycle_round",
			"team_rank_percent",
			"avatar",
			"nickname",
		)

	result, err := client.Search().
		Index(models.Es_index_pk_score_members).
		Type("message").
		FetchSourceContext(fsc).
		Query(query).
		From(start).
		Size(limit).
		Sort("lifecycle_end_time", false).
		Do(ctx)
	ffmt.Puts(err)
	if result.Hits.TotalHits > 0 {
		for _, v := range result.Hits.Hits {
			var info GetUserPkScoreStruct
			json.Unmarshal(*v.Source, &info)
			list = append(list, info)
		}
	}
	ffmt.Puts(list)
	ffmt.Puts("---------")
	count = int(result.Hits.TotalHits)
	return
}

更新一列

根据id Doc更新

//标记消息
func MarkOssIllegal(id, index, targetId string, status, targetMode int) (err error) {
	client, _ := models.ESClient()

	updateData := map[string]interface{}{
		"status": status,
	}
	if index == "content_security_illegal" {
		updateData["isDeal"] = 1
		updateData["dealTime"] = time.Now().UnixNano() / 1e6
	}
	if len(targetId) > 0 {
		updateData["targetId"] = targetId
	}
	if targetMode > 0 {
		updateData["targetMode"] = targetMode
	}
	_, err = client.Update().Index(index).Type("message").Id(id).Doc(updateData).Do(context.Background())

	if err != nil {
		fmt.Printf("ES标记MarkOssIllegal失败id:%v : err: %v", id, err)
		return err
	}
	return nil
}

client.UpdateByQuery elastic.NewScriptInline更新某行 某字段的的值

//修改动态浏览uv(半小时)
func EsUpdateDynVisitLogUv(id string, uv int) error {
	client, _ := models.ESClient()
	query := elastic.NewBoolQuery()
	query = query.Must(elastic.NewTermQuery("_id", id))
	var sql string = "ctx._source['uv']=2"
	if uv == 1 {
		sql = "ctx._source['uv']=1" //1为重复uv 2为已处理未重复uv
	}

	res, err := client.UpdateByQuery(models.Es_index_dynamic_log).Type("message").
		Query(query).
		Script(elastic.NewScriptInline(sql)).
		Do(context.Background())

	if err != nil {
		ffmt.Puts(err)
		ffmt.Puts(res)

	}
	return err
}

分页与排序

result, err := client.Search(messageIndex).Type("message").
	Query(query).
	From(start).Size(limit).
	Sort("is_read", true).
	Sort("addtime", false).
	Do(context.Background())

聚合

简单的聚合 NewTermsAggregation

条件: 获取发言数在指定范围、时间内的用户数

func GetUserCountWithMsgCountFromEs(group_id, time_range string, min_msg_count, max_msg_count int) int {
	client, _ := ESClient()
	query := elastic.NewBoolQuery()
	if group_id != "" {
		query = query.Filter(
			elastic.NewNestedQuery("MsgBody", elastic.NewMatchQuery("MsgBody.MsgType", "TIMTextElem")),
		).Filter(elastic.NewTermQuery("GroupId", group_id))
	}

	var start, end string
	if time_range != "" {
		addTimeS := strings.Split(time_range, ",")
		i := len(addTimeS)
		if i == 1 {
			start = common.GetUnixNanoToSecondStringTime(addTimeS[0])
			end = common.GetCurentSecondStringTime()
		} else if i == 2 {
			start = common.GetUnixNanoToSecondStringTime(addTimeS[0])
			end = common.GetUnixNanoToSecondStringTime(addTimeS[1])
		}
		query = query.Filter(elastic.NewRangeQuery("MsgTime").Gte(start).Lte(end))
	}
	//聚合
	aggs := elastic.NewTermsAggregation().Script(
		elastic.NewScript("doc['From_Account'].values"),
	)
	result, err := client.Search().
		Index(group_message_index).
		Type("message").
		Query(query).
		Aggregation("aggs", aggs).
		Do(ctx)
	if err != nil {
		fmt.Sprintf("GetUserCountWithMsgCountFromEs err: %v", err)
		return 0
	}
	// 处理聚合结果
	var info map[string]interface{}
	json.Unmarshal(*result.Aggregations["aggs"], &info)
	if len(info["buckets"].([]interface{})) == 0 {
		return 0
	}
	var total int
	for _, v := range info["buckets"].([]interface{}) {
		info_buckets := v.(map[string]interface{})
		//fmt.Println(info_buckets["doc_count"], reflect.TypeOf(info_buckets["doc_count"]))
		doc_count := info_buckets["doc_count"].(float64)
		user_msg_count := int(doc_count)
		if user_msg_count >= min_msg_count && user_msg_count <= max_msg_count {
			total += 1
		}
	}
	return total
}

聚合NewTermsAggregation与子聚合 SubAggregation

type AggBucketsStructs struct {
	Buckets []AggBucketsStruct `json:"buckets"`
}

type AggBucketsStruct struct {
	Doc_count    float64       `json:"doc_count"`
	Key          string        `json:"key"`
	Date         BucketsStruct `json:"date"`
	Receiver_uid BucketsStruct `json:"receiver_uid"`
}

//获取通知、审批消息列表(更多)
func GetAggMessageListMore(uid, date, add_time string, limit int) (tmp AggBucketsStructs) {
	ctx := context.Background()
	client, _ := models.ESClient()
	query := elastic.NewBoolQuery()
	query = query.Filter(elastic.NewTermQuery("receiver_uid", uid))
	if len(date) > 0 {
		query = query.Filter(elastic.NewTermQuery("date", date))
	}
	if len(add_time) > 0 {
		query = query.Filter(elastic.NewRangeQuery("addtime").Lt(add_time))
	}

	//聚合
	aggs := elastic.NewTermsAggregation().Script(
		elastic.NewScript("doc['date'].values"),
	).OrderByKey(false).
		SubAggregation("date", elastic.NewTermsAggregation().Field("date")).
		SubAggregation("receiver_uid", elastic.NewTermsAggregation().Field("receiver_uid")).
		Size(limit)
	result, err := client.Search().
		Index(messageIndex).
		Type("message").
		Query(query).
		Aggregation("aggs", aggs).
		Do(ctx)
	//ffmt.Puts(query.Source())
	if err != nil {
		ffmt.Puts(err)
	}
	json.Unmarshal(*result.Aggregations["aggs"], &tmp)
	ffmt.Puts("----------")
	return
}
聚合后再筛选条件
// 获取发言数在指定范围内的用户数
func GetUserCountWithMsgCountFromEs(group_id, time_range, is_robot string, min_msg_count, max_msg_count int) int {
	client, _ := ESClient()
	query := elastic.NewBoolQuery()
	if group_id != "" {
		query = query.Filter(
			elastic.NewNestedQuery("MsgBody", elastic.NewMatchQuery("MsgBody.MsgType", "TIMTextElem")),
		).Filter(elastic.NewTermQuery("GroupId", group_id))
	}

	var start, end string
	if time_range != "" {
		addTimeS := strings.Split(time_range, ",")
		i := len(addTimeS)
		if i == 1 {
			start = common.GetUnixNanoToSecondStringTime(addTimeS[0])
			end = common.GetCurentSecondStringTime()
		} else if i == 2 {
			start = common.GetUnixNanoToSecondStringTime(addTimeS[0])
			end = common.GetUnixNanoToSecondStringTime(addTimeS[1])
		}
		query = query.Filter(elastic.NewRangeQuery("MsgTime").Gte(start).Lte(end))
	}

	if group_id != "" {
		query = query.Filter(
			elastic.NewNestedQuery("MsgBody", elastic.NewMatchQuery("MsgBody.MsgType", "TIMTextElem")),
		).Filter(elastic.NewTermQuery("GroupId", group_id))
	}
	//聚合
	aggs := elastic.NewTermsAggregation().Script(
		elastic.NewScript("doc['From_Account'].values"),
	)
	result, err := client.Search().
		Index(group_message_index).
		Type("message").
		Query(query).
		Aggregation("aggs", aggs).
		Do(ctx)
	if err != nil {
		fmt.Sprintf("GetUserCountWithMsgCountFromEs err: %v", err)
		return 0
	}
	var info map[string]interface{}
	json.Unmarshal(*result.Aggregations["aggs"], &info)
	if len(info["buckets"].([]interface{})) == 0 {
		return 0
	}
	var total int
	for _, v := range info["buckets"].([]interface{}) {
		info_buckets := v.(map[string]interface{})
		//fmt.Println(info_buckets["doc_count"], reflect.TypeOf(info_buckets["doc_count"]))
		doc_count := info_buckets["doc_count"].(float64)
		uid := info_buckets["key"].(string)
		id , _ := strconv.Atoi(uid)
		// 传1为排除马甲号
		if is_robot == "1" {
			count := UserisRobots(id)
			if count ==1 {
				continue
			}
		}
		// 判断是否是马甲号
		user_msg_count := int(doc_count)
		if user_msg_count >= min_msg_count && user_msg_count <= max_msg_count {
			total += 1
		}
	}
	return total
}
字段过滤 NewFetchSourceContext
//群消息
func GetGroupMsgForCron(group_id string, time_start int64) (list []map[string]interface{}) {
	ctx := context.Background()
	client, _ := models.ESClient()
	query := elastic.NewBoolQuery()
	query = query.Filter(
		elastic.NewNestedQuery("MsgBody", elastic.NewMatchQuery("MsgBody.MsgType", "TIMTextElem")),
	).
		Filter(elastic.NewTermQuery("GroupId", group_id)).
		Filter(elastic.NewRangeQuery("MsgTime").Gt(time_start))
	fsc := elastic.
		NewFetchSourceContext(true).
		Include("From_Account",
			"TeamId",
			"GroupId",
			"MsgSeq",
			"MsgTime",
		)
	result, err := client.Search().
		Index(group_message_index).
		Type("message").
		FetchSourceContext(fsc).
		Query(query).
		Sort("MsgSeq", false).
		Do(ctx)
	ffmt.Puts(err)

	if result.Hits.TotalHits > 0 {
		for _, v := range result.Hits.Hits {
			info := map[string]interface{}{}
			json.Unmarshal(*v.Source, &info)
			list = append(list, info)
		}
	}
	//ffmt.Puts(list)
	return list
}

四、ElasticSearch日志

3.1 日志系统

ELK 日志收集分析

Log Source:日志来源。在微服务中,我们的日志主要来源于日志文件和Docker容器,日志文件包括服务器log,例如Nginx access log(记录了哪些用户,哪些页面以及用户浏览器、ip和其他的访问信息), error log(记录服务器错误日志)等。
Logstash:数据收集处理引擎,可用于传输docker各个容器中的日志给EK。支持动态的从各种数据源搜集数据,并对数据进行过滤、分析、丰富、统一格式等操作,然后存储以供后续使用。
Filebeat:和Logstash一样属于日志收集处理工具,基于原先 Logstash-fowarder 的源码改造出来的。与Logstash相比,filebeat更加轻量,占用资源更少
ElasticSearch:日志搜索引擎
Kibana:用于日志展示的可视化工具
Grafana:类似Kibana,可对后端的数据进行实时展示

在这里插入图片描述
由图可知,当我们在Docker中运行应用(application)时,filebeat收集容器中的日志。ElasticSearch收到日志对日志进行实时存储、搜索与分析。我们可在Kibana和Grafana这两个可视化工具中查看日志的操作结果。

EFK架构(FileBeat+ES+Kibana)

Fluentd是一个开源的数据收集器,专为处理数据流设计,使用JSON作为数据格式。它采用了插件式的架构,具有高可扩展性高可用性,同时还实现了高可靠的信息转发。

因此,我们加入Fluentd来收集日志。加入后的EFK架构如图所示。

常见面试题

es写入过程及底层原理

1)客户端选择一个node发送请求过去,这个node就是coordinating node(协调节点)
2)coordinating node,对该数据经过hash后,判断该数据属于哪个shard进程,找到有该shard的primary shard的node,然后对document进行路由,将请求转发给对应的node(有primary shard的结点)
3)具体接收的primary shard处理请求,然后将数据同步到replica node
4)coordinating node,如果发现primary node和所有replica node都搞定之后,就返回响应结果给客户端

底层原理

1)先写入buffer,在buffer里的时候数据是搜索不到的;同时将数据写入translog日志文件(防止宕机buffer数据丢失)

2)如果buffer快满了,或者到一定时间,就会将buffer数据refresh到一个新的segment file中,但是此时数据不是直接进入segment file的磁盘文件的,而是先进入os cache的。这个过程就是refresh。

默认每隔1秒钟,es将buffer中的数据写入一个新的segment file,每秒钟会产生一个新的磁盘文件 segment file,这个segment file中就存储最近1秒内buffer中写入的数据

但是如果buffer里面此时没有数据,那当然不会执行refresh操作,不会创建文件,如果buffer里面有数据,默认1秒钟执行一次refresh操作,刷入一个新的segment file中;

操作系统里面,磁盘文件其实都有一个东西,叫做os cache,操作系统缓存,就是说数据写入磁盘文件之前,会先进入os cache,先进入操作系统级别的一个内存缓存中去,再进入磁盘

只要buffer中的数据被refresh操作,刷入os cache中,就代表这个数据就可以被搜索到了,只要数据被输入os cache中,buffer就会被清空了,因为不需要保留buffer了,数据在translog里面已经持久化到磁盘去一份了

3)只要数据进入os cache,此时就可以让这个segment file的数据对外提供搜索了

4)重复1~3步骤,新的数据不断进入buffer和translog,不断将buffer数据写入一个又一个新的segment file中去(数据写入到segment file里后就建立好了倒排索引),每次refresh完buffer清空,translog保留。随着这个过程推进,translog会变得越来越大。当translog达到一定长度的时候,就会触发 translog 的commit操作。

es搜索过程?

1.客户端发送请求到一个 coordinate node。
2.协调节点将搜索请求转发到所有的 shard 对应的 primary shard 或 replica shard,都可以。
3.query phase:每个 shard 将自己的搜索结果(其实就是一些 doc id)返回给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果。
4.fetch phase:接着由协调节点根据 doc id 去各个节点上拉取实际的 document 数据,最终返回给客户端。

更新和删除文档的过程

1、删除和更新也都是写操作,但是Elasticsearch 中的文档是不可变的, 因此不能被删除或者改动以展示其变更;

2、磁盘上的每个段都有一个相应的.del 文件。当删除请求发送后,文档并没有真的被删除,而是在.del 文件中被标记为删除。该文档依然能匹配查询,但是会在结果中被过滤掉。当段合并时,在.del 文件中被标记为删除的文档将不会被写入新段。

3、在新的文档被创建时,Elasticsearch 会为该文档指定一个版本号,当执行更新时,旧版本的文档在.del 文件中被标记为删除,新版本的文档被索引到一个新段。旧版本的文档依然能匹配查询,但是会在结果中被过滤掉。

段合并

Elasticsearch索引(elasticsearch index)由一个或者若干分片(shard)组成,分片(shard)通过副本(replica)来实现高可用。一个分片(share)其实就是一个Lucene索引(lucene index),一个Lucene索引(lucene index)又由一个或者若干段(segment)组成。所以,当我们查询一个Elasticsearch索引时,查询会在所有分片上执行,既而到段(segment),然后合并所有结果。

段合并(segment merge)
每次refresh都产生一个新段(segment),频繁的refresh会导致段数量的暴增。段数量过多会导致过多的消耗文件句柄、内存和CPU时间,影响查询速度。基于这个原因,Lucene会通过合并段来解决这个问题。

在这里插入图片描述

段合并 --> 强制合并
由于自动刷新流程每秒会创建一个新的段,这样会导致段时间内的段数量暴增。而段数目太多会带来较大的麻烦。每个段都会消耗文件句柄、内存和CPU运行周期。更重要的是,每个请求都必须轮流检查每个段;所以段越多,搜索也就越慢。Elasticsearch通过在后台进行段合并来解决这个问题。小的段被合并到大的段,然后这些大的段再被合并到更大的段。段合并的时候会讲那些旧的一删除的文档从文件系统中清除。被删除的文档(或被更新文档的旧版本)不会被拷贝到新的大段中。启动段合并并不需要你做任何事。进行索引和搜索时会自动进行。

Elasticsearch会有后台线程根据lucene的合并规则定期进行段合并操作,一般不需要用户担心或者采取任何行动。被删除的文档在合并时,才会被真正删除掉。再次之前,它仍然会占用着JVM heap和操作系统的文件cache、磁盘等资源。在某些特定情况下,我们需要ES强制进行段合并,以释放其占用的大量系统、磁盘等资源。POST /index_name/_forcemerge。

_forcemerge 命令可强制进行segment合并,并删除所有标记为删除的文档。Segment merging要消耗CPU,以及大量的I/O资源,所以一定要在你的ElasticSearch集群处于维护窗口期间,并且有足够的I/O空间的(如:SSD)的条件下进行;否则很可能造成集群崩溃和数据丢失。

https://www.cnblogs.com/charles101/p/14490226.html

es乐观锁 文档并发更新问题

在这里插入图片描述

解决方案:

1.消息队列顺序消费
2.es客户端执行增加重试
最终一致性

要求并发

1.先存入redis、mysql合并后的结果,再来更新es

es不适合做一致性要求高的业务,

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值