Introduction

前回vLLM + Open WebUI によるローカル LLM 環境を構築したが、次は OpenSearch を使って RAG を試してみる。

RAG に必要なのは下記。

  • Retrieval
    • 質問からセマンティックに意味を理解し必要なデータを取ってくる
      • ベクトルデータベース (Vector Database)
      • 埋め込みモデル (Embedding Model)
      • 再ランカー (Re-ranking、Rerank)
        • 検索結果のランクを調整する
  • Augmented
    • プロンプトを補完するようなコンテキストを前段の結果から付与し最終的なプロンプトを生成
  • Generation
    • プロンプトから応答を生成
      • LLM

サンプルコードは https://github.com/takuya-takeuchi/Demo/tree/master/AI/RAG/LlamaIndex/00_GetStarted

How to do?

vLLM か Ollama をモデルの推論に使えるが今回は Ollama を採用。
Ollama は前回セットアップ済みなので、 OpenSearch、埋め込みモデルを用意し、python スクリプトから RAG の性能を確認してみる。

1. OpenSearch のインストール

docker で自動起動も含めて設定。
$OPENSEARCH_INITIAL_ADMIN_PASSWORD には、OpenSearch の管理者ユーザのパスワードを設定する。
パスワードは 8 文字以上で、大文字、小文字、数字、および強力な特殊文字をそれぞれ1文字以上含む という条件を満たすこと。
さもなくば、起動時にエラーになる (1敗目)
P@ssword123 は条件を満たしているが、 P@ssword 部分が頻出ワードのため弱いパスワードとして判断されて弾かれる (2敗目)。
-e "plugins.security.disabled=true" を外すと、https を強制され、後述の RAG で詰む (3敗目)

1
2
3
4
5
6
7
8
9
10
11
12
$ mkdir ~/.cache/opensearch
$ sudo chown -R 1000:1000 ~/.cache/opensearch
$ sudo docker run -d \
-p 9200:9200 \
-e "discovery.type=single-node" \
-e "plugins.security.disabled=true" \
-e "OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_INITIAL_ADMIN_PASSWORD}" \
-v $HOME/.cache/opensearch/data:/usr/share/opensearch/data \
/usr/share/opensearch/logs
--name opensearch \
--restart always \
opensearchproject/opensearch:latest

ホスト側の公開ポートはお好みで。
しばらくしたら起動が完了するので疎通確認を行う。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ curl -X GET "http://localhost:9200" -H "Authorization: Basic $(echo -n "admin:${OPENSEARCH_INITIAL_ADMIN_PASSWORD}" | base64)" --insecure
{
"name" : "332f854ae748",
"cluster_name" : "docker-cluster",
"cluster_uuid" : "iYbkUlcYSyeo_Z_RYcEEWQ",
"version" : {
"distribution" : "opensearch",
"number" : "3.6.0",
"build_type" : "tar",
"build_hash" : "4ca747d8d47f80162db323019357447126732e35",
"build_date" : "2026-04-04T06:02:53.715432718Z",
"build_snapshot" : false,
"lucene_version" : "10.4.0",
"minimum_wire_compatibility_version" : "2.19.0",
"minimum_index_compatibility_version" : "2.0.0"
},
"tagline" : "The OpenSearch Project: https://opensearch.org/"
}

GUI が欲しいなら opensearchproject/opensearch-dashboards も起動する。

2. データの準備

