elasticsearch原理简析服务器上安装Python和golang语言下的数据写入和查询简单实战演练

跳槽后来公司,第一个项目就是做一个基于FAQ的智能客服问答系统。在召回阶段,直接采用了elasticsearch。感觉这个工具对NLP项目的落地很重要,当做一篇拓宽知识面的博客吧。

一、elasticsearch简介

Elasticsearch 是一个分布式的免费开源搜索和分析引擎,适用于包括文本、数字、地理空间、结构化和非结构化数据等在内的所有类型的数据。更加准确的说明:

  • 一个分布式的实时文档存储,每个字段可以被索引与搜索
  • 一个分布式实时分析搜索引擎
  • 能胜任上百个服务节点的扩展,并支持 PB 级别的结构化或者非结构化数据

Elasticsearch是在Apache Lucene 的基础上开发而成,采用了倒排索引思想+B_Tree以及一些数据压缩和检索技术手段,保证数据的高效的写入和查询。

elasticsearch由于可以实现全文搜索,并且操作简单都是封装成了rest风格的API,在日志分析系统、客服问答系统等都可以作为一种实现方案。

常用术语

Node & Cluster

es的本质上是一个分布式的数据库,可以使单机单节点,也可以是一个多机集群式的。

索引(Index)

这个可以类比为Mysql等关系型数据库中的一个数据库名称,或者理解是一张表的名称(es高版本中由于没有type的类型)。Elasticsearch 数据管理的顶层单位就叫做 Index(索引),相当于关系型数据库里的数据库的概念。另外,每个Index的名字必须是小写,实战中经常会犯的错误。

文档(Document)

Index里面单条的记录称为 Document(文档)。许多条 Document 构成了一个 Index。Document 使用 JSON 格式表示。同一个 Index 里面的 Document,不要求有相同的结构(scheme),但是最好保持相同,这样有利于提高搜索效率,可以理解为数据库中的一条记录。

类型(Type)

Document 可以分组,比如employee这个 Index 里面,可以按部门分组,也可以按职级分组。这种分组就叫做 Type,它是虚拟的逻辑分组,用来过滤 Document,类似关系型数据库中的数据表。不同的 Type 应该有相似的结构(Schema),性质完全不同的数据(比如 products 和 logs)应该存成两个 Index,而不是一个 Index 里面的两个 Type(虽然可以做到)。

值得注意的是在高版本的es中并没有类型这样的概念了。高版本中一个index就是一个数据,其中只包含了一张表,表中有很多条数据。

文档数据

  文档元数据为_index, _type, _id, 这三者可以唯一表示一个文档,_index表示文档在哪存放,_type表示文档的对象类别,_id为文档的唯一标识。

Fileds(字段)

每个Document都类似一个JSON结构,它包含了许多字段,每个字段都有其对应的值,多个字段组成了一个 Document,可以类比关系型数据库数据表中的字段。

当然这里的字段,也有自己的类型,分别是text和keyword,区别是数据存储的时候是否进行了分词,查询过程中对于text的类型只要有相同的部分就可以被检索出来,而keyword类型的则需要完全相同。

给一个es存储的示例截图

 字段、id、索引具体样式如上图所示。

二、实战演练

es支持很多版本的程序语言的操作,本人常用python和go语言。就python和go语言如何操作es进行一个实战演练。

Python语言版本

1、数据写入

首先看看数据是如何存储近es的。从写入速度来区分,可以一条一条的比较缓慢的写入,也可以按批次快速写入。

具体的步骤可以分为如下步骤:

第一步:es连接

第二步:index创建,同时进行filed属性映射

es写入的数据是采用字典的格式,因此需要把数据封装成字典格式,同时做属性映射的时候需要指定每个字段的类型属性是keyword还是text(分词处理后)。

第三步:写入数据

a、正常写入

直接上代码

    es = Elasticsearch(hosts=['127.0.0.1:9200'])
    properties ={}
   #带标签不带分词
    for k in datas[0].keys():
        if k != "Q_Sample":
            properties[k] = {'type': 'keyword'}
        else:
            properties[k] = {
                'type': 'text'
            }
    mapping = {'properties':properties}
    print(mapping)

    re = es.indices.create(index=index_name, ignore=400)
    print(re)
    #filed属性做映射
    es.indices.put_mapping(index=index_name, doc_type='insurance', body=mapping,include_type_name=True)

    #写入数据
    for data in tqdm(datas,desc='insert data to es'):
        es.index(index=index_name, doc_type='insurance', body=data)

