Closed3

ローカルでOpenSearchをGoで動かしてみる

tamaco489tamaco489

前提:

  • とりあえず動かして挙動等確認したい時用のログ
  • OpenSearch にコールして、どんなレスポンス得られるのかさっと確認する時用
  • opensearch-goを使用
  • Goは1.24.0を使用

クライアントの設定に関しては公式のドキュメントにある程度掲載されている。
https://opensearch.org/docs/latest/clients/go/

以下で *opensearchapi.Client を生成し、それをもとにロジック側で検索処理を組めばとりあえず動く。
※今回はローカルでの検証を目的としているため認証等は省いている

func NewOpenSearchAPIClient() (*opensearchapi.Client, error) {
	client, err := opensearchapi.NewClient(
		opensearchapi.Config{
			Client: opensearch.Config{
				Addresses: []string{
					"http://opensearch:9200", // シングルノード前提
				},
			},
		},
	)
	if err != nil {
		return nil, err
	}
	return client, nil
}
今回検証したindexの構造としては以下、こちらのjsonを使用して、以下のようにindexを作成。
{
  "mappings": {
    "properties": {
      "id": {
        "type": "long"
      },
      "product_id": {
        "type": "long"
      },
      "user_id": {
        "type": "long"
      },
      "title": {
        "type": "text"
      },
      "content": {
        "type": "text"
      },
      "rate": {
        "type": "integer"
      },
      "created_at": {
        "type": "date",
        "format": "yyyy-MM-dd HH:mm:ss||epoch_millis"
      }
    }
  }
}
curl -i -X PUT "http://localhost:9200/product_comments" -H 'Content-Type: application/json' -d @./local/opensearch/product_comments_index.json
index作成後、検証用のドキュメントをいくつか投入する。
{ "index": { "_index": "product_comments", "_id": "70235594" } }
{ "product_id": 20010001, "user_id": 10010004, "title": "良いですね", "content": "価格と性能のバランスが良い", "rate": 4, "created_at": "2025-02-16 15:00:00" }
{ "index": { "_index": "product_comments", "_id": "70235595" } }
{ "product_id": 20010001, "user_id": 10010005, "title": "とても便利", "content": "使いやすくて毎日使っています", "rate": 5, "created_at": "2025-02-16 15:05:00" }
{ "index": { "_index": "product_comments", "_id": "70235596" } }
{ "product_id": 20010001, "user_id": 10010006, "title": "普通の性能", "content": "特に目立つ特徴はないが、使いやすい", "rate": 3, "created_at": "2025-02-16 15:10:00" }
{ "index": { "_index": "product_comments", "_id": "70235597" } }
...
curl -sX POST "http://localhost:9200/_bulk" -H "Content-Type: application/json" --data-binary @./local/opensearch/bulk_data.json

この時点で、直でOpenSearch にリクエストを送っても反応する。

ドキュメントのIDを指定してデータを取得
$ curl -sX GET "http://localhost:9200/product_comments/_doc/70235709?pretty" \
    -H 'Content-Type: application/json' | jq .
{
  "_index": "product_comments",
  "_id": "70235709",
  "_version": 1,
  "_seq_no": 178,
  "_primary_term": 6,
  "found": true,
  "_source": {
    "id": 70235709,
    "product_id": 20010001,
    "user_id": 25540992,
    "title": "デフォルトタイトル",
    "content": "デフォルトのコメント内容",
    "rate": 5,
    "created_at": "2025-02-23 15:49:00"
  }
}
商品IDを指定して、作成日が最も新しいもの1件を取得
$ curl -sX GET "localhost:9200/product_comments/_search?pretty" \
    -H 'Content-Type: application/json' \
    -d '{"query": {"match": {"product_id": 20010001}}, "sort": [{"created_at": {"order": "desc"}}], "size": 1}' | jq .
{
  "took": 11,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 105,
      "relation": "eq"
    },
    "max_score": null,
    "hits": [
      {
        "_index": "product_comments",
        "_id": "70235715",
        "_score": null,
        "_source": {
          "id": 70235715,
          "product_id": 20010001,
          "user_id": 25540992,
          "title": "思っていた以上に中々良い商品でした。",
          "content": "この商品は非常に良いです。特にデザインが素晴らしい。",
          "rate": 4,
          "created_at": "2025-02-24 12:15:58"
        },
        "sort": [
          1740399358000
        ]
      }
    ]
  }
}
tamaco489tamaco489