LLM のモデルにもよるが、モデルがカバーしていない知識に関するデータを用意する。
今回は 2026年度の JRA (日本中央競馬会) 主催の重賞レースの情報を TSV 形式で 1 ファイル用意した。

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
40
41
42
43
44
45
46
47
月日	グレード	レース名	競馬場	性齢	コース	距離	優勝馬	騎手
2026年1月4日 G3 中山金杯 中山 4歳以上 芝 2,000m カラマティアノス 津村明秀
2026年1月4日 G3 京都金杯 京都 4歳以上 芝 1,600m ブエナオンダ 川田将雅
2026年1月11日 G3 フェアリーS 中山 3歳牝 芝 1,600m ブラックチャリス 津村明秀
2026年1月12日 G3 シンザン記念 京都 3歳 芝 1,600m サンダーストラック T.ハマーハンセン
2026年1月18日 G3 京成杯 中山 3歳 芝 2,000m グリーンエナジー 戸崎圭太
2026年1月18日 G2 日経新春杯 京都 4歳以上 芝 2,400m ゲルチュタール 坂井瑠星
2026年1月24日 G3 小倉牝馬S 小倉 4歳以上牝 芝 2,000m ジョスラン C.ルメール
2026年1月25日 G2 アメリカJCC 中山 4歳以上 芝 2,200m ショウヘイ 川田将雅
2026年1月25日 G2 プロキオンS 京都 4歳以上 ダート 1,800m ロードクロンヌ 横山和生
2026年2月1日 G3 根岸S 東京 4歳以上 ダート 1,400m ロードフォンス 横山和生
2026年2月1日 G3 シルクロードS 京都 4歳以上 芝 1,200m フィオライア 太宰啓介
2026年2月10日 G3 きさらぎ賞 京都 3歳 芝 1,800m ゾロアストロ T.ハマーハンセン
2026年2月10日 G3 東京新聞杯 東京 4歳以上 芝 1,600m トロヴァトーレ C.ルメール
2026年2月14日 G3 クイーンC 東京 3歳牝 芝 1,600m ドリームコア C.ルメール
2026年2月14日 J・G3 小倉ジャンプS 小倉 4歳以上 障害 3,390m サンデイビス 上野翔
2026年2月15日 G3 共同通信杯 東京 3歳 芝 1,800m リアライズシリウス 津村明秀
2026年2月15日 G2 京都記念 京都 4歳以上 芝 2,200m ジューンテイク 藤岡佑介
2026年2月21日 G3 ダイヤモンドS 東京 4歳以上 芝 3,400m スティンガーグラス C.ルメール
2026年2月21日 G3 阪急杯 阪神 4歳以上 芝 1,400m ソンシ 川田将雅
2026年2月22日 G1 フェブラリーS 東京 4歳以上 ダート 1,600m コスタノヴァ C.ルメール
2026年2月22日 G3 小倉大賞典 小倉 4歳以上 芝 1,800m タガノデュード 古川吉洋
2026年2月28日 G3 オーシャンS 中山 4歳以上 芝 1,200m ペアポルックス 岩田康誠
2026年3月1日 G2 中山記念 中山 4歳以上 芝 1,800m レーベンスティール 戸崎圭太
2026年3月1日 G2 チューリップ賞 阪神 3歳牝 芝 1,600m タイセイボーグ 西村淳也
2026年3月7日 G3 中山牝馬S 中山 4歳以上牝 芝 1,800m エセルフリーダ 武藤雅
2026年3月7日 G2 フィリーズレビュー 阪神 3歳牝 芝 1,400m ギリーズボール 西塚洸二
2026年3月8日 G2 弥生賞 中山 3歳 芝 2,000m バステール 川田将雅
2026年3月14日 J・G2 阪神スプリングジャンプ 阪神 4歳以上 障害 3,900m ディナースタ 高田潤
2026年3月15日 G2 スプリングS 中山 3歳牡・牝 芝 1,800m アウダーシア 津村明秀
2026年3月15日 G2 金鯱賞 中京 4歳以上 芝 2,000m シェイクユアハート 古川吉洋
2026年3月21日 G3 フラワーC 中山 3歳牝 芝 1,800m スマートプリエール 原優介
2026年3月21日 G3 ファルコンS 中京 3歳 芝 1,400m ダイヤモンドノット 川田将雅
2026年3月22日 G2 阪神大賞典 阪神 4歳以上 芝 3,000m アドマイヤテラ 武豊
2026年3月22日 G3 愛知杯 中京 4歳以上牝 芝 1,400m アイサンサン 幸英明
2026年3月28日 G2 日経賞 中山 4歳以上 芝 2,500m マイユニバース 横山典弘
2026年3月28日 G3 毎日杯 阪神 3歳 芝 1,800m アルトラムス 岩田望来
2026年3月29日 G3 マーチS 中山 4歳以上 ダート 1,800m サンデーファンデー 角田大和
2026年3月29日 G1 高松宮記念 中京 4歳以上 芝 1,200m サトノレーヴ C.ルメール
2026年4月4日 G3 ダービー卿チャレンジT 中山 4歳以上 芝 1,600m スズハローム 藤懸貴志
2026年4月4日 G3 チャーチルダウンズC 阪神 3歳 芝 1,600m アスクイキゴミ 坂井瑠星
2026年4月5日 G1 大阪杯 阪神 4歳以上 芝 2,000m クロワデュノール 北村友一
2026年4月11日 G2 ニュージーランドT 中山 3歳牡・牝 芝 1,600m レザベーション 原優介
2026年4月11日 G2 阪神牝馬S 阪神 4歳以上牝 芝 1,600m エンブロイダリー C.ルメール
2026年4月12日 G1 桜花賞 阪神 3歳牝 芝 1,600m スターアニス 松山弘平
2026年4月18日 J・G1 中山グランドジャンプ 中山 4歳以上 障 4,260m エコロデュエル 草野太郎
2026年4月18日 G3 アンタレスS 阪神 4歳以上 ダート 1,800m ムルソー 坂井瑠星