注意高版本(8.0之后的吧)的es是没有doc_type属性的

es.indices.put_mapping(index=index_name, body=mapping)

这样的写入速度比较慢,要想快速写入就得使用批处理,需要用到

from elasticsearch import helpers
helpers.bulk(es, action)

b、批量写入

es的连接和index创建以及mapping映射通上面的代码是一样的,不同的是采用bulk来快速写入数据的代码,同时这里为了防止内存爆炸,采用分段以及列表生成器来完成写入。上代码:

from elasticsearch import Elasticsearch
from elasticsearch import helpers
from tqdm import  tqdm
import time


def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        res = func(*args, **kwargs)
        print('共耗时约 {:.2f} 秒'.format(time.time() - start))
        return res

    return wrapper


def create_es_index_insert_datas(index_name,datas):
    # 测试环境
    es = Elasticsearch(hosts=['127.0.0.1:9200'])
    # 采用全量重写的方式
    if es.indices.exists(index=index_name):
        es.indices.delete(index=index_name)
    properties ={}
   #带标签不带分词
    for k in datas[0].keys():
        if k != "Q_Sample":
            properties[k] = {'type': 'keyword'}
        else:
            properties[k] = {
                'type': 'text'
            }
    mapping = {'properties':properties}
    print(mapping)

    es.indices.create(index=index_name, ignore=400)
    #filed属性做映射
    es.indices.put_mapping(index=index_name, doc_type='insurance', body=mapping,include_type_name=True)


    return es

def get_texts():
    datas_with_tags = []
    ......
    ......
    for line in ......:
        line = line.strip('\n').split('\t')

        temp_with_tags ={}
        for ele in line[0:-2]:
            if ':' in ele:
                k,v = ele.split(':')
                if v=='null':
                    temp_with_tags[k] = ''
                else:
                    temp_with_tags[k] = v

        temp_with_tags['Q_Sample'] = line[-2]
        temp_with_tags['A_Sample'] = line[-1]
        datas_with_tags.append(temp_with_tags)
    return datas_with_tags




