概要
semantic_textはその名の通り、Elasticsearchで意味的な検索(=ベクトル検索)を実現するためのフィールドタイプです。
このフィールドに保存されたテキストは自動的にチャンクに分割され、inference endpointを使用してベクトル化されます。またこのフィールドに対する検索ではクエリテキストも同様にベクトル化され、各チャンクのベクトルと比較されて関連性の高いドキュメントが返されます。これまでInference processorやKNN検索を組み合わせて実装していたベクトル検索を、劇的に簡単に実現できるようになります。
ただ、そのように実装が簡単になる反面、semantic_textフィールドの内部構造や動作原理を理解しないと、期待通りの検索結果が得られなかったり、細かい要件に対応できなかったりするかもしれません。このドキュメントではsemantic_textフィールドの内部構造や、検索・データ取得の方法について詳しく見てみたいと思います。
Semantic textの内部構造
チャンキングの設定があることもわかる通り、semantic_textフィールドではテキストが複数のチャンクに分割されて保存されます。このチャンク情報を効率的に管理するために、semantic_textフィールドは内部的にnestedフィールドとして実装されています。つまり、各チャンクはnestedドキュメント内部の配列として保存され、その中にベクトル埋め込みやオフセット情報が含まれています。
フィールド階層構造
semantic_textという名前のフィールドがある場合、以下のような内部構造を持ちます:
semantic_text # トップレベルフィールド
├── inference # 推論結果を格納
│ └── chunks # nestedフィールド(チャンクの配列)
│ ├── embeddings # ベクトル埋め込み(dense_vector or sparse_vector)
│ └── offset # チャンクの位置情報(0から始まるインデックス値)
具体的なフィールド名は以下のパターンに従います:
-
Chunksフィールド:
<field_name>.inference.chunks -
Embeddingsフィールド:
<field_name>.inference.chunks.embeddings -
Offsetフィールド:
<field_name>.inference.chunks.offset
nestedクエリーの内部実装
semanticクエリーを実行すると、内部的にはnestedクエリーに変換されます。スコアモードはmaxが使用され、ドキュメント内の最も関連性の高いチャンクのスコアがドキュメント全体のスコアとして採用されます。ただし、semanticクエリーで得られるレスポンスには、nestedクエリーで得られるようなinner hits情報は含まれません。従ってデフォルトではsemantic textフィールドのどのチャンクがマッチしたのか、各チャンクのスコアはいくつだったのか、といった詳細情報は取得できません。どのチャンクがマッチしたのかを知りたい場合は、後述するハイライト機能やnestedクエリーを直接使用する方法を利用する必要があります。
Semantic text型に対する検索とデータ取得
実際にsemantic_textフィールドに対して検索を行い、データを取得する方法について説明します。ただし基本的なsemanticクエリーの使い方については、以前の記事を参照してください。
基本的なSemanticクエリー
標準的なsemanticクエリーの使い方は以下の通りです。
POST /my-index/_search
{
"query": {
"semantic": {
"field": "text_semantic",
"query": "検索したいテキスト"
}
}
}
このクエリーは内部的には以下のように処理されます。
- クエリーテキストが推論エンドポイントでベクトル化
- nestedクエリーに変換
- 各チャンクのベクトルと比較
- 最高スコアのチャンクのスコアをドキュメントのスコアとして使用して、ドキュメントをランク付け
レスポンスは通常のクエリーと同様に、マッチしたドキュメントのリストが返されます。ただし上記で説明した通り、どのチャンクがマッチしたのか、各チャンクのスコアはいくつだったのかといった詳細情報は含まれません。
チャンクの取得方法
そもそも、通常の検索レスポンスには本文のテキストは含まれますが、それがどのようにチャンクに分割されているかの情報は含まれません。チャンク情報を取得するには、_sourceではなく_fieldsを参照する必要があります。
Elasticsearchでは一般的に_sourceにはインデックス元となったJSONの構造がそのまま保存される一方、_fieldsにはElasticsearchのインデックスにインデックスされたフィールド情報が保存されます。semantic_textフィールドの場合、分割されたチャンク情報は_fieldsに保存されているため、チャンク情報を取得するには_fieldsを使用する必要があります。
POST /my-index/_search
{
"_source": false,
"query": {
"match_all": {}
},
"fields": [
{
"field": "text_semantic",
"format": "chunks"
}
]
}
このようにfieldsセクションでformat: "chunks"を指定することで、インデックスされたテキストをチャンクの配列として取得できます。レスポンス例は以下の通りです:
{
"hits": {
"hits": [
{
"_id": "1",
"_score": 1.0,
"fields": {
"text_semantic": [
"チャンク1のテキスト",
"チャンク2のテキスト",
...
]
}
}
]
}
}
ハイライトで関連性の高いチャンクを取得
先ほどのformat: "chunks"では全チャンクがそのまま返されますが、関連性の高いチャンクのみを取得したい場合は、ハイライト機能を使用します。
全文検索におけるハイライト機能は、検索クエリーに含まれる単語がマッチした部分を強調表示するために使用されますが、semantic_textフィールドに対して利用する場合には、検索クエリーと関連性の高いチャンクを取得するために使用できます。
以下の例では、semanticクエリーと組み合わせて、関連性の高いチャンクを3つまで取得しています。
POST /my-index/_search
{
"query": {
"semantic": {
"field": "text_semantic",
"query": "検索クエリー"
}
},
"highlight": {
"fields": {
"text_semantic": {
"type": "semantic",
"number_of_fragments": 3,
"order": "score"
}
}
}
}
全文検索のハイライトとは異なり、semantic_textフィールドに対するハイライトは以下のようなオプションをサポートしています。
ハイライトのオプション:
-
type: "semantic": semantic text専用のハイライターを明示的に指定 -
number_of_fragments: 返すチャンク数(デフォルトは1) -
order:-
"score": スコアの高い順に返す -
"none": 元の順序で返す
-
ここで注意するべきなのは、semantic_textフィールドに対するハイライトはスコア値自体は返さないという点です。ハイライトレスポンスにはチャンクのテキストのみが含まれ、各チャンクのスコアは含まれません。スコア値を取得したい場合は、後述するnestedクエリーとInner hitsを使用する方法を検討してください。
Semantic text型に対するnestedクエリー
semantic_textの内部実装がnestedフィールドであることを利用して、直接クエリーを実行できます。この方法により、ハイライト機能では取得できないスコア値を含む詳細情報を取得できます。text_semanticというsemantic_textフィールドがある場合、以下のフィールドパスを使用します。
-
Nestedパス:
text_semantic.inference.chunks -
Embeddingsフィールド:
text_semantic.inference.chunks.embeddings
以下は、Dense Vectorに対するKNN検索を使用したnestedクエリーの例です。(ここでは使っていませんが、query_vector_builderも利用可能です。)
POST /my-index/_search
{
"query": {
"nested": {
"path": "text_semantic.inference.chunks",
"score_mode": "max",
"query": {
"knn": {
"field": "text_semantic.inference.chunks.embeddings",
"query_vector": [0.1, 0.2, 0.3, 0.4, ...],
"num_candidates": 50
}
},
"inner_hits": {
"name": "matched_chunks",
"size": 10,
"sort": [
{ "_score": { "order": "desc" } }
],
"_source": true
}
}
}
}
このクエリーでは、text_semantic.inference.chunksフィールドに対してnestedクエリーを実行し、KNN検索で関連性の高いチャンクを見つけています。score_modeはmaxに設定されており、ドキュメント全体のスコアは最も高いチャンクのスコアになります。
Inner Hitsの主なオプションは以下の通りです。
-
name: Inner hitsの識別名 -
size: 返すチャンク数 -
from: 開始位置(ページネーション) -
sort: ソート順(通常は_scoreの降順) -
_source: ソースフィールドの制御-
true: 全フィールド -
false: ソースなし -
["field1", "field2"]: 特定フィールドのみ
-
このクエリで得られるInner Hitsのレスポンスは以下のようになります。
{
"hits": {
"hits": [
{
"_id": "doc1",
"_score": 0.95,
"_source": { ... },
"inner_hits": {
"matched_chunks": {
"hits": {
"total": { "value": 8 },
"max_score": 0.95,
"hits": [
{
"_nested": {
"field": "text_semantic.inference.chunks",
"offset": 0
},
"_score": 0.95,
"_source": {
"embeddings": [...],
"offset": {
"start": 0,
"end": 150,
"field": "original_field"
}
}
},
{
"_nested": {
"field": "text_semantic.inference.chunks",
"offset": 3
},
"_score": 0.87,
"_source": { ... }
}
]
}
}
}
}
]
}
}
Inner Hitsに含まれる情報は以下のようなものです。
-
_score: 各チャンクの実際のスコア値(ハイライトでは取得不可) -
offset: チャンクの位置(何番目のチャンクか) -
_source: チャンクの完全なソース情報 -
total: マッチしたチャンクの総数 -
max_score: 最高スコア
まとめ
見てきたとおり、semantic_textフィールドは内部的にnestedフィールドとして実装されており、チャンクごとにベクトル埋め込みやオフセット情報が保存されています。検索時にはsemanticクエリーがnestedクエリーに変換され、最高スコアのチャンクのスコアがドキュメント全体のスコアとして使用されます。
内部がnestedであるため、必要に応じてnestedクエリーを直接使用して、チャンクごとのスコアや詳細情報を取得することも可能です。必要に応じて適切なクエリーを選択し、要件に応じた検索を実現してください。