これを適当なフォルダに放り込む (今回は documents/日本/JRA/重賞.tsv として用意)。

3. スクリプトの用意

下記のコードを用意。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# -*- coding: utf-8 -*-
#!/usr/bin/python
import argparse
import asyncio
import csv
import os
from pathlib import Path

from llama_index.core import Document, SimpleDirectoryReader, StorageContext, VectorStoreIndex
from llama_index.core.prompts import PromptTemplate
from llama_index.embeddings.ollama import OllamaEmbedding
from llama_index.llms.ollama import Ollama
from llama_index.vector_stores.opensearch import (
OpensearchVectorClient,
OpensearchVectorStore,
)

def detect_csv_dialect(sample_text: str) -> csv.Dialect:
try:
return csv.Sniffer().sniff(sample_text, delimiters=[",", "\t", ";"])
except csv.Error:
class DefaultDialect(csv.excel):
delimiter = ","
return DefaultDialect()

def row_to_text(row: dict[str, str]) -> str:
parts = []
for key, value in row.items():
key = "" if key is None else str(key).strip()
value = "" if value is None else str(value).strip()
if key:
parts.append(f"{key}: {value}")
return "、".join(parts)

def clean_row(row: dict) -> dict[str, str]:
cleaned = {}
for key, value in row.items():
if key is None:
continue

k = str(key).strip()
if not k:
continue

v = "" if value is None else str(value).strip()
cleaned[k] = v

return cleaned

def load_tabular_file_as_documents(path: str) -> list[Document]:
file_path = Path(path)

with open(file_path, "r", encoding="utf-8-sig", newline="") as f:
sample = f.read(4096)
f.seek(0)

dialect = detect_csv_dialect(sample)
reader = csv.DictReader(f, dialect=dialect)

documents = []
for i, row in enumerate(reader, start=1):
metadata = clean_row(row)

if not metadata:
continue

text = row_to_text(metadata)

doc = Document(
text=text,
metadata=metadata,
)
documents.append(doc)

return documents

def load_documents(documents_dir: str):
documents = []

tabular_files = []
other_files = []

for path in Path(documents_dir).rglob("*"):
if not path.is_file():
continue

if path.suffix.lower() in [".csv", ".tsv"]:
tabular_files.append(path)
else:
other_files.append(path)

for path in tabular_files:
documents.extend(load_tabular_file_as_documents(str(path)))

if other_files:
other_docs = SimpleDirectoryReader(
input_files=[str(p) for p in other_files]
).load_data(show_progress=True)
documents.extend(other_docs)

return documents

def get_args():
parser = argparse.ArgumentParser()
parser.add_argument("-e", "--embedding_model", type=str, required=True)
parser.add_argument("-s", "--embedding_server", type=str, required=True)
parser.add_argument("-l", "--llm_model", type=str, required=True)
parser.add_argument("-r", "--llm_server", type=str, required=True)
parser.add_argument("-v", "--vector_database_server", type=str, required=True)
parser.add_argument("-q", "--query", type=str, required=True)
parser.add_argument("-t", "--threshold", type=float, default=0.6)
parser.add_argument("-k", "--rank", type=int, default=100)
parser.add_argument("-p", "--prompt_file", type=str, default="qa_template.txt")
parser.add_argument("-d", "--documents_dir", type=str)
return parser.parse_args()