@timer
def gen(index_name,es,datas_with_tags):
    """ 使用生成器批量写入数据 分段写入  一批次1000条数据 """
    print(len(datas_with_tags))
    length = len(datas_with_tags)
    print(length)
    for i in range(length//1000):
        action = ({
            "_index": index_name,
            "_type": "insurance",
            "_source": datas_with_tags[k]
        } for k in range(i*1000, (i+1)*1000))
        helpers.bulk(es, action) #action 列表生成器
    if length%1000 != 0:
        action = ({
            "_index": index_name,
            "_type": "insurance",
            "_source":datas_with_tags[k]
        } for k in range(length//1000*1000, length))
        helpers.bulk(es, action)




if __name__ == '__main__':
    datas = get_texts()
    # index_name = 'weikong_data'  # 本地测试环境
    index_name = 'es_hy_test_0825' #本地测试环境
    # index_name = 'hy_search_test' #自己的测试es
    es = create_es_index_insert_datas(index_name, datas)

    gen(index_name, es, datas)


后面这种写入速度比第一种的速度快很多,1W条速度对比:

2、数据查询相关

es查询可以使python内置的API,也可以是REST风格的url配合编程语言中的http post模块来查询。es查询最重要的就是查询语句,常用的一些bool查询、term语句、match查询等等。这里展示一个python版本的查询代码:

from elasticsearch import Elasticsearch
es = Elasticsearch(hosts=['127.0.0.1:9200'])

body2 = {
        "query": {
            "match": {"question":"道路救援"}
        },
        'from': 0,  # 从第二条数据开始
        'size': 20  # 获取4条数据
    }
result2 = es.search(index='questions', body=body2)
print('result2', result2)

复杂一点的bool、match加term联合查询语句:

{
    "from":0,
    "size":5,
    "query":{
        "bool":{
            "must":[
                {
                    "match":{
                        "Q_Sample":{
                            "query":"我去你们店里付款"
                        }
                    }
                },
                {
                    "term":{
                        "must一级分类":"支付类"
                    }
                },
                {
                    "term":{
                        "must二级分类":"支付方式"
                    }
                },
                {
                    "term":{
                        "must限定":""
                    }
                },
                {
                    "term":{
                        "must句式":""
                    }
                },
                {
                    "term":{
                        "must费用条款":""
                    }
                },
                {
                    "term":{
                        "must操作":""
                    }
                },
                {
                    "term":{
                        "must售后":""
                    }
                },
                {
                    "term":{
                        "must赠品相关":""
                    }
                },
                {
                    "term":{
                        "must险种":""
                    }
                },
                {
                    "term":{
                        "must推诿":""
                    }
                },
                {
                    "term":{
                        "must理赔询问":""
                    }
                },
                {
                    "term":{
                        "must产品服务名":""
                    }
                },
                {
                    "term":{
                        "must强匹配":""
                    }
                },
                {
                    "term":{
                        "must否定含义":""
                    }
                }
            ],
            "should":[
                {
                    "term":{
                        "支付相关":"支付"
                    }
                }
            ]
        }
    }
}

当查询的时候,超过了es最大窗口设置,需要修改窗口大小,假如有很多index的话就比较麻烦,直接在查询的时候分窗口查询:

 sample = {'userId':'2795_search_226268','A_Sample': '', 'extension': '', 'Q_Sample': '你是到哪一步不好使了', 'createTime': '2021-10-08 19:10:59', 'cls': 'history', 'actions': '', 'answerDescs': ''}
    result = {}
    for key in sample.keys():
        result[key] = []
    es = Elasticsearch(hosts=['127.0.0.1:9200'])#对应的url
    for index_name,count in tqdm(zip(all_es_indexs,all_datacounts)):
        interval = 5000
        for i in range(int(count/interval)):
            body = {
                "query": {"match_all": {}},
                "from": i*interval,
                "size": interval
            }
            searchs = es.search(index=index_name,body=body)
            records = searchs['hits']['hits']
            for record in records:
                record['_source']['userId'] = index_name
                if 'createTime' not in record['_source']:
                    record['_source']['createTime'] = '2021-10-27 11:22:59'
                for key, v in record['_source'].items():
                    result[key].append(str(v))

        if count - int((count/interval))*interval > 0:
            body = {
                "query": {"match_all": {}},
                "from": int((count/interval))*interval,
                "size": count - int((count/interval))*interval
            }
            searchs = es.search(index=index_name, body=body)
            records = searchs['hits']['hits']
            for record in records:
                record['_source']['userId'] = index_name
                if 'createTime' not in record['_source']:
                    record['_source']['createTime'] = '2021-10-27 11:22:59'
                for key, v in record['_source'].items():
                    result[key].append(str(v))


    df = pd.DataFrame()
    for key,v in result.items():
        print(key,'---',len(v))
        df[key] = v
    df.to_csv('all_pro_history_es_datas.csv',index=False,sep='\t')

具体的各种查询语句还需要使用的时候多多熟悉;

golang语言版本

1、数据写入

es初始化,包含index的mapping设置,检查index是否存在等;

func initEsMappingsAndIndex(IndexName string, esFiledsList []string, esClient *elastic.Client) {
	//注意number_of_shards 和 number_of_replicas的设置
	mappings := "{\"settings\":{\"number_of_shards\": 1,\"number_of_replicas\": 2},\"mappings\":{\"properties\":{"
	for _, filed := range esFiledsList {
		//if filed != "Q_Sample" {
		//	mappings += "\"" + filed + "\":{\"type\":\"keyword\"},"
		//} else {
		//	mappings += "\"" + filed + "\":{\"type\":\"text\"},"
		//}

		if filed == "createTime" {
			mappings += "\"" + filed + "\":{\"type\":\"date\",\"format\":\"yyyy-MM-dd HH:mm:ss\"},"
		} else if filed == "Q_Sample" {
			mappings += "\"" + filed + "\":{\"type\":\"text\"},"
		} else {
			mappings += "\"" + filed + "\":{\"type\":\"keyword\"},"
		}

	}
	mappings = mappings[0 : len(mappings)-1]
	mappings += "}}}"
	fmt.Println(mappings)

	exists, err := esClient.IndexExists(IndexName).Do(context.Background())
	if err != nil {
		// Handle error

		panic(err)
	}
	if !exists {
		fmt.Println(IndexName + " is not exists ,creations")
		// Create a new index.
		createIndex, err := esClient.CreateIndex(IndexName).BodyString(mappings).Do(context.Background())
		if err != nil {
			// Handle error
			log.Error("esClient build Index and put mappings errors", err)
			panic(err)
		}
		if !createIndex.Acknowledged {
			fmt.Println("mappings puting failure!")
		} else {
			fmt.Println("mappings puting success!")
		}
	}
}

数据插入代码:

bodyJson := make(map[string]string)
body, _ := json.Marshal(bodyJson)
				bodyString := string(body)
				log.Infof("bodyString: %s", bodyString)
				esInsert, err := esClient.Index().Index(EsIndexName).Type("_doc").BodyString(bodyString).Do(context.Background())
if err != nil {
			log.Errorf("esClient insert data errors %s", err)
			panic(err)
		}
//插入成功会返回对应的Id
ids = append(ids, esInsert.Id)

2、数据查询相关

直接采用resful API风格来查询,请求体配置好了,直接发起http post请求

//插入之前对问题做一次去重,针对问题做去重
		targetBody := "{\"from\":0,\"size\":" + strconv.Itoa(1) + ",\"query\":{\"bool\":{\"must\":[{\"match\":{\"Q_Sample\":{\"query\":\"" + question + "\"}}},{\"term\":{\"userId\":\"" + userId + "\"}}]}}}"
		log.Infof("targetBody: %s", targetBody)
		ElkRspBody := Manager.httpPost(session, config.Config().Common.EsServerAddress+"/"+EsIndexName+"/_search", "application/json", string(targetBody))
		//查询es库得到结果
		ElkRsp := WkELKRsp{}
		err = json.Unmarshal(ElkRspBody, &ElkRsp)
		if err != nil {
			log.Error(err.Error())
			panic(constrant.JSONFormatError)
		}
		res := ""
		for _, hit := range ElkRsp.Hits.Hits {
			res = hit.Source.Q_Sample
		}

得到的结果进行json解码即可。

三、postman操作es常见操作

查询——post请求

http://127.0.0.1:9202/weikong_search_shanghai/_search

{"from":0,"size":10}
{"from":0,"size":100,"query":{"bool":{"must":[{"match":{"Q_Sample":{"query":"小保养怎么预约"}}}]}}}
{"from":0,"size":100,"query":{"bool":{"must":[{"match":{"Q_Sample":{"query":"保单帮我寄过来就可以了"}}},{"term":{"must二级分类":"获取"}},{"term":{"must理赔询问":""}},{"term":{"must售后":""}},{"term":{"must否定含义":""}},{"term":{"must险种":""}},{"term":{"must推诿":""}},{"term":{"must产品服务名":""}},{"term":{"must强匹配":""}},{"term":{"must限定":""}},{"term":{"must一级分类":"保单发票"}},{"term":{"must操作":""}},{"term":{"must句式":""}},{"term":{"must费用条款":""}},{"term":{"must赠品相关":""}}],"should":[{"term":{"关键要素":"保单"}}]}}}

查询index数据总数——get请求

http://127.0.0.1:9202/weikong_search_recommend/_count

更新——post请求

根据时间字段修改

http://127.0.0.1:9202/16326_search_9900002058/_update_by_query

{"script": {"lang": "painless", "source": "if (ctx._source.createTime == null) {ctx._source.createTime= '2021-10-08 15:25:48'}"}}

集群shards设置——put接口

http://127.0.0.1:9202/_cluster/settings

{
    "transient": {
        "cluster": {
            "max_shards_per_node":10000
        }
    }
}

es最大单次查询结果限制——put接口

http://127.0.0.1:9202/_cluster/settings

{ "index.max_result_window" :"50000"}

查询es的健康状态——所有索引情况get请求

http://127.0.0.1:9202/_cat/indices

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值