Elasticsearch Inference APIとHugging Faceモデルを組み合わせて使用
推論エンドポイントを使用してElasticsearchをHugging Faceモデルに接続する方法と、セマンティック検索とチャット補完機能を備えた多言語ブログ推奨システムを構築する方法を学びましょう。
最近のアップデートで、ElasticsearchはHugging Face Inference Serviceでホストされているモデルに接続するためのネイティブ統合機能を導入しました。この記事では、この統合を構成し、大規模言語モデル(LLM)を使用して簡単なAPI呼び出しを通じて推論を実行する方法を探ります。リソース使用量と解答品質のバランスが取れた軽量汎用モデルであるSmolLM3-3Bを使用します。

要件
Elasticsearch 9.3またはElastic Cloud Serverless:これらの指示に従ってクラウド導入を作成することもできますし、
start-localクイックスタートを使うこともできます。Python 3.12:Pythonはこちらからダウンロードしてください。
Hugging Faceアクセストークン。
Hugging Face推論エンドポイントを使用したチャットの完了
まず、ElasticsearchをHugging Faceの推論エンドポイントに接続し、ブログ記事のコレクションからAIを活用したレコメンデーションを生成する実践的な例を作成します。アプリのナレッジベースには、会社のブログ記事のデータセットを使用します。これには価値のある情報が含まれていますが、多くの場合、見つけるのが困難です。
このエンドポイントでは、セマンティック検索が指定されたクエリに対して最も関連性の高い記事を取得し、Hugging Face LLMがそれらの結果に基づいて短いコンテキスト推奨を生成します。
これから構築する情報フローの概要を見ていきましょう。

この記事では、コンパクトなサイズと強力な多言語推論能力・ツール呼び出し能力を組み合わせたSmolLM3-3Bの性能を検証します。検索クエリに基づいて、一致するすべてのコンテンツ(英語とスペイン語)をLLMに送信し、検索クエリと結果に基づいたカスタムメイドの説明を含むおすすめ記事のリストを生成します。
AIによる推奨生成システムを備えた記事サイトのUIは次のようになります。

このアプリケーションの完全な実装は、リンク先のノートブックで確認できます。
Elasticsearch推論エンドポイントの構成
Elasticsearch Hugging Face推論エンドポイントを使用するには、2つの重要な要素(Hugging Face APIキーと実行中のHugging FaceエンドポイントURL)が必要です。下の画像のように表示されるはずです。
PUT _inference/chat_completions/hugging-face-smollm3-3b
{
"service": "hugging_face",
"service_settings": {
"api_key": "hugging-face-access-token",
"url": "url-endpoint"
}
}Hugging FaceのElasticsearchにおける推論エンドポイントは、 text_embedding, completion, chat_completion, と rerankの異なるタスクタイプをサポートしています。このブログ記事では、検索結果とシステムプロンプトに基づいて会話形式のレコメンデーションをモデルに生成させる必要があるため、chat_completion を使用します。このエンドポイントを使用すると、Elasticsearch APIを使用してElasticsearchから直接チャットの完了を簡単に実行できます。
POST _inference/chat_completion/hugging-face-smollm3-3b/_stream
{
"messages": [
{ "role": "user", "content": "<user prompt>" }
]
}これはアプリケーションのコアとして機能し、モデルを通過するプロンプトと検索結果を受け取ります。理論について説明したので、アプリケーションの実装を始めましょう。
Hugging Faceでの推論エンドポイントの設定
Hugging Faceモデルをデプロイするために、モデルのエンドポイントをデプロイするための簡単で高速なサービスHugging Faceワンクリック導入を使用します。これは有料サービスであり、利用には追加料金が発生する可能性があることにご注意ください。このステップでは、記事の推奨を生成するために使うモデルインスタンスが作成されます。
ワンクリックカタログからモデルを選択できます。

SmolLM3-3Bモデルを選択します。

ここから、Hugging FaceのエンドポイントURLを取得します。