if __name__ == '__main__':
args = get_args()
embedding_model = args.embedding_model
embedding_server = args.embedding_server
llm_model = args.llm_model
llm_server = args.llm_server
vector_database_server = args.vector_database_server
query = args.query
threshold = args.threshold
rank = args.rank
prompt_file = args.prompt_file
documents_dir = args.documents_dir

print("Arguments")
print(" embedding_model: {}".format(embedding_model))
print(" embedding_server: {}".format(embedding_server))
print(" llm_model: {}".format(llm_model))
print(" llm_server: {}".format(llm_server))
print(" vector_database_server: {}".format(vector_database_server))
print(" query: {}".format(query))
print(" threshold: {}".format(threshold))
print(" rank: {}".format(rank))
print(" prompt_file: {}".format(prompt_file))
print(" documents_dir: {}".format(documents_dir))

embedding_model = OllamaEmbedding(model_name=embedding_model, base_url=embedding_server)
llm_model = Ollama(model=llm_model, base_url=llm_server, request_timeout=120.0)

index = "demo"
dim = 1024
text_field = "content"
embedding_field = "embedding"

client = None
vector_store = None

try:
with open(prompt_file, "r", encoding="utf-8") as f:
prompt_text = f.read()

client = OpensearchVectorClient(
endpoint=vector_database_server,
index=index,
dim=dim,
embedding_field=embedding_field,
text_field=text_field,
)

vector_store = OpensearchVectorStore(client)

if documents_dir and os.path.exists(documents_dir):
documents = load_documents(documents_dir)
storage_context = StorageContext.from_defaults(vector_store=vector_store)
index = VectorStoreIndex.from_documents(
documents=documents,
storage_context=storage_context,
embed_model=embedding_model,
show_progress=False,
)
else:
index = VectorStoreIndex.from_vector_store(
vector_store=vector_store,
embed_model=embedding_model
)

retriever = index.as_retriever(similarity_top_k=rank)
nodes = retriever.retrieve(query)

# print("retrieved nodes:", len(nodes))
# for i, node in enumerate(nodes):
# print(f"[{i}] score={node.score!r}")
# try:
# print(f"[{i}] text={node.node.get_content()[:200]!r}")
# except Exception as e:
# print(f"[{i}] text=<error: {e}>")
# print(f"[{i}] metadata={getattr(node.node, 'metadata', None)}")

# スコアしきい値は要調整
has_good_context = len(nodes) > 0 and any(
(node.score is not None and node.score > threshold) for node in nodes
)

text_qa_template = PromptTemplate(prompt_text)

if has_good_context:
print(f"💡 LLM returns good context!!")

query_engine = index.as_query_engine(
llm=llm_model,
similarity_top_k=3,
text_qa_template=text_qa_template,
)
res = query_engine.query(query)
answer = res.response.strip()
else:
prompt = text_qa_template.format(query_str=query, context_str="")
raw = llm_model.complete(prompt)
answer = getattr(raw, "text", str(raw)).strip()

print(f"👤: {query}\n🤖: {answer}")
finally:
try:
if vector_store is not None:
asyncio.run(vector_store.aclose())
finally:
# To prevent close() from being called again in the __del__ method of the llama-index OpenSearch integration, disable internal references.
# avoid: RuntimeWarning: coroutine 'AsyncOpenSearch.close' was never awaited
if client is not None:
if hasattr(client, "_os_async_client"):
client._os_async_client = None
if hasattr(client, "_os_client"):
client._os_client = None

