在公司需要用到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会根据一系列因素来确定文档的排名,其中包括与查询的相关性、文档的分数以及其他因素。
当你用关键字进行查询时,有些文档排在第一个,而有些排在第二个的情况可能是由以下原因导致的:
-
相关性评分(Relevance Score): ES会根据文档与查询的相关性对文档进行打分。如果一个文档与查询更相关,它往往会被赋予更高的分数,从而排名更靠前。
-
字段匹配: ES可以针对文档的不同字段进行查询。如果某个文档的关键字匹配了查询的字段,并且匹配更多的字段,那么它可能会被认为更相关,因此排名更靠前。
-
文档的其他属性: ES也可能考虑文档的其他属性,比如文档的权重、更新时间等。这些属性可能会影响文档的排名。
-
查询的复杂度: 如果查询比较复杂,包含多个条件或者多个关键字,ES可能会根据查询的复杂度对文档进行更加细致的评分,导致不同文档的排名不同。
-
索引设置: 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不适合做一致性要求高的业务,