Golang ElasticSearch库使用

Elasticsearch 是一个分布式、RESTful 风格的搜索和数据分析引擎。所以可以通过它部署自己的搜索服务,数据分析的话暂时好像还用不到。

下载官方的库

1
go get -u github.com/olivere/elastic

定义自己的索引

文档类型:

  • keyword: 表示精确值不会参与分词
  • text: 会通过分词插件或分词模式对字段进行分词,对中文来说,比较常用的是ik分词器,看了下效果,确实不错。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 索引mapping定义,这里仿微博消息结构定义
const mapping = `
{
"mappings": {
"properties": {
"user": {
"type": "keyword"
},
"message": {
"type": "text"
},
"image": {
"type": "keyword"
},
"created": {
"type": "date"
},
"tags": {
"type": "keyword"
},
"location": {
"type": "geo_point"
},
"suggest_field": {
"type": "completion"
}
}
}
}

创建客户端client:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
func main() {
// 创建client
client, err := elastic.NewClient(
elastic.SetURL("http://127.0.0.1:9200", "http://127.0.0.1:9201"),
elastic.SetBasicAuth("user", "secret")
// 设置监控检查时间间隔
elastic.SetHealthcheckInterval(10*time.Second),
// 设置请求失败最大重试次数
elastic.SetMaxRetries(5),
// 设置错误日志输出
elastic.SetErrorLog(log.New(os.Stderr, "ELASTIC ", log.LstdFlags)),
// 设置info日志输出
elastic.SetInfoLog(log.New(os.Stdout, "", log.LstdFlags))
)
if err != nil {
// Handle error
fmt.Printf("连接失败: %v\n", err)
} else {
fmt.Println("连接成功")
}

// ! 执行ES请求需要提供一个上下文对象
ctx := context.Background()

// 首先检测下你定义的索引是否存在
exists, err := client.IndexExists("yourIndex").Do(ctx)
if err != nil {
// Handle error
panic(err)
}
if !exists {
// 你的索引不存在,则创建一个
_, err := client.CreateIndex("yourIndex").BodyString(mapping).Do(ctx)
if err != nil {
// Handle error
panic(err)
}
}
}

查询,插入,更新,删除这些操作比较简单,也不是我们的用elastic的重点,所以这里主要介绍下 查询操作。

elastic的文档元数据:

  • _index - 代表当前JSON文档所属的文档名字
  • _type - 代表当前JSON文档所属的类型,虽然新版ES废弃了type的用法,但是元数据还是可以看到。
  • _id - 文档唯一Id, 如果我们没有为文档指定id,系统会自动生成
  • _source - 代表我们插入进去的JSON数据
  • _version - 文档的版本号,每修改一次文档数据,字段就会加1, 这个字段新版的ES已经不使用了
  • _seq_no - 文档的版本号, 替代老的_version字段
  • _primary_term - 文档所在主分区,这个可以跟_seq_no字段搭配实现乐观锁。

通过 term 查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 执行ES请求需要提供一个上下文对象
ctx := context.Background()

// 创建term查询条件,用于精确查询
//第一个参数是查询字段,第二个是要匹配的值
termQuery := elastic.NewTermQuery("Content", keywords)

searchResult, err := client.Search().
Index("blogs"). // 设置索引名
Query(termQuery). // 设置查询条件
Sort("Created", true). // 设置排序字段,根据Created字段升序排序,第二个参数false表示逆序
From(0). // 设置分页参数 - 起始偏移量,从第0行记录开始
Size(10). // 设置分页参数 - 每页大小
Pretty(true). // 查询结果返回可读性较好的JSON格式
Do(ctx) // 执行请求

if err != nil {
// Handle error
panic(err)
}

fmt.Printf("查询消耗时间 %d ms, 结果总数: %d\n", searchResult.TookInMillis, searchResult.TotalHits())


if searchResult.TotalHits() > 0 {
// 查询结果不为空,则遍历结果
var b1 Article
// 通过Each方法,将es结果的json结构转换成struct对象
for _, item := range searchResult.Each(reflect.TypeOf(b1)) {
// 转换成Article对象
if t, ok := item.(Article); ok {
fmt.Println(t.Title)
}
}
}
}

对于client.Search()方法会返回一个 SearchResult 结构体,它有两个方法:

  1. func (r *SearchResult) Each(typ reflect.Type) []interface{}

官方解释是它能迭代所有的命中,这样你就不用去判断nil。所以我们可以用这个方法加上类型断言,将搜索出来的结果赋值给我们定义的结构体。

eg:

1
2
3
4
5
6
7
8
var ttyp YourStruct
var ttypes []YourStruct

for _, item := range searchResult.Each(reflect.TypeOf(ttyp)) {
t := item.(YourStruct)
fmt.Printf("YourStruct by %s: %s\n", t.field1, t.field2)
ttypes.append(ttypes, t)
}
  1. func (r *SearchResult) TotalHits() int64

返回你搜索结果命中的数量,可以在循环之前进行一个判断,很简单。

另外很重要的一点就是 SearchResult 有一个字段叫 Hits , 它是一个 *SearchHits 类型, 而 SearchHits 也包含一个字段 Hits,不同的是它是 *SearchHit 类型的数组,我们继续看到 SearchHit 就比较明显了,它包含了我们上面介绍的所有文档元数据字段,还包含一个 Source 字段,这个字段是 json.RawMessage 即查询的json格式的数据,那么我们就可以通过 Unmarshal 方法赋给我们定义的结构体。

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Iterate through results
for _, hit := range searchResult.Hits.Hits {
// hit.Index contains the name of the index

// Deserialize hit.Source into your self-defined struct (could also be just a map[string]interface{}).
var t YourStruct
err := json.Unmarshal(*hit.Source, &t)
if err != nil {
// Deserialization failed
}
fmt.Printf("YourStruct by %s: %s\n", t.field1, t.field2)
}

参考资料

梯子网的,感觉很不错

官方的,不用说,就是英语看的有点累