ブログ

ElasticsearchとGoを使用して、ホリネズミ狩りをハイブリッド検索

Elasticsearch と Elasticsearch Go クライアントを使用してキーワード検索とベクター検索を組み合わせることでハイブリッド検索を実現する方法を学びます。

このシリーズの前回の記事では、Elasticsearch Go クライアントを従来のキーワード検索ベクター検索に使用する方法を説明しました。この第 3 部では、ハイブリッド検索について説明します。Elasticsearch Elasticsearch Go クライアント を使用して、ベクトル検索とキーワード検索の両方を組み合わせる方法の 例を 紹介します。

要件

このシリーズのパート 1 と同様に、この例では次の前提条件が必要です。

  1. Goバージョン1.21以降のインストール

  2. Go ドキュメントに記載されている推奨構造とパッケージ管理を使用して、独自の Go リポジトリを作成します。

  3. 独自の Elasticsearch クラスターを作成し、Wikipedia のフレンドリーな Gopher を含む、 げっ歯類ベースのページ セットを設定します。

Wikipedia Gopherページ

Elasticsearchへの接続

繰り返しになりますが、私たちの例では、Go クライアントが提供するTyped APIを使用します。クエリに対して安全な接続を確立するには、次のいずれかを使用してクライアントを構成する必要があります。

  1. Elastic Cloud を利用する場合の Cloud ID と API キー

  2. クラスターURL、ユーザー名、パスワード、証明書

Elastic Cloud にあるクラスターに接続すると、次のようになります。

func GetElasticsearchClient() (*elasticsearch.TypedClient, error) {
	var cloudID = os.Getenv("ELASTIC_CLOUD_ID")
	var apiKey = os.Getenv("ELASTIC_API_KEY")

	var es, err = elasticsearch.NewTypedClient(elasticsearch.Config{
		CloudID: cloudID,
		APIKey:  apiKey,
		Logger:  &elastictransport.ColorLogger{os.Stdout, true, true},
	})

	if err != nil {
		return nil, fmt.Errorf("unable to connect: %w", err)
	}

	return es, nil
}

後続のセクションで示すように、 client接続は検索に使用できます。

ハイブリッド検索の手動ブースティング

検索アルゴリズムのセットを組み合わせる場合、従来のアプローチでは、各クエリ タイプを強化するために定数を手動で構成していました。具体的には、クエリごとに係数が指定され、結合された結果セットが予想されるセットと比較され、クエリのリコールが決定されます。次に、いくつかの要因セットを繰り返し、希望する状態に最も近いものを選択します。

たとえば、 0.8倍の係数でブーストされた単一のテキスト検索クエリと、 0.2倍の係数が低い knn クエリを組み合わせるには、次の例に示すように、両方のクエリ タイプでBoostフィールドを指定します。

func HybridSearchWithBoost(client *elasticsearch.TypedClient, term string) ([]Rodent, error) {
	var k = 10
	var numCandidates = 10
	var knnBoost float32 = 0.2
	var queryBoost float32 = 0.8

	res, err := client.Search().
		Index("vector-search-rodents").
		Knn(types.KnnSearch{
			Field:         "text_embedding.predicted_value",
			Boost:         &knnBoost,
			K:             &k,
			NumCandidates: &numCandidates,
			QueryVectorBuilder: &types.QueryVectorBuilder{
				TextEmbedding: &types.TextEmbedding{
					ModelId:   "sentence-transformers__msmarco-minilm-l-12-v3",
					ModelText: term,
				},
			}}).
		Query(&types.Query{
			Match: map[string]types.MatchQuery{
				"title": {
					Query: term,
					Boost: &queryBoost,
				},
			},
		}).
		Do(context.Background())

	if err != nil {
		return nil, err
	}

	return getRodents(res.Hits.Hits)
}

各クエリのBoostオプションで指定された係数がドキュメント スコアに追加されます。一致クエリのスコアを knn クエリよりも大きな係数で増加させることにより、キーワード クエリの結果の重み付けがより大きくなります。

