golang + ElasticSearch(docker)を触ってみる

はじめに

検索機能をelastic searchを使って実装したいけど、何ができるかわからないし、どうやって実装して良いのかもわからなかったので golangとelastic searchで簡単な実装をしてみようと思います

対象

  • golang + elastic searchに興味がある人、初めて触る人

環境

ツール バージョン
go 1.16.3
docker Engine 20.10.6
docker compose 1.29.1

コンテナを立ち上げる

docker-compose.yml

version: "3.8"

services: 
   es:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.13.1
    container_name: elasticsearch
    environment:
      - discovery.type=single-node
    volumes:
      - ./es/data:/usr/share/elasticsearch/data
    ports:
      - 9200:9200
      - 9300:9300

docker-compose up

参考: Install Elasticsearch with Docker | Elasticsearch Guide [7.13] | Elastic

Infoを取得してみる

まずはREADMEに書いてあるUsageを参考にコードを書いていきます

func GetInfo() {
    es, err := elasticsearch.NewDefaultClient()
    if err != nil {
        log.Fatal(err)
    }
    res, err := es.Info()
    if err != nil {
        log.Fatal(err)
    }
    defer res.Body.Close()
    var r map[string]interface{}
    if err := json.NewDecoder(res.Body).Decode(&r); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%+v\n", r)
}

output

map[cluster_name:docker-cluster cluster_uuid:T38_LnjTS_aBtlDa1lMZIA name:0cdf44e15374 tagline:You Know, for Search version:map[build_date:2021-05-28T17:40:59.346932922Z build_flavor:default build_hash:9a7758028e4ea59bcab41c12004603c5a7dd84a9 build_snapshot:false build_type:docker lucene_version:8.8.2 minimum_index_compatibility_version:6.0.0-beta1 minimum_wire_compatibility_version:6.8.0 number:7.13.1]]

Q, elastic searchのURLなど指定していないけど、なぜアクセスできるのか
A, 何も指定していない時はhttp://localhost:9200が使用されるから

// NewDefaultClient creates a new client with default options.
// It will use http://localhost:9200 as the default address.
// It will use the ELASTICSEARCH_URL environment variable, if set,
// to configure the addresses; use a comma to separate multiple URLs.

go-elasticsearch/elasticsearch.go at master · elastic/go-elasticsearch · GitHub

データをpostしてみる

func ExampleRequest() {
    es, err := elasticsearch.NewDefaultClient()
    if err != nil {
        log.Fatal(err)
    }

    req := esapi.IndexRequest{
        Index:      "test",
        DocumentID: strconv.Itoa(1),
        Body:       strings.NewReader(`{"test": "hogehoge"}`),
    }

    res, err := req.Do(context.Background(), es)
    if err != nil {
        log.Fatal(err)
    }
    defer res.Body.Close()
    if res.IsError() {
        log.Fatalf("failed to request %+v\n", res)
    }
    var r map[string]interface{}
    if err := json.NewDecoder(res.Body).Decode(&r); err != nil {
        log.Fatalf("Error parsing the response body: %s", err)
    }
    log.Printf("[%s] %s; version=%d", res.Status(), r["result"], int(r["_version"].(float64)))
}

output

[201 Created] created; version=1

データを取得してみる

func ExampleSearch() {
    es, err := elasticsearch.NewDefaultClient()
    if err != nil {
        log.Fatal(err)
    }

    var buf bytes.Buffer
    query := map[string]interface{}{
        "query": map[string]interface{}{
            "match": map[string]interface{}{
                "test": "hogehoge",
            },
        },
    }
    if err := json.NewEncoder(&buf).Encode(query); err != nil {
        log.Fatalf("Error encoding query: %s", err)
    }
    res, err := es.Search(
        es.Search.WithContext(context.Background()),
        es.Search.WithIndex("test"),
        es.Search.WithBody(&buf),
        es.Search.WithTrackTotalHits(true),
        es.Search.WithPretty(),
    )
    if err != nil {
        log.Fatalf("Error getting response: %s", err)
    }
    defer res.Body.Close()
    if res.IsError() {
        var e map[string]interface{}
        if err := json.NewDecoder(res.Body).Decode(&e); err != nil {
            log.Fatalf("Error parsing the response body: %s", err)
        }
        // Print the response status and error information.
        log.Fatalf("[%s] %s: %s",
            res.Status(),
            e["error"].(map[string]interface{})["type"],
            e["error"].(map[string]interface{})["reason"],
        )
    }

    var r map[string]interface{}
    if err := json.NewDecoder(res.Body).Decode(&r); err != nil {
        log.Fatalf("Error parsing the response body: %s", err)
    }

    fmt.Printf("%v\n", r)

    log.Printf(
        "[%s] %d hits; took: %dms",
        res.Status(),
        int(r["hits"].(map[string]interface{})["total"].(map[string]interface{})["value"].(float64)),
        int(r["took"].(float64)),
    )

    for _, hit := range r["hits"].(map[string]interface{})["hits"].([]interface{}) {
        log.Printf(" * ID=%s, %s", hit.(map[string]interface{})["_id"], hit.(map[string]interface{})["_source"])
    }
}

output

map[_shards:map[failed:0 skipped:0 successful:1 total:1] hits:map[hits:[map[_id:1 _index:test _score:0.2876821 _source:map[test:hogehoge] _type:_doc]] max_score:0.2876821 total:map[relation:eq value:1]] timed_out:false took:3]
2021/06/06 20:09:59 [200 OK] 1 hits; took: 3ms
2021/06/06 20:09:59  * ID=1, map[test:hogehoge]

まとめ

ほとんどREADMEのUsageに書いてあるコードを使用しました
環境構築やgo-elasticsearchの使い方がわからないと言う人が多いと思うので、この記事を参考に導入の手助けになればと思います