処理の流れは下記となる。

  1. 埋め込みモデルの初期化
  2. LLM モデルの初期化
  3. OpenSearch の接続クライアント初期化
  4. 入力データの指定をチェック
    4.1. ドキュメントフォルダを指定していれば、指定フォルダを再帰的に検索し csv/tsv を読み取りデータベースに登録する形式に整形し、埋め込みモデルを使用してベクトルデータとして登録及びインデックスの作成
    4.2. ドキュメントフォルダを指定しない場合は、既にデータベースにベクトルデータが登録済みだと判断し、データベースからインデックスの作成
  5. インデックスを使用してクエリから検索を実行
  6. 有効な参考情報があるかどうかをチェック
    6.1. 有効な参考情報がある場合は、プロンプトに参考情報と元のクエリを埋め込み LLM で最終的な回答を生成
    6.2. 有効な参考情報がない場合は、元のクエリをそのまま使って、LLM で回答を生成

4. テスト

下記のモデルを使用。

  • 埋め込みモデル
    • kun432/cl-nagoya-ruri-large
      • 日本語に対応した埋め込みモデル。名古屋大学情報学部大学院情報学研究科で開発されたモデル。Apache 2.0 License
  • LLM モデル

いずれのモデルも Open WebUI 経由で事前にプルしておくこと。

各種サーバは下記を使用。

事前知識のみで未知の質問に回答

まず、入力データの登録なしで推論を実行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ python qa.py --embedding_model kun432/cl-nagoya-ruri-large \
--embedding_server http://192.168.11.45:11434 \
--llm_model gemma4:26b \
--llm_server http://192.168.11.45:11434 \
--vector_database_server http://192.168.11.45:9200 \
--query "2026年のヤクルトスワローズの開幕戦について説明して"
Arguments
embedding_model: kun432/cl-nagoya-ruri-large
embedding_server: http://192.168.11.45:11434
llm_model: gemma4:26b
llm_server: http://192.168.11.45:11434
vector_database_server: http://192.168.11.45:9200
query: 2026年のヤクルトスワローズの開幕戦について説明して
threshold: 0.6
rank: 100
prompt_file: qa_template.txt
documents_dir: None
👤: 2026年のヤクルトスワローズの開幕戦について説明して
🤖: 現時点では、2026年のプロ野球(NPB)の公式日程はまだ発表されていません。そのため、ヤクルトスワローズの2026年開幕戦の具体的な**対戦相手、日程、開催球場などの詳細については、現段階では不明です。**

一般的に、NPBの次年度の試合日程は、その前年の秋から冬にかけて(2025年の後半から年末頃)に発表されるのが通例です。

開幕戦の詳細を知るためには、2025年後半以降に発表される公式のシーズンスケジュールを確認する必要があります。

ハルシネーションもなく素直な回答を返してくれました。

事前知識+参考情報で未知の質問に対して回答

続いて、先述の競馬データを登録し、同じ質問を実行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ python qa.py --embedding_model kun432/cl-nagoya-ruri-large \
--embedding_server http://192.168.11.45:11434 \
--llm_model gemma4:26b \
--llm_server http://192.168.11.45:11434 \
--vector_database_server http://192.168.11.45:9200 \
--query "2026年のヤクルトスワローズの開幕戦について説明して" \
--documents_dir documents
Arguments
embedding_model: kun432/cl-nagoya-ruri-large
embedding_server: http://192.168.11.45:11434
llm_model: gemma4:26b
llm_server: http://192.168.11.45:11434
vector_database_server: http://192.168.11.45:9200
query: 2026年のヤクルトスワローズの開幕戦について説明して
threshold: 0.6
rank: 100
prompt_file: qa_template.txt
documents_dir: documents
💡 LLM returns good context!!
👤: 2026年のヤクルトスワローズの開幕戦について説明して
🤖: 提供された参考情報には、2026年のヤクルトスワローズの開幕戦に関する情報は含まれていません(参考情報は競馬のレース結果に関するものです)。

また、一般的な知識としても、2026年のプロ野球(NPB)の具体的な日程(対戦相手、開催日、球場など)は現時点ではまだ発表されていません。プロ野球のシーズン日程は通常、前年の年末(2025年後半)に発表されるため、現段階で詳細を説明することは不可能です。

回答の大枠は変わりませんが、入力されたデータが競馬に関することを理解しています。
また、競馬に関する情報を誤って使っていることもなし。
これは事前に用意した下記のテンプレート qa_template.txt が役に立っていると推測。

1
2
3
4
5
6
7
8
9
10
11
12
以下は検索で得られた参考情報です。
---------------------
{context_str}
---------------------