上記のリクエストを参考に Goアプリ経由でOpenSearch に対してリクエストの処理を組み込む。

メソッドとして定義したいので、*opensearchapi.Client を持つ構造体を作り、DIしてロジック側で呼び出せるようにする。

type productCommentUseCase struct {
	opsApiClient *opensearchapi.Client
}

func NewCreateProductComment(opsApiClient *opensearchapi.Client) IProductCommentUseCase {
	return &productCommentUseCase{
		opsApiClient: opsApiClient,
	}
}

新しくドキュメントを作成する。直近のIDを取得して、その値に+1したものドキュメントIDとする。※もっと良い方法がないのか

func (u *productCommentUseCase) CreateProductComment(ctx context.Context, request CreateProductCommentRequestObject) (CreateProductCommentResponseObject, error) {

	// 検索クエリをJSON文字列として構築
	query := strings.NewReader(`{
		"query": {
			"match_all": {}
		},
		"sort": [
			{ "created_at": { "order": "desc" } }
		],
		"size": 1
	}`)

	// 検索リクエストを作成、直近のコメントIDを取得する
	// ユーザデータ等のリレーションが多いデータの場合、トランザクションなど考慮する必要があると思うので、この類のものは別DBで管理するのが良い
	searchResult, err := u.opsApiClient.Search(
		ctx,
		&opensearchapi.SearchReq{
			Indices: []string{entity.ProductComments.String()},
			Body:    query,
		},
	)
	if err != nil {
		return nil, fmt.Errorf("failed to search for comments: %v", err)
	}

    ... 省略

	// OpenSearch にデータ投入
	idxRequest := opensearchapi.IndexReq{
		Index:      entity.ProductComments.String(),          // product_comments
		DocumentID: strconv.FormatUint(commentEntity.ID, 10), // ドキュメントIDはstring型なので
		Body:       bytes.NewReader(commentEntityJSON),       // 予め定義しておいた構造体を使用してJSON にシリアライズしたものをbytes.NewReader() に渡してストリーム化
		Params: opensearchapi.IndexParams{
			Refresh: "true",
			Timeout: 5 * time.Second,
		},
	}
	_, err = u.opsApiClient.Index(ctx, idxRequest)
	if err != nil {
		return nil, fmt.Errorf("failed to creating comment: %v", err)
	}
tamaco489tamaco489

作成したドキュメントIDを指定して、OpenSearch からデータを取得する。※以下を参考
https://github.com/opensearch-project/opensearch-go/blob/main/opensearchapi/api_document-get.go

func (u productCommentUseCase) GetProductCommentByID(ctx context.Context, request GetProductCommentByIDRequestObject) (GetProductCommentByIDResponseObject, error) {
go/blob/main/opensearchapi/api_document-get.go
	documentClient := u.opsApiClient.Document // opensearchapi.documentClient を経由する
	getResult, err := documentClient.Get(
		ctx,
		opensearchapi.DocumentGetReq{
			Index:      entity.ProductComments.String(),
			DocumentID: documentID,
		},
	)
	if err != nil {
		// OpenSearchのレスポンスに "found": false が含まれていた場合、コメントが見つからないので 404エラーを返す
		if !getResult.Found {
			return nil, fmt.Errorf("not found comment id: %v", err)
		}
		return nil, fmt.Errorf("failed to get comment by id: %v", err)
	}

ここで少しややこしいのが、ドキュメント操作の場合は*opensearch.Clientではなく、opensearchapi.documentClientを経由してGetメソッドを呼び出す必要がある。
https://github.com/opensearch-project/opensearch-go/blob/a71fcf0dd9072eaf5cdb721455cc1b62e9ab44b7/opensearchapi/opensearchapi.go#L29
*opensearch.ClientGetRequestが存在しているのでやや紛らわしい。ソースを見に行くと確かに様々な種別のClientがある

このスクラップは5ヶ月前にクローズされました