Elasticsearch Hugging Faceの推論エンドポイントのドキュメントで述べられているように、テキスト生成にはOpenAI APIと互換性のあるモデルが必要です。そのため、/v1/chat/completionsのサブパスをHugging FaceのエンドポイントURLに追加する必要があります。最終的な結果は次のようになります。
https://j2g31h0futopfkli.us-east-1.aws.endpoints.huggingface.cloud/v1/chat/completionsこれで準備が整いましたので、Pythonノートブックでコーディングを開始できます。
Hugging Face APIキーの生成
Hugging Faceアカウントを作成し、以下の指示に従ってAPIトークンを取得してください。トークンの種類は、fine-grained(本番環境に推奨。特定のリソースへのアクセスのみを提供)、read(読み取り専用アクセス用)、write(読み取りおよび書き込みアクセス用)の3つから選択できます。このチュートリアルでは、推論エンドポイントを呼び出すだけでよいので、readトークンで十分です。次のステップのために、このキーを保存しておいてください。
Elasticsearch推論エンドポイントの設定
まず、Elasticsearch Pythonクライアントを宣言します。
os.environ["ELASTICSEARCH_API_KEY"] = "your-elasticsearch-api-key"
os.environ["ELASTICSEARCH_URL"] = "https://xxxx.us-central1.gcp.cloud.es.io:443"
es_client = Elasticsearch(
os.environ["ELASTICSEARCH_URL"], api_key=os.environ["ELASTICSEARCH_API_KEY"]
)次に、Hugging Faceモデルを使用するElasticsearch推論エンドポイントを作成します。このエンドポイントを使用すると、ブログ記事とモデルに渡されたプロンプトに基づいて応答を生成できます。
INFERENCE_ENDPOINT_ID = "smollm3-3b-pnz"
os.environ["HUGGING_FACE_INFERENCE_ENDPOINT_URL"] = (
"https://j2g31h0futopfkli.us-east-1.aws.endpoints.huggingface.cloud/v1/chat/completions"
)
os.environ["HUGGING_FACE_API_KEY"] = "hf_xxxxx"
resp = es_client.inference.put(
task_type="chat_completion",
inference_id=INFERENCE_ENDPOINT_ID,
body={
"service": "hugging_face",
"service_settings": {
"api_key": os.environ["HUGGING_FACE_API_KEY"],
"url": os.environ["HUGGING_FACE_INFERENCE_ENDPOINT_URL"],
},
},
)データセット
このデータセットには、クエリの対象となるブログ記事が含まれており、ワークフロー全体で使用される多言語コンテンツセットを表しています。
// Articles dataset document example:
{
"id": "6",
"title": "Complete guide to the new API: Endpoints and examples",
"author": "Tomas Hernandez",
"date": "2025-11-06",
"category": "tutorial",
"content": "This guide describes in detail all endpoints of the new API v2. It includes code examples in Python, JavaScript, and cURL for each endpoint. We cover authentication, resource creation, queries, updates, and deletion. We also explain error handling, rate limiting, and best practices. Complete documentation is available on our developer portal."
}Elasticsearch マッピング
データセットが定義されたので、ブログ記事の構造に適切にフィットするデータスキーマを作成する必要があります。Elasticsearchにデータを格納するために以下のインデックスマッピングが使用されます。
INDEX_NAME = "blog-posts"
mapping = {
"mappings": {
"properties": {
"id": {"type": "keyword"},
"title": {
"type": "object",
"properties": {
"original": {
"type": "text",
"copy_to": "semantic_field",
"fields": {"keyword": {"type": "keyword"}},
},
"translated_title": {
"type": "text",
"fields": {"keyword": {"type": "keyword"}},
},
},
},
"author": {"type": "keyword", "copy_to": "semantic_field"},
"category": {"type": "keyword", "copy_to": "semantic_field"},
"content": {"type": "text", "copy_to": "semantic_field"},
"date": {"type": "date"},
"semantic_field": {"type": "semantic_text"},
}
}
}
es_client.indices.create(index=INDEX_NAME, body=mapping)ここで、データがどのように構造化されているかをより明確に見ることができます。セマンティック検索を使用して自然言語に基づいて結果を取得し、copy_toプロパティを使用してフィールドの内容をsemantic_textフィールドにコピーします。さらに、titleフィールドには2つのサブフィールドが含まれています。originalサブフィールドは、記事の元の言語に応じて英語またはスペイン語でタイトルを格納し、translated_titleサブフィールドはスペイン語の記事にのみ存在し、元のタイトルの英語訳が含まれています。
データの取り込み
以下のコードスニペットはbulk APIを使用してブログ投稿データセットをElasticsearchに取り込みます。
def build_data(json_file, index_name):
with open(json_file, "r") as f:
data = json.load(f)
for doc in data:
action = {"_index": index_name, "_source": doc}
yield action
try:
success, failed = helpers.bulk(
es_client,
build_data("dataset.json", INDEX_NAME),
)
print(f"{success} documents indexed successfully")
if failed:
print(f"Errors: {failed}")
except Exception as e:
print(f"Error: {str(e)}")Elasticsearchに記事を取り込んだので、次にsemantic_textフィールドに対して検索できる関数を作成する必要があります:
def perform_semantic_search(query_text, index_name=INDEX_NAME, size=5):
try:
query = {
"query": {
"match": {
"semantic_field": {
"query": query_text,
}
}
},
"size": size,
}
response = es_client.search(index=index_name, body=query)
hits = response["hits"]["hits"]
return hits
except Exception as e:
print(f"Semantic search error: {str(e)}")
return []推論エンドポイントを呼び出す関数も必要です。この場合、chat_completion タスクタイプを使用してエンドポイントを呼び出し、ストリーミング応答を取得します。
def stream_chat_completion(messages: list, inference_id: str = INFERENCE_ENDPOINT_ID):
url = f"{ELASTICSEARCH_URL}/_inference/chat_completion/{inference_id}/_stream"
payload = {"messages": messages}
headers = {
"Authorization": f"ApiKey {ELASTICSEARCH_API_KEY}",
"Content-Type": "application/json",
}
try:
response = requests.post(url, json=payload, headers=headers, stream=True)
response.raise_for_status()
for line in response.iter_lines(decode_unicode=True):
if line:
line = line.strip()
if line.startswith("event:"):
continue
if line.startswith("data: "):
data_content = line[6:]
if not data_content.strip() or data_content.strip() == "[DONE]":
continue
try:
chunk_data = json.loads(data_content)
if "choices" in chunk_data and len(chunk_data["choices"]) > 0:
choice = chunk_data["choices"][0]
if "delta" in choice and "content" in choice["delta"]:
content = choice["delta"]["content"]
if content:
yield content
except json.JSONDecodeError as json_err:
print(f"\nJSON decode error: {json_err}")
print(f"Problematic data: {data_content}")
continue
except requests.exceptions.RequestException as e:
yield f"Error: {str(e)}"ここで、 chat_completions 推論エンドポイントと推薦エンドポイントを合わせてセマンティック検索関数を呼び出し、カードに割り当てられるデータを生成する関数を書くことができます。
def recommend_articles(search_query, index_name=INDEX_NAME, max_articles=5):
print(f"\n{'='*80}")
print(f"🔍 Search Query: {search_query}")
print(f"{'='*80}\n")
articles = perform_semantic_search(search_query, index_name, size=max_articles)
if not articles:
print("❌ No relevant articles found.")
return None, None
print(f"✅ Found {len(articles)} relevant articles\n")
# Build context with found articles
context = "Available blog articles:\n\n"
for i, article in enumerate(articles, 1):
source = article.get("_source", article)
context += f"Article {i}:\n"
context += f"- Title: {source.get('title', 'N/A')}\n"
context += f"- Author: {source.get('author', 'N/A')}\n"
context += f"- Category: {source.get('category', 'N/A')}\n"
context += f"- Date: {source.get('date', 'N/A')}\n"
context += f"- Content: {source.get('content', 'N/A')}\n\n"
system_prompt = """You are an expert content curator that recommends blog articles.
Write recommendations in a conversational style starting with phrases like:
- "If you're interested in [topic], this article..."
- "This post complements your search with..."
- "For those looking into [topic], this article provides..."
FORMAT REQUIREMENTS:
- Return ONLY a JSON array
- Each element must have EXACTLY these three fields: "article_number", "title", "recommendation"
- If the original title is in spanish, use the "translated_title" subfield in the "title" field
Keep each recommendation concise (2-3 sentences max) and focused on VALUE to the reader.
EXAMPLE OF CORRECT FORMAT:
[
{"article_number": 1, "title": "Article title in english", "recommendation": "If you are interested in [topic], this article provides..."},
{"article_number": 2, "title": "Article title in english", "recommendation": " for those looking into [topic], this article provides..."}
]
Return ONLY the JSON array following this exact structure."""
user_prompt = f"""Search query: "{search_query}"
Generate recommendations for the following articles: {context}
"""
messages = [
{"role": "system", "content": "/no_think"},
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
]
# LLM generation
print(f"{'='*80}")
print("🤖 Generating personalized recommendations...\n")
full_response = ""
for chunk in stream_chat_completion(messages):
print(chunk, end="", flush=True)
full_response += chunk
return context, articles, full_response最後に、情報を抽出して出力できるようにフォーマットする必要があります。
def display_recommendation_cards(articles, recommendations_text):
print("\n" + "=" * 100)
print("📇 RECOMMENDED ARTICLES".center(100))
print("=" * 100 + "\n")
# Parse JSON recommendations - clean tags and extract JSON
recommendations_list = []
try:
# Clean up <think> tags
cleaned_text = re.sub(
r"<think>.*?</think>", "", recommendations_text, flags=re.DOTALL
)
# Remove markdown code blocks ( ... ``` or ``` ... ```)
cleaned_text = re.sub(r"```(?:json)?", "", cleaned_text)
cleaned_text = cleaned_text.strip()
parsed = json.loads(cleaned_text)
# Extract recommendations from list format
for item in parsed:
article_number = item.get("article_number")
title = item.get("title", "")
rec_text = item.get("recommendation", "")
if article_number and rec_text:
recommendations_list.append(
{
"article_number": article_number,
"title": title,
"recommendation": rec_text,
}
)
except json.JSONDecodeError as e:
print(f"⚠️ Could not parse recommendations as JSON: {e}")
return
for i, article in enumerate(articles, 1):
source = article.get("_source", article)
# Card border
print("┌" + "─" * 98 + "┐")
# Find recommendation and title for this article number
recommendation = None
title = None
for rec in recommendations_list:
if rec.get("article_number") == i:
recommendation = rec.get("recommendation")
title = rec.get("title")
break
# Print title
title_lines = textwrap.wrap(f"📌 {title}", width=94)
for line in title_lines:
print(f"│ {line}".ljust(99) + "│")
# Card border
print("├" + "─" * 98 + "┤")
# Print recommendation
if recommendation:
recommendation_lines = textwrap.wrap(recommendation, width=94)
for line in recommendation_lines:
print(f"│ {line}".ljust(99) + "│")
# Card bottom
print("└" + "─" * 98 + "┘")セキュリティブログの投稿について質問して、これをテストしてみましょう。
search_query = "Security and vulnerabilities"
context, articles, recommendations = recommend_articles(search_query)
print("\nElasticsearch context:\n", context)
# Display visual cards
display_recommendation_cards(articles, recommendations)ここでは、ワークフローによって生成されたコンソール内のカードを確認できます。

すべてのヒットとLLMの対応を含む完全な結果をこのファイルでご覧いただけます。
「Security and vulnerabilities」に関連する記事をクエリしています。この質問は、Elasticsearchに保存されているドキュメントに対する検索クエリとして使用されます。取得された結果はモデルに渡され、モデルはその内容に基づいてレコメンデーションを生成します。ご覧の通り、このモデルは読者がクリックする動機付けとなる魅力的な短いテキストを非常にうまく生成しています。
まとめ
この例では、ElasticsearchとHugging Faceを組み合わせて、AIアプリケーション向けの高速で効率的な集中型システムを構築する方法を示します。Hugging Faceの豊富なモデルカタログにより、このアプローチでは手作業を削減し、柔軟性を確保できます。特にSmolLM3-3Bを使用すると、コンパクトな多言語モデルでも、セマンティック検索と組み合わせることで有意義な推論とコンテンツ生成を実現できることがわかります。これらのツールを組み合わせることで、インテリジェントなコンテンツ分析と多言語アプリケーションを構築するための、拡張性が高く効果的な基盤を提供できます。