上の参考情報が役立つ場合は活用してください。
参考情報に答えがない、または不十分な場合は、あなた自身の一般知識も使って質問に答えてください。
ただし、参考情報に含まれる事実と矛盾しないようにしてください。

質問: {query_str}

回答:

最後に競馬に関する質問を 2 つ実施。

事前知識+参考情報で参考情報のみに含まれる質問に対して回答

武豊騎手騎乗のアドマイヤテラのレースについて質問。

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
$ python qa.py --embedding_model kun432/cl-nagoya-ruri-large \
--embedding_server http://192.168.11.45:11434 \
--llm_model gemma4:26b \
--llm_server http://192.168.11.45:11434 \
--vector_database_server http://192.168.11.45:9200 \
--query "2026年のアドマイヤテラの勝利レースについて説明して"
Arguments
embedding_model: kun432/cl-nagoya-ruri-large
embedding_server: http://192.168.11.45:11434
llm_model: gemma4:26b
llm_server: http://192.168.11.45:11434
vector_database_server: http://192.168.11.45:9200
query: 2026年のアドマイヤテラの勝利レースについて説明して
threshold: 0.6
rank: 100
prompt_file: qa_template.txt
documents_dir: None
💡 LLM returns good context!!
👤: 2026年のアドマイヤテラの勝利レースについて説明して
🤖: 2026年のアドマイヤテラの勝利レースについては、以下の通りです。

アドマイヤテラは、**2026年3月22日**に行われた**阪神大賞典**で優勝しました。

このレースの詳細は以下の通りです。
* **グレード**: G2
* **競馬場**: 阪神競馬場
* **コース**: 芝
* **距離**: 3,000m
* **出走条件**: 4歳以上
* **騎手**: 武豊

正しい回答なので問題なし。

事前知識+参考情報で参考情報のみに含まれていないが関連性の高い質問に対して回答

愛知杯に関する質問だが、参考情報に入っていない 2025年のレースについて質問。

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
$ python qa.py --embedding_model kun432/cl-nagoya-ruri-large \
--embedding_server http://192.168.11.45:11434 \
--llm_model gemma4:26b \
--llm_server http://192.168.11.45:11434 \
--vector_database_server http://192.168.11.45:9200 \
--query "2025年のJRA主催愛知杯について説明して"
Arguments
embedding_model: kun432/cl-nagoya-ruri-large
embedding_server: http://192.168.11.45:11434
llm_model: gemma4:26b
llm_server: http://192.168.11.45:11434
vector_database_server: http://192.168.11.45:9200
query: 2025年のJRA主催愛知杯について説明して
threshold: 0.6
rank: 100
prompt_file: qa_template.txt
documents_dir: None
💡 LLM returns good context!!
👤: 2025年のJRA主催愛知杯について説明して
🤖: 提供された参考情報には2026年のレースに関するデータのみが記載されており、2025年の愛知杯についての具体的な結果(優勝馬や騎手など)に関する情報は含まれていません。

一般的な知識に基づくと、愛知杯は以下のような特徴を持つJRA主催の競馬レースです。

* **グレード**: 通常はG3(グレード3)として開催されます。
* **競馬場**: 主に中京競馬場で行われます。
* **コース**: ダートコースで行われ、距離は1,600mまたは1,800mが一般的です。
* **時期**: 例年、11月下旬から12月上旬にかけて開催される、年末のダート重賞に向けた重要な一戦の一つです。

2025年の具体的なレース内容や結果については、提供された資料からは確認できません。

きちんと参考情報に入っていないことを告げた上で自身の知識に基づいて回答を実施。
しかし、事前知識が間違っているのか、微妙なミスが。

  • 時期
    • ここ 30年は 1-3月開催。10-11月開催だったのは 1982年までで、12 月開催は一度もない
  • コース
    • 距離は 1400m で、1400m になったのも 2025年でそれ以前はずっと 2000m
    • ダートだったのは初回開催から1969年までで、以降は芝

正解なのはグレードと競馬場。

5. まとめ

LlamaIndex の参考コードが大抵 load_index_from_storage によるローカルにインデックスを永続化を前提としてコピペの嵐で大変困ったが、何とか形になった。