博客

使用 Elasticsearch 推理 API 以及 Hugging Face 模型

了解如何使用推理终端将 Elasticsearch 连接到 Hugging Face 模型,并利用语义搜索和聊天补全功能构建多语言博客推荐系统。

在最近的更新中,Elasticsearch 引入了原生集成,用于连接到托管在 Hugging Face Inference Service 上的模型。在本文中,我们将探讨如何配置此集成,并使用大型语言模型 (LLM) 通过简单的 API 调用执行推理。我们将使用 SmolLM3-3B,这是一款轻量级通用模型,在资源使用和答案质量之间取得了良好的平衡。

散点图显示了几个小型语言模型,其 X 轴表示模型大小(以十亿为单位的参数),Y 轴表示胜率(百分比)。SmolLM3-3B 在效率趋势中名列前茅,其胜率高于其他类似大小的模型。

准备工作

使用 Hugging Face 推理终端完成聊天

首先,我们将构建一个实用示例,将 Elasticsearch 连接到 Hugging Face 推理终端,以从博客文章集合中生成 AI 驱动的推荐。对于应用知识库,我们将使用公司博客文章数据集,其中包含有价值但通常难以查找的信息。

通过这个终端,语义搜索可以检索与给定查询最相关的文章,而 Hugging Face LLM 则会根据这些结果生成简短的上下文推荐。

让我们来看看我们将要构建的信息流的高级概述:

流程图展示了 Elasticsearch 索引将语义搜索结果输入到推理终端,该终端会返回文章推荐。

在本文中,我们将测试 SmolLM3-3B 是否能将其紧凑的大小与强大的多语言推理和工具调用能力相结合。根据搜索查询,我们将把所有匹配的内容(英语和西班牙语)发送到 LLM,以生成一份推荐文章列表,并根据搜索查询和结果提供自定义描述。

以下是具备 AI 推荐生成系统的文章网站用户界面可能的外观。

具备 AI 推荐生成系统的文章网站的用户界面,列出了三个示例,文本为英语,标题为英语或西班牙语。

您可以在已链接的笔记本中找到此应用程序的完整实现。

配置 Elasticsearch 推理终端

要使用 Elasticsearch Hugging Face 推理终端,我们需要两个重要元素: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" 
    }
}

Elasticsearch 中的 Hugging Face 推理终端支持不同的任务类型:text_embeddingcompletionchat_completionrerank。在这篇博客文章中,我们使用 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 一键式部署,这是一种用于部署模型终端的简单快速的服务。请记住,这是一项付费服务,使用它可能会产生额外费用。此步骤将创建用于生成文章推荐的模型实例。

您可以从一键目录中选择一个模型:

接口视图显示了一个已筛选为“smoll3”的模型目录,其中显示了一个名为“smollm3‑3b”的模型,具有文本生成、vLLM、GPU 1× NVIDIA L4,标价 0.8 美元,并附有一条建议将搜索范围扩展至所有 Hugging Face 模型的提示。

让我们选择 SmolLM3-3B 模型:

用于创建 SmolLM3‑3B 模型终端的接口,显示模型名称、“已由 Hugging Face 验证”注释、终端名称字段、每个运行副本每小时 0.80 美元的成本、cURL 选项和“创建终端”按钮。

从此处获取 Hugging Face 终端 URL:

名为“smollm3‑3b‑pnz”的 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 令牌。您可以选择三种令牌类型:细粒度(推荐用于生产,因为它仅提供对特定资源的访问)、读取(适用于只读访问)或写入(适用于读取和写入访问)。在本教程中,读取令牌就足够了,因为我们只需要调用推理终端。请保存此密钥以备下一步使用。

设置 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 字段包含两个子字段:original 子字段根据文章的原始语言存储英语或西班牙语标题;而 translated_title 子字段仅存在于西班牙语文章中,并包含原始标题的英语翻译。

采集数据

以下代码片段使用批量 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)

如下所示,我们可以看到工作流在控制台中生成的卡片:

标题为“推荐文章”的部分显示了五篇方框式文章摘要,包括身份验证系统漏洞、迁移风险、REST API v2 性能和身份验证改进、通知系统更改以及新 API 的完整指南等主题。

您可以在此文件中查看全部结果,包括所有点击和 LLM 响应。

我们正在征集与“安全与漏洞”相关的文章。此问题将用作针对 Elasticsearch 中存储的文档的搜索查询。然后将检索到的结果传递给模型,该模型根据这些结果的内容生成推荐。我们可以看到,该模型出色地生成了引人入胜的短文本,能够激发读者点击的欲望。

结论

本示例展示了如何将 Elasticsearch 和 Hugging Face 结合起来,为 AI 应用程序创建一个快速高效的集中式系统。由于 Hugging Face 拥有丰富的模型目录,这种方法不仅减少了人工操作,还具有灵活性。通过使用 SmolLM3-3B,我们特别看到了紧凑的多语言模型在与语义搜索搭配使用时,仍能提供有意义的推理和内容生成。这些工具共同为构建智能内容分析和多语言应用程序提供了可扩展且高效的基础。

相关内容

Agent Builder,超越聊天框:介绍增强型基础架构

Alexander Wert

使用 Elasticsearch 管理智能体记忆

Someshwaran Mohankumar

利用弹性代理生成器和 GPT-OSS 构建人力资源人工智能代理

Tomás Murúa

使用 TypeScript 构建 Elasticsearch MCP 服务器

Jeffrey Rengifo

shell 工具并非上下文工程的灵丹妙药

Leonie Monigatti

准备好打造最先进的搜索体验了吗?

足够先进的搜索不是一个人的努力就能实现的。Elasticsearch 由数据科学家、ML 操作员、工程师以及更多和您一样对搜索充满热情的人提供支持。让我们联系起来,共同打造神奇的搜索体验,让您获得想要的结果。

亲自试用