手動によるブースティングの課題は、特に検索の専門家でない場合、望ましい結果セットにつながる要因を見つけ出すために調整が必要になることです。ランダムな値を試してみて、希望する結果セットに近づくかどうかを調べるだけです。

ハイブリッド検索とGoクライアントにおける相互ランク融合

Reciprocal Rank Fusion (RRF) は、Elasticsearch 8.9 のハイブリッド検索のテクニカル プレビューとしてリリースされました。チューニングに関連する学習曲線を短縮し、結果セットを最適化するための要素の実験にかかる時間を短縮することを目的としています。

RRF では、以下のアルゴリズムでスコアをブレンドしてドキュメント スコアが再計算されます。

score := 0.0
// q is a query in the set of queries (vector and keyword search)
for _, q := range queries {
    // result(q) is the results 
    if document in result(q) {
        // k is a ranking constant (default 60)
        // rank(result(q), d) is the document's rank within result(q) 
        // range from 1 to the window_size (default 100)
        score +=  1.0 / (k + rank(result(q), d))
    }
}

return score

RRF を使用する利点は、Elasticsearch 内で適切なデフォルト値を利用できることです。ランキング定数kデフォルトは60です。大規模なデータ セットを検索するときに、返されるドキュメントの関連性とクエリ パフォーマンスの間のトレードオフを提供するために、検討される各クエリの結果セットのサイズはwindow_sizeの値に制限されます。この値は、ドキュメントで説明されているように、デフォルトで100になります。

k また、 windows_size 、以下の例のように、Go クライアントのRankメソッド内のRrf構成内で構成することもできます。

func HybridSearchWithRRF(client *elasticsearch.TypedClient, term string) ([]Rodent, error) {
	var k = 10
	var numCandidates = 10

	// Minimum required window size for the default result size of 10
	var windowSize int64 = 10
	var rankConstant int64 = 42

	res, err := client.Search().
		Index("vector-search-rodents").
		Knn(types.KnnSearch{
			Field:         "text_embedding.predicted_value",
			K:             &k,
			NumCandidates: &numCandidates,
			QueryVectorBuilder: &types.QueryVectorBuilder{
				TextEmbedding: &types.TextEmbedding{
					ModelId:   "sentence-transformers__msmarco-minilm-l-12-v3",
					ModelText: term,
				},
			}}).
		Query(&types.Query{
			Match: map[string]types.MatchQuery{
				"title": {Query: term},
			},
		}).
		Rank(&types.RankContainer{
			Rrf: &types.RrfRank{
				WindowSize:   &windowSize,
				RankConstant: &rankConstant,
			},
		}).
		Do(context.Background())

	if err != nil {
		return nil, err
	}

	return getRodents(res.Hits.Hits)
}

まとめ

ここでは、 Elasticsearch Go クライアントを使用して Elasticsearch でベクトル検索とキーワード検索を組み合わせる方法について説明しました。

このシリーズのすべてのコードについては、 GitHub リポジトリをご覧ください。まだご覧になっていない方は、このシリーズのすべてのコードについてはパート 1パート 2をご覧ください。

楽しいホリネズミ狩りを!

各種資料

  1. Elasticsearchガイド

  2. Elasticsearch Goクライアント

  3. ベクトル検索とは何ですか?| 弾性

  4. 相互ランク融合

関連記事

LINQ to Elasticsearch ES|QL:C#を記述してElasticsearchをクエリ

Florian Bernd

Elasticsearch の NLP とベクトル検索によるチャットボット機能の強化

Priscilla Parodi

OpenSearchとElasticsearchの違い:ベクトル検索のパフォーマンス比較

Ugo Sangiorgi

AI盗作:Elasticsearchによる盗作検出

Priscilla Parodi

高度なRAGテクニックパート2:クエリとテスト

Han Xiang Choong

最先端の検索体験を構築する準備はできましたか?

十分に高度な検索は 1 人の努力だけでは実現できません。Elasticsearch は、データ サイエンティスト、ML オペレーター、エンジニアなど、あなたと同じように検索に情熱を傾ける多くの人々によって支えられています。ぜひつながり、協力して、希望する結果が得られる魔法の検索エクスペリエンスを構築しましょう。

はじめましょう