目录
1、背景
自从trans到自动驾驶事业部数仓团队后,每天的工作基本都是围绕数据展开,重点是在检索方向上,因此熟练掌握elasticsearch的基本知识成为当务之急,es基础也是提升工作效率的必备技能。
在项目中,我们使用了官方提供的包:github.com/elastic/go-elasticsearch,但是在使用的过程中,发现这个包比较偏底层,开发成本略高,且不容易理解,因此打算调研一个使用方便、开发成本低的三方包。
目前网络上使用较多的三方包为:
https://github.com/olivere/elastic
目前最新版本:7.0.32,其发布都是在2年前(2022.3.19),好久没更新了,但是使用热度依然不减。下面就使用这个包来完成es文档的基本操作,包括增加、删除、更新、查询以及对应的批量操作。
2、elasticsearch基础操作
2.1 创建es链接客户端
首先定义一个es实例结构:
type EsInstance struct {
Client *elastic.Client // es客户端
Index map[string]Indexes // 所有索引
}
type Indexes struct {
Name string `json:"name"` // 索引名称
Mapping string `json:"mapping"` // 索引结构
}
创建实例:
package elastic
import (
"github.com/olivere/elastic/v7"
)
type EsInstance struct {
Client *elastic.Client // es客户端
Index map[string]Indexes // 所有索引
}
type Indexes struct {
Name string `json:"name"` // 索引名称
Mapping string `json:"mapping"` // 索引结构
}
func NewEsInstance() (*EsInstance, error) {
client, err := elastic.NewClient(
elastic.SetURL("http://127.0.0.1:9200"), // 支持多个服务地址,逗号分隔
elastic.SetBasicAuth("user_name", "user_password"),
elastic.SetSniff(false), // 跳过ip检查,默认是true
)
if err != nil {
return &EsInstance{}, err
}
return &EsInstance{
Client: client,
Index: map[string]Indexes{},
}, nil
}
2.2 创建、删除索引
对于索引,如果我们只是要做普通的标签检索,索引的mapping结构不需要单独设置,如果要支持向量检索,索引的mapping是需要手动创建的,创建索引:
func (es *EsInstance) CreateIndex(index string, mapping string) error {
// 判断索引是否存在
exists, err := es.Client.IndexExists(index).Do(context.Background())
if err != nil {
return err
}
// 索引存在,直接返回
if exists {
return nil
}
// 创建索引
createIndex, err := es.Client.CreateIndex(index).BodyString(mapping).Do(context.Background()) // 指定索引名称和结构
if err != nil {
return err
}
if !createIndex.Acknowledged {
return errors.New("create index failed")
}
es.Index[index] = Indexes{Name: index, Mapping: mapping}
return nil
}
删除索引:
func (es *EsInstance) DeleteIndex(index string) error {
// 判断索引是否存在
exists, err := es.Client.IndexExists(index).Do(context.Background())
if err != nil {
return err
}
// 索引不存在,直接返回
if !exists {
return nil
}
// 删除索引
deleteIndex, err := es.Client.DeleteIndex(index).Do(context.Background())
if err != nil {
return err
}
if !deleteIndex.Acknowledged {
return errors.New("delete index failed")
}
return nil
}
2.3 插入文档
定义文档结构:
type Record struct {
Index string `json:"_index"` // 索引名称
Type string `json:"_type"` // 索引类型
Id string `json:"_id"` // 索引ID
Source Source `json:"source"` // 文档内容
}
type Source struct {
Id string `json:"id"` // 学号
Name string `json:"name"` // 姓名
Age int `json:"age"` // 年龄
Sex string `json:"sex"` // 性别
}
插入文档,文档id自定义,如果不传入id,es将会自动生成一个id:
func (es *EsInstance) InsertOneRecord(indexName string, record Record) error {
_, err := es.Client.Index().Index(indexName).Id(record.Source.Id).BodyJson(record).Do(context.Background())
if err != nil {
return err
}
return nil
}
2.3 查询文档
根据文档id获取一条文档。
func (es *EsInstance) GetOneRecord(indexName, id string) (*Record, error) {
record := &Record{}
fmt.Println("index name: ", indexName, " id: ", id)
result, err := es.Client.Get().Index(indexName).Id(id).Do(context.Background())
if err != nil {
fmt.Printf("Error getting a document: %s\n", err.Error())
return record, err
} else {
fmt.Println("Document got!")
}
if result.Found {
err := json.Unmarshal(result.Source, &record.Source)
if err != nil {
fmt.Printf("Error unmarshalling the source: %s\n", err.Error())
}
}
record.Id = result.Id
record.Index = result.Index
record.Type = result.Type
return record, nil
}
2.4 删除一条文档
根据文档id删除一条文档。
func (es *EsInstance) DeleteOneRecord(indexName, id string) error {
_, err := es.Client.Delete().Index(indexName).Id(id).Do(context.Background())
if err != nil {
fmt.Printf("Error deleting a document: %s\n", err.Error())
}
return nil
}
2.5 更新文档
根据文档id更新文档:
func (es *EsInstance) UpdateOneRecord(indexName, id string, record Source) error {
_, err := es.Client.Update().Index(indexName).Id(id).Doc(record).Do(context.Background())
if err != nil {
fmt.Printf("Error updating a document: %s\n", err.Error())
}
return nil
}
2.6 逻辑查询
根据自定义的dsl进行数据检索,该函数不支持滚动,只能进行单次检索,每次最多1W条:
// 只返回设定的条数,最多10000
func (es *EsInstance) Search(indexName string, size int, query elastic.Query) ([]Record, error) {
var json = jsoniter.ConfigCompatibleWithStandardLibrary
records := make([]Record, 0)
if size > 10000 {
size = 10000
} else if size < 1 {
size = 1
}
searchResult, err := es.Client.Search().Index(indexName).Query(query).Size(size).Do(context.Background())
if err != nil {
fmt.Printf("Error searching: %s\n", err.Error())
return records, err
} else {
fmt.Println("Search successful!")
}
if searchResult.Hits.TotalHits.Value > 0 {
for _, hit := range searchResult.Hits.Hits {
record := &Record{}
err := json.Unmarshal(hit.Source, &record.Source)
if err != nil {
fmt.Printf("Error unmarshalling the source: %s\n", err.Error())
} else {
record.Id = hit.Id
records = append(records, *record)
}
}
}
return records, err
}
2.7 滚动查询(批量查询)
滚动查询的意思是,分批多次查询,直到检索到全部数据:
// 滚动搜索,每次最多1000条
func (es *EsInstance) SearchScroll(indexName string, size int, query elastic.Query) ([]Record, error) {
var json = jsoniter.ConfigCompatibleWithStandardLibrary
records := make([]Record, 0)
if size > 1000 {
size = 1000
} else if size < 1 {
size = 1
}
searchResult, err := es.Client.Scroll(indexName).Query(query).Size(size).Do(context.Background())
if err != nil {
fmt.Printf("Error searching: %s\n", err.Error())
return records, err
}
if searchResult.Hits.TotalHits.Value > 0 {
for _, hit := range searchResult.Hits.Hits {
record := &Record{}
err := json.Unmarshal(hit.Source, &record.Source)
if err != nil {
fmt.Printf("Error unmarshalling the source: %s\n", err.Error())
} else {
record.Id = hit.Id
record.Index = hit.Index
record.Type = hit.Type
records = append(records, *record)
}
}
}
scrollId := searchResult.ScrollId
fmt.Printf("\n..........begin scroll...........\n")
for {
scrollResult, err := es.Client.Scroll().ScrollId(scrollId).Do(context.Background())
if err != nil {
fmt.Printf("Error searching: %s\n", err.Error())
return records, err
}
// 检查是否还有结果
if scrollResult.Hits.TotalHits.Value == 0 {
break
}
// 更新滚动ID
scrollId = scrollResult.ScrollId
// 处理结果
for _, hit := range scrollResult.Hits.Hits {
record := &Record{}
err := json.Unmarshal(hit.Source, &record.Source)
if err != nil {
fmt.Printf("Error unmarshalling the source: %s\n", err.Error())
} else {
record.Id = hit.Id
record.Index = hit.Index
record.Type = hit.Type
records = append(records, *record)
}
}
}
es.Client.ClearScroll(scrollId).Do(context.Background())
return records, err
}
这个函数,只是演示了滚动查询的操作步骤,在实际使用中,不能这样操作,很有可能会出现因为数据量过大导致的内存不够问题,理想的操作步骤应该是滚动一次处理一次数据,而不是把所有数据全部检索出来单次返回。
2.8 批量插入数据
为了提升操作es的性能,批量操作肯定是首选,批量操作主要使用Bulk的api,如下:
func (es *EsInstance) BatchInserRecords(indexName string, records []Record) error {
req := es.Client.Bulk().Index(indexName)
for _, record := range records {
u := &record.Source
doc := elastic.NewBulkIndexRequest().Id(record.Id).Doc(u)
req.Add(doc)
}
if req.NumberOfActions() < 0 {
fmt.Printf("no actions to bulk insert\n")
return nil
}
if _, err := req.Do(context.Background()); err != nil {
fmt.Println("bulk insert error:" + err.Error())
return err
}
return nil
}
2.9 批量更新文档
批量更新和批量插入大同小异,值更换一个api即可:
func (es *EsInstance) BatchUpdateRecords(indexName string, records []Record) error {
req := es.Client.Bulk().Index(indexName)
for _, record := range records {
u := &record.Source
doc := elastic.NewBulkUpdateRequest().Id(record.Id).Doc(u)
req.Add(doc)
}
if req.NumberOfActions() < 0 {
fmt.Printf("no actions to bulk insert\n")
return nil
}
if _, err := req.Do(context.Background()); err != nil {
fmt.Println("bulk insert error:" + err.Error())
return err
}
return nil
}
2.10 批量删除文档
func (es *EsInstance) BatchDeleteRecords(indexName string, records []Record) error {
req := es.Client.Bulk().Index(indexName)
for _, record := range records {
doc := elastic.NewBulkDeleteRequest().Id(record.Id)
req.Add(doc)
}
if req.NumberOfActions() < 0 {
fmt.Printf("no actions to bulk insert\n")
return nil
}
if _, err := req.Do(context.Background()); err != nil {
fmt.Println("bulk insert error:" + err.Error())
return err
}
return nil
}
3、检索条件设置
一般情况下,bool查询使用的比较多,就以bool查询为例说明,构造bool查询:
boolQuery := elastic.NewBoolQuery()
增加一个match查询条件:
boolQuery.Filter(elastic.NewMatchQuery("tag_name", "turn_left"))
增加一个range查询:
boolQuery.Filter(elastic.NewRangeQuery("start_time").Gte(1723737600000))
其他各类的检索条件我们可以根据自身需要进行设置,如果对dsl有一定的了解,这些api使用起来非常简单。
4、测试
假设要检索某辆车、某个时间段内路口左转的数据,构造dsl代码如下:
boolQuery.Filter(elastic.NewMatchQuery("tag_name", "turn_left"))
boolQuery.Filter(elastic.NewMatchQuery("car_id", "京A0001"))
boolQuery.Filter(elastic.NewRangeQuery("start_time").Gte(1723737600000))
boolQuery.Filter(elastic.NewRangeQuery("end_time").Lte(1723823999000))
之后调用2.7章节的滚动查询函数,则可以检索出所有数据:
SearchScroll("index_2024", 4000, boolQuery)
5、总结
以上总共介绍了10个api,基本上就满足了日常对es操作的需求,测试、验证适用于使用单条检索,这样方便验证数据的准确性,批量操作用于验证数据、逻辑没有问题后的操作,支持批量主要是为了提升效率,以快速完成对数据的操作。