<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[Jeffrey Rengifo - Elasticsearch Labs]]></title>
    <description><![CDATA[Articles and tutorials from the Search team at Elastic]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[Jeffrey Rengifo - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/de/search-labs/author/jeffrey-rengifo</link>
    </image>
    <link>https://www.elastic.co/de/search-labs/author/jeffrey-rengifo</link>
    <atom:link href="https://www.elastic.co/de/search-labs/rss/author/jeffrey-rengifo.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[de]]></language>
    <lastBuildDate>Fri, 11 Sep 2026 20:30:18 GMT</lastBuildDate>
  <item>
    <title><![CDATA[So messen und verbessern Sie den Elasticsearch-Suchabruf: von 0,43 auf 0,75 mit Hybridsuche]]></title>
    <description><![CDATA[Erfahren Sie, wie Sie den Suchabruf in Elasticsearch messen und verbessern können, indem Sie die lexikalische BM25-Suche mit Jina AI-Vektoreinbettungen kombinieren und dabei die rank_eval-API nutzen, um die Verbesserung mit realen Zahlen zu validieren.]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/docs/solutions/search/full-text">Die lexikalische Suche</a> mit dem <a href="https://www.elastic.co/blog/practical-bm25-part-1-how-shards-affect-relevance-scoring-in-elasticsearch">BM25-Ranking-Algorithmus</a> ist günstig, schnell und sehr effektiv für eine breite Palette von Abfragen. Sie hat jedoch einen blinden Fleck: Abfragen, die keine Tokens mit Ihren Dokumenten teilen. In diesem Artikel messen Sie genau, wo BM25 auf der Strecke bleibt. Wir werden die <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval">Ranking-Evaluations-API</a> von Elasticsearch (<code>rank_eval</code>) verwenden und diese Lücke schließen, indem wir <a href="https://www.elastic.co/search-labs/es/blog/jina-embeddings-v3-elastic-inference-service">Jina AI Einbettungen</a> über den <a href="https://www.elastic.co/docs/explore-analyze/elastic-inference/eis">Elastic Inference Service</a> (EIS) hinzufügen. Sie erfahren, wie der Abruf-Score von <code>0.43</code> auf <code>0.75</code> steigt und verstehen, warum er dies tut.</p><h2>Was ist ein Abruf?</h2><p><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval#k-recall">Der Abruf</a> misst auf einer Skala von <code>0</code> bis <code>1</code>, wie viele der Dokumente, die Ihre Nutzer tatsächlich haben möchten, irgendwo in Ihren Suchergebnissen erscheinen. Wenn eine Abfrage drei Produkte anzeigen sollte und Ihre Suche nur zwei davon in den Top 10 zurückgibt, gilt <code>recall@10 = 0.67</code> für diese Abfrage. Es ist eine mengenbasierte Metrik: Die Position der relevanten Dokumente innerhalb dieser <em>k</em> Ergebnisse spielt keine Rolle. Ein relevantes Dokument auf Position 10 zählt genauso wie eines auf Position 1. Ein hoher Abruf-Wert bedeutet, dass Sie keine relevanten Ergebnisse verlieren.</p><p>
</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5ffd147b13705680/6a170a6fe8fbce11a539fc22/b13af2a5d0ca055535d8bfe3dfe4b3d1093ee6da-1457x796.png" alt="Das Venn-Diagramm veranschaulicht die Berechnung des Recall@10-Werts, indem es die Überschneidung zwischen allen relevanten Dokumenten und den Top 10 Ergebnissen von BM25 darstellt. Daraus ergibt sich ein Recall@10-Wert von 0,40." /><p>Das Diagramm zeigt zwei Mengen: alle relevanten Dokumente (links) und die Top 10, die BM25 tatsächlich abgerufen hat (rechts). Nur die Schnittmenge zählt für den Rückruf: <code>prod_1</code> und <code>prod_2</code> wurden gefunden, während <code>prod_3</code>, <code>prod_4</code> und <code>prod_6</code> vollständig übersehen wurden. Ergebnis: <code>Recall@10 = 2/5 = </code><strong><code>0.40</code></strong>.</p><h2>Voraussetzungen</h2><p>Lassen Sie uns zur Sache kommen, um besser zu verstehen, wie das Abrufverfahren funktioniert. Diese Demonstration verwendet Python. Sie können es im begleitenden Notebook verfolgen (<a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/relevance-tuning-improving-recall-adding-vectors/notebook.ipynb">notebook.ipynb</a>), wo jeder Codeblock eine ausführbereite Zelle darstellt.</p><p>Der bereitgestellte Code verwendet Folgendes:</p><ul><li><p>Elasticsearch 9.3+</p></li><li><p>Python 3.10+</p></li></ul>pip install elasticsearch pandas plotly python-dotenv<ul><li><p>Eine <code>.env</code>-Datei mit Ihren Elasticsearch-Zugangsdaten</p></li></ul>ELASTICSEARCH_URL=https://your-cluster-url
ELASTICSEARCH_API_KEY=your-api-key<h2>Der Datensatz</h2><p>Wir verwenden einen Produktkatalog mit 1.000 Produkten, der Kategorien wie Schuhe, Elektronik, Werkzeuge und mehr umfasst.</p><p>Jedes Dokument hat vier Felder:</p><p>Feld</p><p>Typ</p><p>`Titel`</p><p>Text</p><p>`Beschreibung`</p><p>Text</p><p>`Marke`</p><p>Keyword</p><p>`Kategorie`</p><p>Keyword</p><p>Der Datensatz wird aus <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/relevance-tuning-improving-recall-adding-vectors/dataset.csv"><code>dataset.csv</code></a> geladen.</p><h2>Die Stärken und Grenzen der lexikalischen Suche</h2><p>BM25 ist der Standard-Ranking-Algorithmus in Elasticsearch und den meisten Suchmaschinen. Er bewertet Dokumente danach, wie oft Ihre Abfragebegriffe darin erscheinen, angepasst an die Dokumentlänge und die Häufigkeit dieser Begriffe im gesamten Index. Sie erhalten zusätzlich <a href="https://www.elastic.co/docs/reference/text-analysis/analyzer-reference">Analyzer</a>: Kleinschreibungsnormalisierung, Stemming und Stoppwort-Entfernung. Eine Suchanfrage nach „Laufschuhen“ liefert Treffer für „Laufschuhe“ und wahrscheinlich auch für „Laufen“.</p><p>Dies funktioniert gut für eine große Gruppe von Abfragen:</p><ul><li><p>„Laufschuhe“ findet sofort Produkte, die genau diesen Begriff im Titel enthalten.</p></li><li><p>„Bluetooth-Lautsprecher“ taucht bei tragbaren Audioprodukten auf, weil die Token wortwörtlich erscheinen.</p></li></ul><p>Die Ergebnisse sind deterministisch und erklärbar: Ein Dokument rangiert hoch, weil die Abfragebegriffe darin erscheinen. Die Relevanzprüfung ist unkompliziert.</p><h3>Wo es fehlschlägt</h3><p>Nun probieren wir diese Abfragen mit demselben Katalog aus:</p><ul><li><p><strong>„Hautpflege-Routine“:</strong> Das Wort „Routine“ kommt in keinem Produkttitel vor. BM25 kann teilweise mit „Hautpflege“ abgestimmt werden, aber Gesichtsseren, Körperöle und Feuchtigkeitscremes werden mit Begriffen wie „Vitamin C“, „Retinol“ oder „Strahlkraft“ beschrieben, von denen sich keine mit der Abfrage überschneidet. Produkte, die eine vollständige Hautpflegeroutine darstellen, sind über den Index verstreut, ohne ein gemeinsames verankerndes Token.</p></li></ul>ID: B06XX6DS3P, Score: 9.0552, Title: Replenix Retinol Smooth + Tighten Body Lotion - Collagen-Boosting, Regenerating Anti-Aging Body Cream, Reduces Appearance of Stretch Marks, 6.7 oz.

  ID: B08XMPKJ1L, Score: 5.2699, Title: Bio-Oil Skincare Body Oil (Natural) Serum for Scars and Stretchmarks, Face and Body Moisturizer Hydrates Skin, with Organic Jojoba Oil and Vitamin E, For All Skin Types, 6.7 oz

  ID: B01CY764KQ, Score: 5.0057, Title: Nike Up Or Down Men Deodorant - Pack of 2 | Long-Lasting Fragrance, Body Spray Combo for Men | Deodorant for Active Living | Nike Men's Deo Set | Ultimate Odor Protection | Grooming Essentials | Signature Nike Scent | High-Performance Men's Deodorant<ul><li><p><strong>„Reisezubehör für Haustiere“:</strong> Dies ist eine Anwendungsfall-Gruppierung, keine Produktkategorie. Eine Hundetragetasche, ein Autositz für Haustiere und eine Transportbox sind alle relevant, aber in ihren Beschreibungen geht es eher um Tragbarkeit, Sicherheit und Komfort als um „Reisezubehör“. BM25 stimmt weitgehend mit „Haustier“ überein, hat aber kein Signal, um reisespezifische Produkte vom Rest des Haustierkatalogs zu unterscheiden.</p></li></ul>ID: B0BVV7BKTW, Score: 7.4371, Title: Large Foldable Travel Duffel Bag with Shoes Compartment

ID: B07TNPHYNV, Score: 6.6455, Title: 40 Pieces Christmas Bronze Jingle Bells Craft Small Bells

ID: B08R8FRW53, Score: 6.6335, Title: CUBY Dog and Cat Sling Carrier
ID: B08QMCQYGM, Score: 6.5259, Title: YTFGGY Whiteboard Pinstripe Tape 6 Rolls 1/8"
ID: B0CP3LQSWM, Score: 6.2994, Title: Portable Dog Water Bottle 32 Oz<p>Das ist ein <strong>Abrufproblem</strong>. Die relevanten Dokumente finden sich in Ihrem Index. BM25 kann sie einfach nicht finden, weil die Wörter des Nutzers und die Wörter des Dokuments nicht genau genug übereinstimmen.</p><p>Das Hinzufügen von Synonymen hilft bei bekannten Fällen. Man kann aber nicht alle Möglichkeiten aufzählen, wie ein Nutzer seine Absicht ausdrücken könnte. An dieser Stelle kommen Vektoren ins Spiel.</p><h2>Warum Sie die Abruf-Rate messen sollten</h2><p>Bevor Sie ein Problem beheben, müssen Sie es quantifizieren.</p><p><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval#k-recall"><strong>Recall@k</strong></a> misst, wie viele der Dokumente, die Ihre Nutzer tatsächlich suchen, tatsächlich in Ihren Suchergebnissen erscheinen. Formell:</p>Recall@k = (relevant documents found in top k) / (total relevant documents)<p><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval#k-precision"><strong>Precision@k</strong></a> misst die besten k-Ergebnisse und wie viele tatsächlich relevant sind:</p>Precision@k = (relevant documents in top k) / k<p>Hohe Präzision bedeutet, dass die Ergebnisse gut sind. Im E-Commerce ist es oft schlimmer, ein relevantes Produkt zu übersehen (niedrige Abruf-Werte), als ein nicht ganz perfektes Ergebnis anzuzeigen (geringere Präzision), denn ein verstecktes Produkt ist ein verlorener Verkauf.</p><p>Mit der <code>rank_eval</code>-API von Elasticsearch können Sie beides systematisch messen. Sie stellen eine Liste von Abfragen mit jeweils einer Reihe von bewerteten Dokumenten zur Verfügung, und Elasticsearch berechnet für Sie die Metriken für alle Abfragen.</p><h2>Die Bewertung einrichten</h2><p>Die <code>rank_eval</code> API benötigt einen <strong>Bewertungsdatensatz</strong>: ein Mapping von Abfragen zu den Dokumenten, die jeweils relevant sind, zusammen mit einer Relevanznote (0 = nicht relevant, 1 = relevant, 2 = sehr relevant).</p><p>Im Notebook ist dies die <a href="https://www.elastic.co/docs/solutions/search/ranking/learning-to-rank-ltr#learning-to-rank-judgement-list">Bewertungsliste</a>:</p>judgments = [
    # Query 1: "running shoes" BM25 handles well (tokens appear in product titles) 
    {"query_id": "q1", "doc_id": "B09NQJFRW6", "grade": 2, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B08JMD4LMM", "grade": 2, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B08VRJ6F2Q", "grade": 2, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B07S8NRRWR", "grade": 2, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B01HD620I8", "grade": 2, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B07DX86321", "grade": 2, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B0968YVLQ8", "grade": 1, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B093QJ39ZS", "grade": 1, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B096FGSC39", "grade": 1, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B01GVQWVV2", "grade": 1, "query": "running shoes"},

    # Query 2: "skincare routine" intent-based, "routine" never appears in product titles
    {"query_id": "q2", "doc_id": "B08XMPKJ1L", "grade": 2, "query": "skincare routine"},
    {"query_id": "q2", "doc_id": "B0BN3WQB92", "grade": 2, "query": "skincare routine"},
    {"query_id": "q2", "doc_id": "B0BT7B7P5T", "grade": 2, "query": "skincare routine"},
    {"query_id": "q2", "doc_id": "B00NPA2WEY", "grade": 2, "query": "skincare routine"},
    {"query_id": "q2", "doc_id": "B06XX6DS3P", "grade": 1, "query": "skincare routine"},
    {"query_id": "q2", "doc_id": "B07PDRD1KT", "grade": 1, "query": "skincare routine"},
    {"query_id": "q2", "doc_id": "B074J7869B", "grade": 1, "query": "skincare routine"},
    {"query_id": "q2", "doc_id": "B08JV31QW4", "grade": 1, "query": "skincare routine"},
    {"query_id": "q2", "doc_id": "B00K3TVJMQ", "grade": 1, "query": "skincare routine"},

    # Query 3: "study desk setup" intent-based, products are desks/stands/organizers
    {"query_id": "q3", "doc_id": "B08CS35J2T", "grade": 2, "query": "study desk setup"},
    {"query_id": "q3", "doc_id": "B09B3LFDXJ", "grade": 2, "query": "study desk setup"},
    {"query_id": "q3", "doc_id": "B07W58LMND", "grade": 1, "query": "study desk setup"},
    {"query_id": "q3", "doc_id": "B0CHYDX91L", "grade": 1, "query": "study desk setup"},

    # Query 4: "pet travel accessories" use-case grouping, products are carriers/crates/seats
    {"query_id": "q4", "doc_id": "B08R8FRW53", "grade": 2, "query": "pet travel accessories"},
    {"query_id": "q4", "doc_id": "B01MYUYX33", "grade": 2, "query": "pet travel accessories"},
    {"query_id": "q4", "doc_id": "B003C5RKE4", "grade": 2, "query": "pet travel accessories"},
    {"query_id": "q4", "doc_id": "B09GF8GBF6", "grade": 1, "query": "pet travel accessories"},
    {"query_id": "q4", "doc_id": "B0CP3LQSWM", "grade": 1, "query": "pet travel accessories"},
]<p>Die Mischung ist beabsichtigt: <code>q1</code> ist eine Abfrage, die BM25 gut verarbeiten kann (exakte Tokens in Produkttiteln), während <code>q2</code>, <code>q3</code> und <code>q4</code> absichtsbasierte Abfragen sind, bei denen die Absicht des Nutzers als Konzept und nicht als spezifische Produktschlüsselwörter ausgedrückt wird.</p><h2>Messung des BM25-Baseline-Abrufs</h2><p>Richten Sie zuerst den Elasticsearch-Client ein und indexieren Sie die Rohtextdaten:</p>import os
import json
import pandas as pd
import plotly.graph_objects as go
from elasticsearch import Elasticsearch, helpers
from dotenv import load_dotenv

load_dotenv()

es = Elasticsearch(
    os.getenv("ELASTICSEARCH_URL"),
    api_key=os.getenv("ELASTICSEARCH_API_KEY")
)

INDEX_NAME = "ecommerce-products"<p>Erstellen Sie nun die <code>rank_eval</code>-Abfrage für BM25. Jede Abfrage in der Liste kombiniert eine Abfrage mit ihren Bewertungen:</p>judgments_df = pd.DataFrame(judgments)

bm25_requests = []
for query_id, query_text in (
    judgments_df[["query_id", "query"]].drop_duplicates().values
):
    relevant_docs = judgments_df[judgments_df["query_id"] == query_id]
    ratings = [
        {"_index": INDEX_NAME, "_id": row["doc_id"], "rating": row["grade"]}
        for _, row in relevant_docs.iterrows()
    ]

    bm25_requests.append({
        "id": query_id,
        "request": {
            "query": {
                "multi_match": {
                    "query": query_text,
                    "fields": ["title", "description"]
                }
            }
        },
        "ratings": ratings,
    })

bm25_eval = {
    "requests": bm25_requests,
    "metric": {"recall": {"k": 10, "relevant_rating_threshold": 1}},
}

bm25_result = es.rank_eval(index=INDEX_NAME, body=bm25_eval)
print("BM25 Recall@10:", bm25_result.body["metric_score"])<p>Ergebnis:</p>BM25 Recall@10: 0.43<p><code>0.43</code> Das bedeutet, dass BM25 bei allen vier Abfragen nur 43 % der Dokumente findet, die es finden sollte. Das Defizit konzentriert sich auf die absichtsbasierten Suchanfragen: Bei der Suche nach „Hautpflegeroutine“ werden Gesichtsseren und Körperöle nicht erfasst, da „Routine“ nie in den Produkttiteln vorkommt, und bei der Suche nach „Reisezubehör für Haustiere“ werden themenfremde Haustierprodukte gefunden, während Transportboxen und -käfige, die eher im Hinblick auf Tragbarkeit und Sicherheit als auf „Reisezubehör“ beschrieben werden, nicht erfasst werden.</p><p>Dies ist unsere Referenzgrundlage. Jetzt haben wir eine Zahl, die es zu übertreffen gilt.</p><h2>Vektorsuche mit Jina-Embeddings hinzufügen</h2><p><a href="https://www.elastic.co/docs/solutions/search/vector"><code>Vector search</code></a> Dokumente und Abfragen werden als hochdimensionale Vektoren kodiert, eine Art von Vektor, der aus Hunderten oder Tausenden numerischer Werte besteht, wobei jeder Wert ein spezifisches Feature der dargestellten Daten kodiert. Dokumente mit ähnlicher Bedeutung befinden sich im Vektorraum nahe beieinander, selbst wenn sie keine gemeinsamen Wörter enthalten. „Fitnessgeräte“ und „Hantelset“ werden sich in unmittelbarer Nähe befinden, da die Konzepte miteinander verwandt sind. Ich habe Elasticsearch als meine Vektordatenbank gewählt, da es die hybride Suche unterstützt und mir sowohl semantisches Verständnis als auch präzise Stichwortsuche direkt bietet.</p><p><a href="https://www.elastic.co/docs/explore-analyze/elastic-inference/eis">EIS</a> bietet eine fertige Unterstützung für das Einbetten von Modellen über seine <a href="https://www.elastic.co/docs/api/doc/elasticsearch/group/endpoint-inference">Inferenz-API</a>.</p><h3>Schritt 1: Verwendung von Jina-Embeddings v5 als Inferenz-Endpoint</h3>INFERENCE_ENDPOINT_ID = ".jina-embeddings-v5-text-small"<p>Wenn Ihr Cluster GPU-Ressourcen hat (verfügbar in Elastic Cloud und Elasticsearch 9.3+), werden die Einbettungen auf GPU generiert, was deutlich schneller ist als die CPU-Inferenz und den Leistungskompromiss beseitigt, der Vektoren historisch im großen Maßstab teuer gemacht hat.</p><p>Warum genau Jina-Einbettungen? <a href="https://www.elastic.co/search-labs/blog/jina-embeddings-v5-text">jina-embeddings-v5-text</a> ist ein mehrsprachiges Modell (über 119 Sprachen) mit einem Kontextfenster von 32.000 Token und Unterstützung für aufgabenspezifische <a href="https://arxiv.org/abs/2106.09685">Low-Rank Adaptation (LoRA) Adapter</a>. Es eignet sich hervorragend für kurze Produktbeschreibungen. Erfahren Sie mehr über das <code>jina-embeddings-v5-text</code>-Modell <a href="https://huggingface.co/jinaai/jina-embeddings-v5-text-small">hier</a>.</p><h3>Schritt 2: Erstellen Sie den Index mit einem semantischen Feld</h3>index_mappings = {
    "mappings": {
        "properties": {
            "title": {"type": "text", "copy_to": "semantic_field"},
            "description": {"type": "text", "copy_to": "semantic_field"},
            "brand": {"type": "keyword"},
            "category": {"type": "keyword"},
            "semantic_field": {
                "type": "semantic_text",
                "inference_id": INFERENCE_ENDPOINT_ID,
            },
        }
    }
}

if not es.indices.exists(index=INDEX_NAME):
    es.indices.create(index=INDEX_NAME, body=index_mappings)
    print(f"Created index: {INDEX_NAME}")<p>Der <a href="https://www.elastic.co/docs/solutions/search/semantic-search/semantic-search-semantic-text"><code>semantic_text</code></a>-Feldtyp ist hier der Schlüssel. Es handelt sich um eine Abstraktion auf höherer Ebene über <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector"><code>dense_vector</code></a>: Sie verweisen auf einen Inferenz-Endpoint, und Elasticsearch kümmert sich automatisch um die Generierung von Einbettungen.</p><p>Die <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/copy-to"><code>copy_to</code></a>-Eigenschaft auf <code>title</code> und <code>description</code> bedeutet, dass Inhalte aus beiden Feldern in <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-text"><code>semantic_field</code></a> zur Einbettung fließen, sodass ein einzelner Vektor die vollständige Produktdarstellung erfasst.</p><h3>Schritt 3: Produkte indexieren</h3>def bulk_index(products, index_name):
    actions = []
    for product in products:
        doc_id = product.get("_id")
        source = {k: v for k, v in product.items() if k != "_id"}
        action = {"_index": index_name, "_source": source}
        if doc_id:
            action["_id"] = doc_id
        actions.append(action)

    success, failed = helpers.bulk(es, actions, raise_on_error=False)
    if failed:
        for error in failed:
            print(f"Error: {error}")
    else:
        print(f"Successfully indexed {success} documents")

bulk_index(products, INDEX_NAME)<p>Zur Indexzeit ruft Elasticsearch für jedes Dokument den Inferenz-Endpoint auf und speichert die resultierende Einbettung in <code>semantic_field</code>. Kein zusätzlicher Code für Sie erforderlich.</p><h2>Hybridsuche: Kombination von BM25 und Vektoren mit RRF</h2><p>Das Hinzufügen von Vektoren verbessert den Abruf, aber die Verwendung von Vektoren allein birgt das Risiko, dass die Präzision bei exakten Übereinstimmungen auf der Strecke bleibt. „Laufschuhe“ sollten immer noch wortwörtliche Übereinstimmungen an erster Stelle einordnen. Die Hybridsuche behält die lexikalische Komponente bei, um diese Präzision zu erhalten.</p><p>Hybride Suche mit <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion">Reciprocal Rank Fusion</a> (RRF) kombiniert das Beste von beidem:</p><ul><li><p>BM25 verarbeitet exakte und nahezu exakte Abfragen mit hoher Präzision.</p></li><li><p>Die semantische Suche bewältigt absichtsbasierte und mehrsprachige Abfragen mit hoher Trefferquote.</p></li><li><p>RRF kombiniert die beiden Ranglisten zu einer einzigen Rangliste.</p></li></ul><p>Die RRF-Formel weist jedem Dokument eine Bewertung basierend auf seinem Rang in jeder Ergebnisliste zu:</p>score = sum(1 / (rank_constant + rank))<p>Ein Dokument, das in beiden Listen einen hohen Rang einnimmt, erhält eine höhere kombinierte Punktzahl. <code>rank_constant</code> steuert, wie viel Gewicht Dokumente mit niedrigerem Rang erhalten.</p>hybrid_requests = []

for query_id, query_text in (
    judgments_df[["query_id", "query"]].drop_duplicates().values
):
    relevant_docs = judgments_df[judgments_df["query_id"] == query_id]
    ratings = [
        {"_index": INDEX_NAME, "_id": row["doc_id"], "rating": row["grade"]}
        for _, row in relevant_docs.iterrows()
    ]

    hybrid_requests.append({
        "id": query_id,
        "request": {
            "retriever": {
                "rrf": {
                    "retrievers": [
                        {
                            "standard": {
                                "query": {
                                    "multi_match": {
                                        "query": query_text,
                                        "fields": ["title", "description"],
                                    }
                                }
                            }
                        },
                        {
                            "standard": {
                                "query": {
                                    "match": {
                                        "semantic_field": {"query": query_text}
                                    }
                                }
                            }
                        },
                    ],
                    "rank_window_size": 50,
                    "rank_constant": 5,
                }
            }
        },
        "ratings": ratings,
    })

hybrid_eval = {
    "requests": hybrid_requests,
    "metric": {"recall": {"k": 10, "relevant_rating_threshold": 1}},
}

hybrid_result = es.rank_eval(index=INDEX_NAME, body=hybrid_eval)
print("Hybrid Recall@10:", hybrid_result.body["metric_score"])<p>Ergebnis:</p>Hybrid Recall@10: 0.75<p>Hybrid verbessert sich erheblich gegenüber BM25 (<code>0.43</code>) und erhält die Präzision für exakte Treffer bei Abfragen wie „Laufschuhe“ bei.</p><h2>Ergebnisse: Vorher und Nachher</h2><p>Hier der vollständige Vergleich aller drei Ansätze:</p>methods = {
    "BM25 (Lexical)": bm25_requests,
    "Hybrid (BM25 + Vectors)": hybrid_requests,
}

recall_metric = {"recall": {"k": 10, "relevant_rating_threshold": 1}}

comparison_data = []
for method_name, requests in methods.items():
    result = es.rank_eval(
        index=INDEX_NAME,
        body={"requests": requests, "metric": recall_metric}
    )
    comparison_data.append({
        "method": method_name,
        "recall@10": result.body["metric_score"]
    })

comparison_df = pd.DataFrame(comparison_data)
print(comparison_df.to_string(index=False))<p>Ergebnis:</p><p>Methode</p><p>Recall@10</p><p>BM25 (Lexikalisch)</p><p>0,43</p><p>Hybrid (BM25 + Vektoren)</p><p>0,75</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5a1d72b57056fe64/6a170a71c1e8a56c58f882ab/e49f6c10516b0a48a0ad75962c6590ee07311407-700x500.png" alt="Ein Balkendiagramm vergleicht die Recall@10-Werte zwischen der lexikalischen BM25-Suche und der Hybridsuche, die BM25 mit Vektoren kombiniert. Das Diagramm zeigt, dass die Hybridsuche einen deutlich höheren Abruf erzielt." /><p>Aufschlüsselung nach Abfragen:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt871347f754c866d0/6a170a73839dfa40abdcfeb4/40e36dcb7b34cbf4649c512bcb60cef60f1778a6-700x500.png" alt="Ein gruppiertes Balkendiagramm vergleicht die Recall@10-Werte zwischen der lexikalischen und hybriden Suche in BM25 über vier Produktabfragen hinweg und zeigt, dass die hybride Suche bei jeder Abfrage durchweg besser abschneidet als die lexikalische Abfrage." /><h2>Fazit</h2><p>Im Laufe dieses Beitrags haben wir erfahren, dass die lexikalische BM25-Suche zuverlässig ist, wenn Nutzer exakte Suchanfragen eingeben, aber an Treffsicherheit verliert, wenn sie nach Suchintention statt nach Schlüsselwörtern suchen. Mit <code>rank_eval</code>haben wir eine reproduzierbare Basislinie festgelegt, um diese Lücke mit reellen Zahlen zu messen. Von dort aus haben wir ein <code>semantic_text</code> Feld hinzugefügt, das von Jina-Einbettungen unterstützt wird, und die Bewertung erneut durchgeführt. Das Ergebnis: Die Hybridsuche verbesserte die Trefferquote von <code>0.43</code> auf <code>0.75</code>, während die Genauigkeit bei exakten Suchabfragen erhalten blieb. Die tatsächliche Verbesserung hängt jedoch von der Zusammensetzung Ihrer Suchabfragen ab.</p><p>Das Muster lässt sich über dieses Beispiel hinaus skalieren: Sammeln Sie Relevanzurteile aus den tatsächlichen Abfragen Ihrer Nutzer, führen Sie <code>rank_eval</code> als Baseline aus, fügen Sie <code>semantic_text</code> hinzu und messen Sie erneut. Sie wissen genau, was sich verbessert hat und um wie viel.</p><h2>Wie geht es weiter?</h2><ul><li><p>Tauchen Sie tiefer in Abruf und Vektorsuche ein: <a href="https://www.elastic.co/search-labs/blog/recall-vector-search-quantization">Recall and Vector Search Quantization</a> von Jeff Vestal</p></li><li><p>Fügen Sie ein Reranking hinzu, um die besten Ergebnisse noch präziser zu ermitteln</p></li><li><p>Erkunden Sie die <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/rrf.html">Elasticsearch-Dokumentation zur Hybridsuche</a></p></li><li><p>Erfahren Sie mehr über <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-rank-eval.html"><code>rank_eval</code></a><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-rank-eval.html"> API</a></p></li></ul>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-relevance-tuning-improve-recall</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-relevance-tuning-improve-recall</guid>
    <category><![CDATA[Hybride Suche]]></category>
    <category><![CDATA[Vektordatenbank]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt37c9d2971b5a2db3/6a170a75cf4f254223b2d149/492c9b5432a2b9e40cebb3b60f0df019a8c7bf6d-1280x720.png" length="0" type="image/png"/>
    <pubDate>Mon, 04 May 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Erstellung eines Elasticsearch MCP-Servers mit TypeScript]]></title>
    <description><![CDATA[Erfahren Sie, wie Sie mit TypeScript und Claude Desktop einen Elasticsearch MCP-Server erstellen.]]></description>
    <content:encoded><![CDATA[<p>Bei der Arbeit mit großen Wissensdatenbanken in Elasticsearch ist das Finden von Informationen nur die halbe Miete. Entwickler müssen häufig Ergebnisse aus mehreren Dokumenten zusammenführen, Zusammenfassungen erstellen und Antworten bis zu ihren Quellen zurückverfolgen. Um dies zu erreichen, bietet das Modellkontextprotokoll (MCP) eine standardisierte Möglichkeit, Elasticsearch mit LLM-gestützten Anwendungen zu verbinden. Während Elastic offizielle Lösungen anbietet, wie den Elastic Agent Builder (der unter anderem einen <a href="https://www.elastic.co/docs/solutions/search/agent-builder/mcp-server">MCP-Endpoint</a> zu seinen Features zählt), ermöglicht die Entwicklung eines benutzerdefinierten MCP-Servers die volle Kontrolle über die Suchlogik, die Ergebnisformatierung und die Art und Weise, wie abgerufene Inhalte an ein LLM zur Synthese, Zusammenfassung und Zitierung weitergegeben werden.</p><p>In diesem Artikel untersuchen wir die Vorteile der Entwicklung eines benutzerdefinierten Elasticsearch MCP-Servers und zeigen, wie man einen in TypeScript erstellt, der Elasticsearch mit LLM-gestützten Anwendungen verbindet.</p><h2>Warum einen benutzerdefinierten Elasticsearch MCP-Server entwickeln?</h2><p>Elastic bietet einige Alternativen für <a href="https://www.elastic.co/docs/solutions/search/mcp">MCP-Server</a>:</p><ul><li><p><a href="https://www.elastic.co/docs/solutions/search/agent-builder/mcp-server">Elastic Agent Builder MCP-Server für Elasticsearch 9.2+</a></p></li><li><p><a href="https://github.com/elastic/mcp-server-elasticsearch?tab=readme-ov-file#elasticsearch-mcp-server">Elasticsearch MCP-Server für ältere Versionen (Python)</a></p></li></ul><p>Wenn Sie mehr Kontrolle darüber benötigen, wie Ihr MCP-Server mit Elasticsearch interagiert, bietet Ihnen die Entwicklung eines eigenen benutzerdefinierten Servers die nötige Flexibilität, um ihn genau an Ihre Bedürfnisse anzupassen. Zum Beispiel ist der MCP-Endpoint von Agent Builder auf Elasticsearch Query Language (ES|QL) Abfragen beschränkt, während ein benutzerdefinierter Server die Verwendung des vollständigen Abfrage-DSL ermöglicht. Sie erhalten außerdem die Kontrolle über die Formatierung der Ergebnisse, bevor sie an das LLM weitergeleitet werden, und können zusätzliche Verarbeitungsschritte integrieren, wie die OpenAI-gestützte Zusammenfassung, die wir in dieses Tutorial implementieren.</p><p>Am Ende dieses Artikels verfügen Sie über einen MCP-Server in TypeScript, der in einem Elasticsearch-Index gespeicherte Informationen durchsucht, zusammenfasst und Zitate bereitstellt. Wir verwenden Elasticsearch für den Abruf, das <code>gpt-4o-mini</code>-Modell von OpenAI zur Zusammenfassung und Erzeugung von Zitaten sowie Claude Desktop als MCP-Client und Benutzeroberfläche, um Nutzeranfragen zu empfangen und darauf zu reagieren. Das Endergebnis ist ein interner Wissensassistent, der Entwicklern dabei hilft, Best Practices in der technischen Dokumentation ihres Unternehmens zu entdecken und zu synthetisieren.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltad9133cb083ad352/6a170c19b0367d411e72bd5b/ec5771a874cf9740d4cac6888622cbe8cd6aede7-1999x1133.png" alt="Erstellen eines Elastic MCP-Servers mit TypeScript und Claude Desktop." /><h2>Voraussetzungen:</h2><ul><li><p>Node.js 20+</p></li><li><p>Elasticsearch</p></li><li><p>OpenAI-API-Schlüssel</p></li><li><p>Claude Desktop</p></li></ul><h3>Was ist MCP?</h3><p><a href="https://www.elastic.co/what-is/mcp">MCP</a> ist ein offener Standard, der von <a href="https://www.anthropic.com/news/model-context-protocol">Anthropic</a> entwickelt wurde und sichere, bidirektionale Verbindungen zwischen LLMs und externen Systemen wie Elasticsearch ermöglicht. Mehr über den aktuellen Stand von MCP können Sie in <a href="https://www.elastic.co/search-labs/blog/mcp-current-state">diesem Artikel</a> lesen.</p><p>Die MCP-Landschaft <a href="https://www.elastic.co/search-labs/blog/mcp-current-state#mcp-project-updates:-transport,-elicitation,-and-structured-tooling">entwickelt sich täglich weiter</a>, wobei Server für eine Vielzahl von Anwendungsfällen zur Verfügung stehen. In diesem Artikel zeigen wir Ihnen außerdem, wie einfach es ist, Ihren eigenen benutzerdefinierten MCP-Server zu entwickeln.</p><h3>MCP-Clients</h3><p>Es gibt eine lange <a href="https://modelcontextprotocol.io/clients">Liste verfügbarer MCP-Clients</a>, von denen jeder seine eigenen Eigenschaften und Einschränkungen hat. Aufgrund seiner Einfachheit und Beliebtheit verwenden wir <a href="https://claude.ai/download">Claude Desktop</a> als unseren MCP-Client. Er dient als Chat-Schnittstelle, auf der Nutzer Fragen in natürlicher Sprache stellen können, und ruft automatisch die von unserem MCP-Server bereitgestellten Tools auf, um Dokumente zu durchsuchen und Zusammenfassungen zu erstellen.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt06fd7a02042094e1/6a170c1b14b2700024e3c651/66eb0b11473347b6cf2d85718251eeac38d6249d-1999x1491.png" alt="Claude 4.5 Sonnet Seite mit dem Vermerk: „Zeit für Kaffee und Claude? Wie kann ich Ihnen weiterhelfen?“" /><h2>Erstellen eines Elasticsearch MCP-Servers</h2><p>Mit dem <a href="https://github.com/modelcontextprotocol/typescript-sdk">TypeScript SDK</a> können wir einfach einen Server erstellen, der versteht, wie er unsere Elasticsearch-Daten basierend auf einer Nutzereingabe abfragt.</p><p>In diesem Artikel werden die folgenden Schritte zur Integration des Elasticsearch MCP-Servers mit dem Claude Desktop-Client beschrieben:</p><ol><li><p><a href="https://www.elastic.co/search-labs/blog/elastic-mcp-server-typescript-claude#configure-mcp-server-for-elasticsearch">MCP-Server für Elasticsearch konfigurieren.</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/elastic-mcp-server-typescript-claude#load-the-mcp-server-into-claude-desktop">MCP-Server in Claude Desktop laden.</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/elastic-mcp-server-typescript-claude#test-it-out">Probieren Sie es aus.</a></p></li></ol><h3>MCP-Server für Elasticsearch konfigurieren</h3><p>Zunächst initialisieren wir eine Node-Anwendung:</p>npm init -y<p>Dadurch wird eine <code>package.json</code>-Datei erstellt, wodurch wir damit beginnen können, die notwendigen Abhängigkeiten für diese Anwendung zu installieren.</p>npm install @elastic/elasticsearch @modelcontextprotocol/sdk openai zod &amp;&amp; npm install --save-dev ts-node @types/node typescript<ul><li><p><strong>@elastic/elasticsearch</strong> verschafft Ihnen Zugriff auf die Node.js-Bibliothek von Elasticsearch.</p></li><li><p><strong>@modelcontextprotocol/SDK</strong> stellt die Kerntools bereit, um einen MCP-Server zu erstellen und zu verwalten, Tools zu registrieren und die Kommunikation mit MCP-Clients zu übernehmen.</p></li><li><p><strong>OpenAI</strong> ermöglicht die Interaktion mit OpenAI-Modellen, um Zusammenfassungen oder Antworten in natürlicher Sprache zu generieren.</p></li><li><p><a href="https://zod.dev/"><strong>ZOD</strong></a>sorgt dafür, strukturierte Schemata für Eingangs- und Ausgangsdaten in jedem Tool zu definieren und zu validieren.</p></li></ul><p><code>ts-node</code>Während der Entwicklung werden <code>@types/node</code> und <code>typescript</code> verwendet, um den Code zu schreiben und die Skripte zu kompilieren.</p><h4>Datensatz einrichten</h4><p>Um die Daten bereitzustellen, die Claude Desktop über unseren MCP-Server abfragen kann, verwenden wir einen simulierten <a href="https://github.com/Delacrobix/typescript-elasticsearch-mcp/blob/main/dataset.json">internen Wissensdatenbank-Datensatz</a>. So sieht ein Dokument aus diesem Datensatz aus:</p>{
    "id": 5,
    "title": "Logging Standards for Microservices",
    "content": "Consistent logging across microservices helps with debugging and tracing. Use structured JSON logs and include request IDs and timestamps. Avoid logging sensitive information. Centralize logs in Elasticsearch or a similar system. Configure log rotation to prevent storage issues and ensure logs are searchable for at least 30 days.",
    "tags": ["logging", "microservices", "standards"]
}<p>Für die Aufnahme der Daten haben wir ein Skript vorbereitet, das einen Index in Elasticsearch erstellt und den Datensatz darin lädt. Sie finden es <a href="https://github.com/Delacrobix/typescript-elasticsearch-mcp/blob/main/setup.ts">hier</a>.</p><h4>MCP-Server</h4><p>Erstellen Sie eine Datei mit dem Namen <a href="https://github.com/Delacrobix/typescript-elasticsearch-mcp/blob/main/index.ts"><code>index.ts</code></a> und fügen Sie den folgenden Code hinzu, um die Abhängigkeiten zu importieren und Umgebungsvariablen zu verarbeiten:</p>// index.ts
import { z } from "zod";
import { Client } from "@elastic/elasticsearch";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import OpenAI from "openai";

const ELASTICSEARCH_ENDPOINT =
  process.env.ELASTICSEARCH_ENDPOINT ?? "http://localhost:9200";
const ELASTICSEARCH_API_KEY = process.env.ELASTICSEARCH_API_KEY ?? "";
const OPENAI_API_KEY = process.env.OPENAI_API_KEY ?? "";
const INDEX = "documents";<p>Außerdem initialisieren wir die Clients, um Elasticsearch- und OpenAI-Aufrufe zu bewältigen:</p>const openai = new OpenAI({
  apiKey: OPENAI_API_KEY,
});

const _client = new Client({
  node: ELASTICSEARCH_ENDPOINT,
  auth: {
    apiKey: ELASTICSEARCH_API_KEY,
  },
});<p>Um unsere Implementierung robuster zu machen und einen strukturierten Eingang und Ausgang zu gewährleisten, definieren wir Schemata mit <a href="https://zod.dev/"><code>zod</code></a>. Dadurch können wir Daten zur Laufzeit validieren, Fehler frühzeitig abfangen und die Tool-Reaktionen einfacher programmatisch verarbeiten.</p>const DocumentSchema = z.object({
  id: z.number(),
  title: z.string(),
  content: z.string(),
  tags: z.array(z.string()),
});

const SearchResultSchema = z.object({
  id: z.number(),
  title: z.string(),
  content: z.string(),
  tags: z.array(z.string()),
  score: z.number(),
});

type Document = z.infer&lt;typeof DocumentSchema&gt;;
type SearchResult = z.infer&lt;typeof SearchResultSchema&gt;;<p>Erfahren Sie <a href="https://www.elastic.co/search-labs/blog/structured-outputs-elasticsearch-guide">hier</a> mehr über strukturierte Ausgänge.</p><p>Nun initialisieren wir den MCP-Server:</p>const server = new McpServer({
  name: "Elasticsearch RAG MCP",
  description:
    "A RAG server using Elasticsearch. Provides tools for document search, result summarization, and source citation.",
  version: "1.0.0",
});<h4>Definition der MCP-Tools</h4><p>Wenn alles konfiguriert ist, können wir mit dem Schreiben der Tools beginnen, die von unserem MCP-Server bereitgestellt werden. Dieser Server stellt zwei Tools bereit:</p><ul><li><p><strong><code>search_docs</code></strong><strong>: </strong>Sucht nach Dokumenten in Elasticsearch mittels Volltextsuche.</p></li><li><p><strong><code>summarize_and_cite</code></strong><strong>:</strong> Fasst Informationen aus zuvor abgerufenen Dokumenten zusammen und synthetisiert sie, um eine Nutzerfrage zu beantworten. Dieses Tool fügt außerdem Zitate hinzu, die auf die Quelldokumente verweisen.</p></li></ul><p>Zusammen bilden diese Tools einen einfachen Workflow zum Abrufen und Zusammenfassen, bei dem ein Tool relevante Dokumente abruft und das andere diese Dokumente verwendet, um eine zusammengefasste, zitierte Reaktion zu generieren.</p><h4>Reaktionsformat des Tools</h4><p>Jedes Tool kann beliebige Eingangsparameter akzeptieren, muss aber mit folgender Struktur antworten:</p><ul><li><p><strong>Inhalt:</strong> Dies ist die Reaktion des Tools im unstrukturierten Format. Dieses Feld wird in der Regel verwendet, um Text, Bilder, Audio, Links oder Einbettungen zurückzugeben. In dieser Anwendung dient es dazu, formatierten Text mit den Informationen zurückzugeben, die von den Tools generiert wurden.</p></li><li><p><strong>structuredContent: </strong>Dies ist eine optionale Rückgabe, die verwendet wird, um die Ergebnisse der einzelnen Tools in einem strukturierten Format bereitzustellen. Dies ist für programmatische Zwecke nützlich. Zwar wird es in diesem MCP-Server nicht verwendet, es kann jedoch von Nutzen sein, wenn Sie andere Tools entwickeln oder die Ergebnisse programmatisch verarbeiten möchten.</p></li></ul><p>Vor diesem Hintergrund betrachten wir nun jedes Tool im Detail.</p><h4>Tool „Search_docs“</h4><p>Dieses Tool führt eine <a href="https://www.elastic.co/docs/solutions/search/full-text">Volltextsuche</a> im Elasticsearch-Index durch, um die relevantesten Dokumente basierend auf der Nutzeranfrage abzurufen. Es hebt die wichtigsten Treffer hervor und bietet einen schnellen Überblick mit Relevanzbewertungen.</p>server.registerTool(
  "search_docs",
  {
    title: "Search Documents",
    description:
      "Search for documents in Elasticsearch using full-text search. Returns the most relevant documents with their content, title, tags, and relevance score.",
    inputSchema: {
      query: z
        .string()
        .describe("The search query terms to find relevant documents"),
      max_results: z
        .number()
        .optional()
        .default(5)
        .describe("Maximum number of results to return"),
    },
    outputSchema: {
      results: z.array(SearchResultSchema),
      total: z.number(),
    },
  },
  async ({ query, max_results }) =&gt; {
    if (!query) {
      return {
        content: [
          {
            type: "text",
            text: "Query parameter is required",
          },
        ],
        isError: true,
      };
    }

    try {
      const response = await _client.search({
        index: INDEX,
        size: max_results,
        query: {
          bool: {
            must: [
              {
                multi_match: {
                  query: query,
                  fields: ["title^2", "content", "tags"],
                  fuzziness: "AUTO",
                },
              },
            ],
            should: [
              {
                match_phrase: {
                  title: {
                    query: query,
                    boost: 2,
                  },
                },
              },
            ],
          },
        },
        highlight: {
          fields: {
            title: {},
            content: {},
          },
        },
      });

      const results: SearchResult[] = response.hits.hits.map((hit: any) =&gt; {
        const source = hit._source as Document;

        return {
          id: source.id,
          title: source.title,
          content: source.content,
          tags: source.tags,
          score: hit._score ?? 0,
        };
      });

      const contentText = results
        .map(
          (r, i) =&gt;
            `[${i + 1}] ${r.title} (score: ${r.score.toFixed(
              2,
            )})\n${r.content.substring(0, 200)}...`,
        )
        .join("\n\n");

      const totalHits =
        typeof response.hits.total === "number"
          ? response.hits.total
          : (response.hits.total?.value ?? 0);

      return {
        content: [
          {
            type: "text",
            text: `Found ${results.length} relevant documents:\n\n${contentText}`,
          },
        ],
        structuredContent: {
          results: results,
          total: totalHits,
        },
      };
    } catch (error: any) {
      console.log("Error during search:", error);

      return {
        content: [
          {
            type: "text",
            text: `Error searching documents: ${error.message}`,
          },
        ],
        isError: true,
      };
    }
  }
);<p><em>Wir konfigurieren </em><a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-fuzzy-query"><em><code>fuzziness</code></em></a><em><code>: “AUTO”</code></em><em> so, dass eine variable Tippfehlertoleranz basierend auf der Länge des analysierten Tokens besteht. Wir legen außerdem </em><em><code>title^2</code></em><em> fest, um die Bewertung der Dokumente zu erhöhen, bei denen die Übereinstimmung im Feld „Titel“ erfolgt.</em></p><h4>Tool „summarize_and_cite“</h4><p>Dieses Tool erstellt eine Zusammenfassung auf Basis von Dokumenten, die bei der vorherigen Suche ermittelt wurden. Es verwendet das <code>gpt-4o-mini</code>-Modell von OpenAI, um die relevantesten Informationen zur Beantwortung der Nutzerfragen zu synthetisieren und Antworten zu liefern, die direkt aus den Suchergebnissen abgeleitet werden. Neben der Zusammenfassung werden auch Metadaten für Zitate zu den verwendeten Quelldokumenten zurückgegeben.</p>server.registerTool(
  "summarize_and_cite",
  {
    title: "Summarize and Cite",
    description:
      "Summarize the provided search results to answer a question and return citation metadata for the sources used.",
    inputSchema: {
      results: z
        .array(SearchResultSchema)
        .describe("Array of search results from search_docs"),
      question: z.string().describe("The question to answer"),
      max_length: z
        .number()
        .optional()
        .default(500)
        .describe("Maximum length of the summary in characters"),
      max_docs: z
        .number()
        .optional()
        .default(5)
        .describe("Maximum number of documents to include in the context"),
    },
    outputSchema: {
      summary: z.string(),
      sources_used: z.number(),
      citations: z.array(
        z.object({
          id: z.number(),
          title: z.string(),
          tags: z.array(z.string()),
          relevance_score: z.number(),
        })
      ),
    },
  },
  async ({ results, question, max_length, max_docs }) =&gt; {
    if (!results || results.length === 0 || !question) {
      return {
        content: [
          {
            type: "text",
            text: "Both results and question parameters are required, and results must not be empty",
          },
        ],
        isError: true,
      };
    }

    try {
      const used = results.slice(0, max_docs);

      const context = used
        .map(
          (r: SearchResult, i: number) =&gt;
            `[Document ${i + 1}: ${r.title}]\\n${r.content}`
        )
        .join("\n\n---\n\n");

      // Generate summary with OpenAI
      const completion = await openai.chat.completions.create({
        model: "gpt-4o-mini",
        messages: [
          {
            role: "system",
            content:
              "You are a helpful assistant that answers questions based on provided documents. Synthesize information from the documents to answer the user's question accurately and concisely. If the documents don't contain relevant information, say so.",
          },
          {
            role: "user",
            content: `Question: ${question}\\n\\nRelevant Documents:\\n${context}`,
          },
        ],
        max_tokens: Math.min(Math.ceil(max_length / 4), 1000),
        temperature: 0.3,
      });

      const summaryText =
        completion.choices[0]?.message?.content ?? "No summary generated.";

      const citations = used.map((r: SearchResult) =&gt; ({
        id: r.id,
        title: r.title,
        tags: r.tags,
        relevance_score: r.score,
      }));

      const citationText = citations
        .map(
          (c: any, i: number) =&gt;
            `[${i + 1}] ID: ${c.id}, Title: "${c.title}", Tags: ${c.tags.join(
              ", ",
            )}, Score: ${c.relevance_score.toFixed(2)}`,
        )
        .join("\n");

      const combinedText = `Summary:\\n\\n${summaryText}\\n\\nSources used (${citations.length}):\\n\\n${citationText}`;

      return {
        content: [
          {
            type: "text",
            text: combinedText,
          },
        ],
        structuredContent: {
          summary: summaryText,
          sources_used: citations.length,
          citations: citations,
        },
      };
    } catch (error: any) {
      return {
        content: [
          {
            type: "text",
            text: `Error generating summary and citations: ${error.message}`,
          },
        ],
        isError: true,
      };
    }
  }
);<p>Zu guter Letzt starten wir den Server mithilfe von <a href="https://github.com/modelcontextprotocol/typescript-sdk?tab=readme-ov-file#stdio">stdio</a>. Das bedeutet, dass der MCP-Client mit unserem Server kommuniziert, indem er seine standardmäßigen Eingangs- und Ausgangsstreams liest und schreibt. stdio ist die einfachste Transportoption und funktioniert gut für lokale MCP-Server, die vom Client als Unterprozesse gestartet werden. Fügen Sie den folgenden Code am Ende der Datei hinzu:</p>const transport = new StdioServerTransport();
server.connect(transport);<p>Kompilieren Sie das Projekt jetzt mit folgendem Befehl:</p>npx tsc index.ts --target ES2022 --module node16 --moduleResolution node16 --outDir ./dist --strict --esModuleInterop<p>Dadurch wird ein Ordner <code>dist</code> erstellt und darin eine Datei <code>index.js</code>.</p><h3>Laden Sie den MCP-Server in Claude Desktop</h3><p>Folgen Sie <a href="https://modelcontextprotocol.io/docs/develop/connect-local-servers">dieser Anleitung</a>, um den MCP-Server mit Claude Desktop zu konfigurieren. In der Konfigurationsdatei von Claude müssen die folgenden Werte festgelegt werden:</p>{
  "mcpServers": {
    "elasticsearch-rag-mcp": {
      "command": "node",
      "args": [   "/Users/user-name/app-dir/dist/index.js"
      ],
      "env": {
        "ELASTICSEARCH_ENDPOINT": "your-endpoint-here",
        "ELASTICSEARCH_API_KEY": "your-api-key-here",
        "OPENAI_API_KEY": "your-openai-key-here"
      }
    }
  }
}<p>Der Wert <code>args</code> sollte auf die kompilierte Datei im Ordner <code>dist</code> verweisen. Außerdem müssen Sie die Umgebungsvariablen in der Konfigurationsdatei mit genau den gleichen Namen festlegen, die im Code definiert sind.</p><h3>Probieren Sie es aus</h3><p>Klicken Sie vor der Ausführung jedes Tools auf <strong>Search and Tools</strong>, um sicherzustellen, dass die Tools aktiviert sind. Hier können Sie außerdem alle Funktionen aktivieren oder deaktivieren:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt395a7337021f9820/6a170c1c67045bb74d45c228/172981c2a54adabc70d5819013c3007670935605-1999x1002.png" alt="Claude 4.5 Sonnet Seite mit dem Vermerk: „Guten Tag Jeff. Wie kann ich Ihnen weiterhelfen?“" /><p>Zum Schluss testen wir den MCP-Server über den Claude Desktop-Chat und beginnen, Fragen zu stellen:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf4ac458dc0206271/6a170c1e66c4f91328f8c072/03654c0f8c53c714f801fba8b25747071179209b-1999x1353.png" alt="Nutzersuchanfrage im Claude Desktop-Chat nach Dokumenten über Authentifizierungsmethoden und RBAC sowie Reaktionen von Claude." /><p>Für die Frage „<strong>Suche nach Dokumenten zu Authentifizierungsmethoden und RBAC</strong>“ wird das <code>search_docs</code>-Tool ausgeführt und liefert folgende Ergebnisse:</p>Most Relevant Documents:
Access Control and Role Management (highest relevance) - This document covers role-based access control (RBAC) principles, including ensuring users only have necessary permissions, regular auditing of user roles, revoking inactive accounts, and implementing just-in-time access for sensitive operations.
User Authentication with OAuth 2.0 - This document explains OAuth 2.0 authentication, which enables secure delegated access without credential sharing. It covers configuring identity providers, token management with limited scope and lifetime, and secure storage of refresh tokens.
Container Security Guidelines - While primarily about container security, this document touches on access control aspects like running containers as non-root users and avoiding embedded credentials.
Incident Response Playbook - This mentions role assignment during incidents (incident commander, communications lead, etc.), which relates to access control in emergency scenarios.
Logging Standards for Microservices - This document includes guidance on avoiding logging sensitive information, which is relevant to authentication security.<p>Die Reaktion lautet: „Großartig! Ich habe 5 relevante Dokumente über Authentifizierungsmethoden und RBAC gefunden. Folgendes wurde gefunden:“</p><p>Der Toolaufruf gibt die Quelldokumente als Teil seiner Reaktionnutzlast zurück, die später zur Erstellung von Zitaten verwendet werden.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbaf48a9468770ce2/6a170c21509168bffee1bb14/25ff4c7e9563d99752f95540dafdc7fd211a66e3-800x530.gif" alt="Claude 4.5 Sonett Seite mit Reaktionen zum Durchscrollen, die die fünf relevanten Dokumente zu Authentifizierungsmethoden und RBAC enthalten." /><p>Es ist auch möglich, mehrere Tools in einer einzigen Interaktion zu verketten. In diesem Fall analysiert Claude Desktop die Frage des Nutzers und stellt fest, dass zunächst <code>search_docs</code> aufgerufen werden muss, um relevante Dokumente abzurufen, und dass diese Ergebnisse anschließend an <code>summarize_and_cite</code> übergeben werden müssen, um die endgültige Antwort zu generieren, ohne dass dafür separate Prompts vom Nutzer erforderlich sind:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta46ff45274e64192/6a170c230c4857a91501aac1/e6a8a46acb4236e77058f18bcd2f0737b5882c05-1999x1101.png" alt="Claude Desktop-Chat mit dem Hinweis „Jeff ist zurück“ und einer neuen Nutzerfrage: „Was sind die wichtigsten Empfehlungen zur Verbesserung der Authentifizierung und Zugriffskontrolle in unseren Systemen? Referenzen einbeziehen.“" /><p>In diesem Fall haben wir für die Abfrage „<strong>Was sind die wichtigsten Empfehlungen zur Verbesserung der Authentifizierung und Zugriffssteuerung in unseren Systemen? Referenzen einbeziehen.</strong>“ folgende Ergebnisse erzielt:</p>Based on the documentation, here are the main recommendations to improve authentication and access control across your systems:

Key Recommendations

1. Implement Role-Based Access Control (RBAC)
Ensure users have only the permissions necessary for their job functions. This principle of least privilege minimizes security risks by limiting access to what's truly needed. [1]

2. Regular Access Audits
Conduct regular audits of user roles and promptly revoke access for inactive accounts. This helps prevent unauthorized access from dormant or compromised accounts. [1]

3. Just-in-Time (JIT) Access
Implement just-in-time access for sensitive operations, where permissions are granted only when needed and for a limited duration. This reduces the attack surface by minimizing standing privileges. [1]

4. OAuth 2.0 for Secure Authentication
Use OAuth 2.0 to enable secure delegated access without sharing user credentials. This protocol allows third-party applications to access resources on behalf of users without exposing passwords. [2]

5. Token Security and Management
Configure your identity provider to issue tokens with limited scope and lifetime

Store refresh tokens securely
Validate access tokens consistently to maintain security [2]
References

Access Control and Role Management (Tags: security, access-control)
User Authentication with OAuth 2.0 (Tags: authentication, oauth)
These recommendations work together to create a defense-in-depth approach, where multiple security layers protect your systems from unauthorized access.<p>Wie im vorherigen Schritt können wir die Reaktion jedes Tools auf diese Frage einsehen:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8f633c518e708a99/6a170c25ab7f082991db9ed6/cb606d356b2f7d5e4878a5eff71bc881869ac0ee-800x585.gif" alt="Claude Desktop-Chatseite mit Text zum Durchscrollen, der die Reaktion jedes Tools auf die Frage enthält: „Was sind die wichtigsten Empfehlungen zur Verbesserung der Authentifizierung und Zugriffskontrolle in unseren Systemen? Referenzen einbeziehen.“" /><p><em>Hinweis: Wenn ein Untermenü erscheint, in dem gefragt wird, ob Sie die Nutzung jedes Tools genehmigen möchten, wählen Sie </em><em><strong>Immer erlauben</strong></em><em> oder </em><em><strong>Einmal erlauben</strong></em><em>.</em></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6627ee0bff1862df/6a170c266f7f040f6f91488c/aea942ba9b0037526ea215bec65690f1a5c3099c-1522x250.png" alt="Claude Desktop bietet Nutzern die Auswahl der Optionen „Immer zulassen“ und „Einmal zulassen“." /><h2>Fazit</h2><p>MCP-Server stellen einen bedeutenden Schritt zur Standardisierung von LLM-Tools für lokale und entfernte Anwendungen dar. Die vollständige Kompatibilität ist zwar noch in Bearbeitung, wir verzeichnen jedoch große Fortschritte in diese Richtung.</p><p>In diesem Artikel haben wir gelernt, wie man einen benutzerdefinierten MCP-Server in TypeScript entwickelt, der Elasticsearch mit LLM-gestützten Anwendungen verbindet. Unser Server stellt zwei Tools bereit: <code>search_docs</code> zum Abrufen relevanter Dokumente mittels Abfrage-DSL und <code>summarize_and_cite</code> zur Erstellung von Zusammenfassungen mit Zitaten über OpenAI-Modelle und Claude Desktop als Client-Benutzeroberfläche.</p><p>Die Zukunft der Kompatibilität zwischen verschiedenen Client- und Server-Anbietern sieht vielversprechend aus. Zu den nächsten Schritten gehört die Erweiterung des Funktionsumfangs und die Erhöhung der Flexibilität Ihres Agenten. Es gibt einen praktischen <a href="https://www.elastic.co/search-labs/blog/llm-functions-elasticsearch-intelligent-query">Artikel</a> dazu, wie Sie Ihre Abfragen mithilfe von Suchvorlagen für mehr Präzision und Flexibilität parametrisieren können.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elastic-mcp-server-typescript-claude</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elastic-mcp-server-typescript-claude</guid>
    <category><![CDATA[Agentische KI]]></category>
    <category><![CDATA[Integrationen]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5600198cb47666a5/6a170c28509168ce3ae1bb18/0bb24c05fff391f42070c2883182ea6fe9cb9680-1280x720.png" length="0" type="image/png"/>
    <pubDate>Fri, 27 Mar 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Die Verwendung der Elasticsearch Inference API zusammen mit Hugging Face-Modellen]]></title>
    <description><![CDATA[Erfahren Sie, wie Sie Elasticsearch mithilfe von Inferenz-Endpoints mit Hugging Face Modellen verbinden und ein mehrsprachiges Blog-Empfehlungssystem mit semantischer Suche und Chat-Abschlüssen erstellen.]]></description>
    <content:encoded><![CDATA[<p>In den letzten Aktualisierungen hat Elasticsearch eine native Integration eingeführt, um sich mit Modellen zu verbinden, die auf dem <a href="https://endpoints.huggingface.co/">Hugging Face Inference Service</a> gehostet werden. In diesem Beitrag erfahren Sie, wie Sie diese Integration konfigurieren und über einfache API-Aufrufe mithilfe eines großen Sprachmodells (LLM) Inferenzen durchführen können. Wir verwenden <a href="https://huggingface.co/HuggingFaceTB/SmolLM3-3B">SmolLM3-3B</a>, ein leichtes Allzweckmodell mit einem ausgewogenen Gleichgewicht zwischen Ressourcenverbrauch und Antwortqualität.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9094997548bd70f8/6a170d6a839dfa0ad6dcff54/7ddadf1976421a860a7d62087239adb9150d808b-1999x1388.png" alt="Punktdiagramm mit mehreren kleinen Sprachmodellen, aufgetragen nach Modellgröße (in Milliarden von Parametern) auf der x-Achse und Gewinnrate (in Prozent) auf der y-Achse. SmolLM3‑3B erscheint nahe an der Spitze des Effizienztrends, mit einer höheren Gewinnrate als andere Modelle ähnlicher Größe." /><h2>Voraussetzungen</h2><ul><li><p><strong>Elasticsearch 9.3 oder Elastic Cloud Serverless: </strong>Sie können ein Cloud-Deployment erstellen, indem Sie <a href="https://www.elastic.co/search-labs/tutorials/install-elasticsearch/elastic-cloud">diese Anweisungen</a> befolgen, oder stattdessen den <a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed/local-development-installation-quickstart#local-dev-quick-start"><code>start-local</code></a> Quickstart verwenden.</p></li><li><p><strong>Python 3.12: </strong>Laden Sie Python <a href="https://www.python.org/">hier</a> herunter.</p></li><li><p><strong>Hugging Face </strong><a href="https://huggingface.co/docs/hub/en/security-tokens">Zugriffstoken</a>.</p></li></ul><h2>Chat-Abschlüsse unter Verwendung eines Inferenz-Endpoints von Hugging Face</h2><p>Zuerst erstellen wir ein praktisches Beispiel, das Elasticsearch mit einem Hugging Face <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-put">Inferenz-Endpoint</a> verbindet, um KI-gestützte Empfehlungen aus einer Reihe von Blogbeiträgen zu generieren. Für die Wissensdatenbank der App verwenden wir einen Datensatz mit Blogartikeln des Unternehmens, der wertvolle, aber oft schwer zugängliche Informationen enthält.</p><p>Bei diesem Endpoint ruft die <a href="https://www.elastic.co/docs/solutions/search/semantic-search">semantische Suche</a> die relevantesten Artikel für eine gegebene Abfrage ab, und ein Hugging Face LLM generiert kurze, kontextuelle Empfehlungen basierend auf diesen Ergebnissen.</p><p>Verschaffen wir uns einen groben Überblick über den Informationsfluss, den wir entwickeln werden:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf217b7b7db4e1e6c/6a170d6ca929cf8022ae0a3b/1dfbc2323438feaaa42e13ab242dd1f7166f74aa-1200x676.png" alt="Flussdiagramm, das einen Elasticsearch-Index zeigt, der semantische Suchergebnisse an einen Inferenz-Endpoint weiterleitet, der Artikelempfehlungen zurückgibt." /><p>In diesem Artikel testen wir die Fähigkeit von <strong>SmolLM3-3B</strong> undkombinieren seine kompakte Größe mit einer starken mehrsprachigen Argumentations- und Tool-Aufruffunktion. Basierend auf einer Suchabfrage senden wir alle passenden Inhalte (auf Englisch und Spanisch) an das LLM, um eine Liste empfohlener Artikel mit einer individuell erstellten Beschreibung basierend auf der Suchanfrage und den Ergebnissen zu erstellen.</p><p>So könnte die Benutzeroberfläche einer Artikelseite mit einem System zur Generierung von KI-Empfehlungen aussehen.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt20e69b9a06fecd65/6a170d6e839dfa6f97dcff58/8d3b86b212f28ff279f2da67a33e6134039f0e4e-1999x949.png" alt="Benutzeroberfläche einer Artikelseite mit einem KI-gestützten Empfehlungssystem und drei Beispielen mit englischem Text sowie englischen oder spanischen Titeln." /><p>Sie können die vollständige Implementierung dieser Anwendung im verlinkten <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/elasticsearch-inference-api-and-hugging-face/notebook.ipynb">Notizbuch</a> finden.</p><h3>Konfiguration der Elasticsearch Inferenz-Endpoints</h3><p>Um den Elasticsearch <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-put-hugging-face">Hugging Face Inferenz-Endpoint</a> zu verwenden, benötigen wir zwei wichtige Elemente: einen Hugging Face API-Schlüssel und eine ausgeführte Hugging Face Endpoint-URL. Dies sollte so aussehen:</p>PUT _inference/chat_completions/hugging-face-smollm3-3b
{
    "service": "hugging_face",
    "service_settings": {
        "api_key": "hugging-face-access-token", 
        "url": "url-endpoint" 
    }
}<p>Der Hugging Face Inferenz-Endpoint in Elasticsearch unterstützt verschiedene Aufgabentypen: <code>text_embedding</code>, <code>completion</code>, <code>chat_completion</code> und <code>rerank</code>. In diesem Blogbeitrag verwenden wir <code>chat_completion</code>, weil das Modell Gesprächsempfehlungen basierend auf den Suchergebnissen und einem Systemprompt generieren kann. Dieser Endpoint ermöglicht es uns, Chatabschlüsse direkt von Elasticsearch aus auf einfache Weise mit der Elasticsearch-API durchzuführen:</p>POST _inference/chat_completion/hugging-face-smollm3-3b/_stream
{
  "messages": [
      { "role": "user", "content": "&lt;user prompt&gt;" }
  ]
}<p>Dies dient als Kern der Anwendung, der den Prompt und die Suchergebnisse empfängt, die durch das Modell laufen werden. Nachdem die Theorie geklärt ist, können wir mit der Implementierung der Anwendung beginnen.</p><h4>Einrichten des ​​Inferenz-Endpoints auf Hugging Face</h4><p>Um das Hugging Face Modell bereitzustellen, werden wir <a href="https://huggingface.co/inference-endpoints/dedicated">Hugging Face One-Click-Deployments</a> verwenden, einen einfachen und schnellen Service für die Bereitstellung von Modell-Endpoints. Beachten Sie bitte, dass es sich um einen kostenpflichtigen Service handelt, durch dessen Nutzung zusätzliche Kosten entstehen können. In diesem Schritt wird die Modellinstanz erstellt, die zur Generierung der Empfehlungen für die Artikel verwendet wird.</p><p>Sie können ein Modell aus dem Ein-Klick-Katalog aussuchen:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta7bdfa43d6766324/6a170d6fb339d59e5476a039/b816e9fba1fe172687bf58f5143fb1f838c1077f-549x331.png" alt="Ansicht der Schnittstelle eines Modellkatalogs, der auf „smoll3“ gefiltert ist, mit einem Modell namens „smollm3‑3b“, das Textgenerierung, vLLM, GPU 1× Nvidia L4 und einen angegebenen Preis von 0,80 $ aufweist, sowie einem Hinweis, die Suche auf alle Hugging Face Modelle auszuweiten." /><p>Wählen Sie das <strong>SmolLM3-3B</strong> Modell:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdb0a2e6ffd7deb20/6a170d710c48574b7401aafc/610d3aba0429f3666c2df3616d513eb6a4397c0c-502x478.png" alt="Schnittstelle zum Erstellen eines Endpoints für das SmolLM3‑3B-Modell mit dem Modellnamen, dem Hinweis „verified by Hugging Face“, einem Feld für den Endpointnamen, Kosten von 0,80 $ pro Stunde und laufendem Replikat, einer cURL-Option und einer Schaltfläche „Create Endpoint“." /><p>Hier können Sie die URL des Hugging Face Endpoints abrufen:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt25714021711ed6ff/6a170d72c1e8a54853f88336/025094ddb2cfbd1f0f216a5ec4e119b0f4fa2c42-646x328.png" alt="Dashboard-Ansicht eines Hugging Face Inferenz-Endpoints mit dem Namen „smollm3‑3b‑pnz“, die den grünen Status „Running“, ein aktives Replikat, null Anfragen in der letzten Stunde, Navigationstabs und die angezeigte Endpoint-URL zeigt." /><p>Wie in der <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-put-hugging-face">Dokumentation zu den Hugging Face Inferenz-Endpunkten</a> von Elasticsearch erwähnt, erfordert die Textgenerierung ein Modell, das mit der OpenAI-API kompatibel ist. Aus diesem Grund müssen wir den <code>/v1/chat/completions</code>-Subpfad an die Hugging Face Endpoint-URL anhängen. Das Ergebnis sieht wie folgt aus:</p>https://j2g31h0futopfkli.us-east-1.aws.endpoints.huggingface.cloud/v1/chat/completions<p>Mit dieser Voraussetzung können wir mit dem Codieren in einem Python-Notebook beginnen.</p><h4>API-Schlüssel für Hugging Face generieren</h4><p>Erstellen Sie ein <a href="https://huggingface.co/join">Hugging Face Konto</a> und erhalten Sie ein API-Token, indem Sie <a href="https://huggingface.co/docs/hub/en/security-tokens#user-access-tokens">diesen Anweisungen</a> folgen. Man kann zwischen drei Token-Typen wählen: <em>detailliert</em> (empfohlen für die Produktion, da es nur Zugriff auf bestimmte Ressourcen bietet); <em>Lesezugriff</em> (schreibgeschützt); oder <em>Schreibzugriff</em> (für Lese- und Schreibzugriff). Für dieses Tutorial ist ein Lesezugriffstoken ausreichend, da wir nur den Inferenz-Endpoint aufrufen müssen. Speichern Sie diesen Code für den nächsten Schritt.</p><h4>Einrichten des Elasticsearch Inferenz-Endpoints</h4><p>Zunächst legen wir einen Elasticsearch-Python-Client fest:</p>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"]
)<p>Danach erstellen wir einen Elasticsearch Inferenz-Endpoint, der das Hugging Face Modell verwendet. Dieser Endpoint ermöglicht es uns, Reaktionen zu generieren, die auf den Blogbeiträgen und dem an das Modell übergebenen Prompt basieren.</p>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"],
            },
        },
    )<h3>Datensatz</h3><p>Der Datensatz enthält die <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/elasticsearch-inference-api-and-hugging-face/dataset.json">Blogbeiträge</a>, die abgefragt werden, und repräsentiert einen mehrsprachigen Inhaltssatz, der im gesamten Workflow verwendet wird:</p>// 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."
  }<h4>Elasticsearch-Mappings</h4><p>Mit dem definierten Datensatz müssen wir ein Datenschema erstellen, das der Struktur des Blogbeitrags entspricht. Die folgenden <a href="https://www.elastic.co/docs/manage-data/data-store/mapping">Index-Mappings</a> werden verwendet, um die Daten in Elasticsearch zu speichern:</p>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)<p>Hier können wir klar erkennen, wie die Daten strukturiert sind. Wir werden die semantische Suche verwenden, um Ergebnisse basierend auf natürlicher Sprache abzurufen, zusammen mit der <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/copy-to"><code>copy_to</code></a>-Eigenschaft, um den Feldinhalt in das <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-text"><code>semantic_text</code></a>-Feld zu kopieren. Zusätzlich enthält das <code>title</code>-Feld zwei Unterfelder: Das <code>original</code>-Unterfeld speichert den Titel entweder auf Englisch oder Spanisch, abhängig von der Originalsprache des Artikels, und das <code>translated_title</code>-Unterfeld ist nur für spanische Artikel vorhanden und enthält die englische Übersetzung des Originaltitels.</p><h3>Ingestieren von Daten</h3><p>Der folgende Code-Schnipsel überträgt den Datensatz des Blogbeitrags in Elasticsearch mithilfe der <a href="https://www.elastic.co/docs/reference/elasticsearch/clients/javascript/bulk_examples">Bulk-API</a>:</p>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)}")<p>Nun, da wir die Artikel in Elasticsearch aufgenommen haben, müssen wir eine Funktion erstellen, die nach dem Feld <code>semantic_text</code> sucht:</p>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 []<p>Wir benötigen außerdem eine Funktion, die den Endpoint aufruft. In diesem Fall rufen wir den Endpoint mit dem <strong><code>chat_completion</code></strong>Aufgabentyp auf, um Streaming-Antworten zu erhalten:</p>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"]) &gt; 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)}"<p>Nun können wir eine Funktion schreiben, die die semantische Suchfunktion, den <code>chat_completions</code> Inferenz-Endpoint und den Empfehlungs-Endoint aufruft, um die Daten zu generieren, die in den Karten angezeigt werden:</p>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<p>Abschließend müssen wir die Informationen extrahieren und formatieren, um sie zu drucken:</p>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 &lt;think&gt; tags
        cleaned_text = re.sub(
            r"&lt;think&gt;.*?&lt;/think&gt;", "", 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 + "┘")<p>Wir führen einen Test durch, indem wir eine Frage zu den Sicherheitsblogbeiträgen stellen:</p>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)<p>Hier sehen wir die vom Workflow in der Konsole generierten Karten:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4aa221a08a51aeb3/6a170d7460084be1413c45d6/730d35212594bb3db30447c3ea7e2a92857287b7-1999x1515.png" alt="Abschnitt mit dem Titel „Recommended Articles“ zeigt fünf zusammengefasste Artikel an, darunter Themen zu einer Schwachstelle im Authentifizierungssystem, Migrationsrisiken, Leistungs- und Authentifizierungsverbesserungen der REST API v2, Änderungen im Benachrichtigungssystem und eine vollständige Anleitung zur neuen API." /><p>Die vollständigen Ergebnisse, einschließlich aller Treffer und der LLM-Reaktion, können Sie in <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/elasticsearch-inference-api-and-hugging-face/results.md">dieser Datei</a> sehen.</p><p>Wir bitten um Artikel zum Thema: „Sicherheit und Schwachstellen“. Diese Frage wird als Suchabfrage gegen die in Elasticsearch gespeicherten Dokumente verwendet. Die abgerufenen Ergebnisse werden dann an das Modell weitergegeben, das basierend auf ihrem Inhalt Empfehlungen generiert. Wie wir sehen können, hat das Modell gute Arbeit geleistet und einen ansprechenden kurzen Text erstellt, der den Leser zum Anklicken motivieren kann.</p><h2>Fazit</h2><p>Dieses Beispiel zeigt, wie Elasticsearch und Hugging Face kombiniert werden können, um ein schnelles und effizientes zentrales System für KI-Anwendungen zu schaffen. Dieser Ansatz reduziert den manuellen Aufwand und bietet dank des umfangreichen Modellkatalogs von Hugging Face mehr Flexibilität. Insbesondere die Verwendung von SmolLM3-3B zeigt, wie kompakte, mehrsprachige Modelle in Kombination mit semantischer Suche weiterhin sinnvolles Schlussfolgern und Content-Generierung liefern können. Zusammen bieten diese Tools eine skalierbare und effektive Grundlage für die Entwicklung intelligenter Inhaltsanalysen und mehrsprachiger Anwendungen.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/hugging-face-elasticsearch-inference-api</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/hugging-face-elasticsearch-inference-api</guid>
    <category><![CDATA[Agentische KI]]></category>
    <category><![CDATA[Integrationen]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5f961af4cb26ec97/6a170d767d8d6790c770e790/1417d6ff033712206c9bd4bcc22074ee3437ce96-1999x1125.png" length="0" type="image/png"/>
    <pubDate>Mon, 23 Mar 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[So erstellen Sie mit LangGraph.js und Elasticsearch einen KI-Suchworkflow für das Finanzwesen]]></title>
    <description><![CDATA[Erfahren Sie, wie Sie LangGraph.js mit Elasticsearch verwenden können, um einen KI-gestützten Finanz-Suchworkflow zu erstellen, der Abfragen in natürlicher Sprache in dynamische, bedingte Filter für Investitions- und Marktanalysen umwandelt.]]></description>
    <content:encoded><![CDATA[<p>Beim Erstellen von KI-Suchanwendungen müssen häufig mehrere Aufgaben, der Datenabruf und die Datenextraktion zu einem nahtlosen Workflow koordiniert werden. LangGraph vereinfacht diesen Prozess, indem es Entwicklern ermöglicht, KI-Agenten mithilfe einer node-basierten Struktur zu orchestrieren. In diesem Artikel werden wir eine Finanzlösung mit <a href="https://langchain-ai.github.io/langgraphjs/">LangGraph.js</a> erstellen.</p><h2>Was ist LangGraph?</h2><p><a href="https://langchain-ai.github.io/langgraphjs/">LangGraph</a> ist ein Framework zum Erstellen von KI-Agenten und deren Orchestrierung in einem Workflow, um KI-unterstützte Anwendungen zu entwickeln. LangGraph verfügt über eine Knotenarchitektur, in der wir Funktionen deklarieren können, die Aufgaben darstellen, und diese als Knoten des Workflows zuweisen können. Das Ergebnis der Interaktion mehrerer Knoten ist ein Graph. LangGraph ist Teil des umfassenderen <a href="https://js.langchain.com/docs/introduction/">LangChain-Ökosystems</a>, das Tools für die Erstellung modularer und zusammensetzbarer KI-Systeme bereitstellt.</p><p>Zur Veranschaulichung dessen, warum LangGraph nützlich ist, werden wir eine problematische Situation damit lösen.</p><h2>Überblick über die Lösung</h2><p>In einem Risikokapitalunternehmen haben Investoren Zugriff auf eine umfangreiche Datenbank mit zahlreichen Filteroptionen, aber wenn man Kriterien kombinieren möchte, wird es schwierig und langsam. Dies kann dazu führen, dass einige relevante Start-ups für Investitionen nicht entdeckt werden. Das Ergebnis: Man verbringt viele Stunden damit, die besten Kandidaten zu identifizieren, oder verpasst sogar Chancen.</p><p>Mit LangGraph und Elasticsearch können wir gefilterte Suchen in natürlicher Sprache durchführen, sodass Nutzer komplexe Anfragen mit Dutzenden von Filtern nicht manuell erstellen müssen. Um die Flexibilität zu erhöhen, entscheidet der Workflow anhand der Nutzereingaben automatisch zwischen zwei Abfragetypen.</p><ul><li><p><strong>Investitionsorientierte Anfragen</strong>: Diese zielen auf finanzielle und finanzierungsbezogene Aspekte von Start-ups ab, wie <a href="https://www.investopedia.com/articles/personal-finance/102015/series-b-c-funding-what-it-all-means-and-how-it-works.asp">Finanzierungsrunden</a>, Bewertung oder <a href="https://www.investopedia.com/terms/r/revenue.asp">Umsatz</a>. <em>Beispiel:</em> „Suche Start-ups mit einer Series-A- oder Series-B-Finanzierung zwischen 8 und 25 Mio. Dollar und einem monatlichen Umsatz von über 500.000 Dollar.“</p></li><li><p><strong>Marktorientierte Anfragen</strong>: Diese konzentrieren sich auf <a href="https://en.wikipedia.org/wiki/Vertical_market">Branchensegmente</a>, <a href="https://en.wikipedia.org/wiki/Target_market">geografische Märkte</a> oder <a href="https://www.investopedia.com/terms/b/businessmodel.asp">Geschäftsmodelle</a> und helfen dabei, Chancen in bestimmten Sektoren oder Regionen zu identifizieren. <em>Beispiel:</em> „Suche Fintech- und Healthcare-Start-ups in San Francisco, New York oder Boston.“</p></li></ul><p>Um die Abfragen robust zu halten, werden wir das LLM dazu bringen, <a href="https://www.elastic.co/docs/solutions/search/search-templates">Suchvorlagen</a> anstelle vollständiger <a href="https://www.elastic.co/docs/explore-analyze/query-filter/languages/querydsl">DSL-Abfragen</a> zu erstellen. Auf diese Weise erhalten Sie immer die gewünschte Abfrage, und der LLM muss lediglich die Lücken ausfüllen und trägt nicht die Verantwortung, die benötigte Abfrage jedes Mal neu zu erstellen.</p><h2>Was Sie brauchen, um loszulegen</h2><ul><li><p>Elasticsearch API-Schlüssel</p></li><li><p>OpenAPI-API-Schlüssel</p></li><li><p>Node 18 oder neuer</p></li></ul><h2>Schritt-für-Schritt-Anweisungen</h2><p>In diesem Abschnitt schauen wir uns an, wie die App aussehen wird. Dafür verwenden wir <a href="https://www.typescriptlang.org/">TypeScript</a>, ein Superset von JavaScript, das statische Typen hinzufügt, um den Code zuverlässiger, leichter zu pflegen und sicherer zu machen, indem Fehler frühzeitig erkannt werden, während er vollständig kompatibel mit bestehendem JavaScript bleibt.</p><p>Der Knotenfluss sieht wie folgt aus:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt90db8f03f372608c/6a170986dc55de6e16e00d93/b47d7f238c4964a6febc0de7fe5e68b186f539c3-363x555.png" alt="" /><p>Das obige Bild wird von LangGraph generiert und stellt den Workflow dar, der die Ausführungsreihenfolge und die bedingte Logik zwischen den Knoten definiert:</p><ul><li><p><strong>decideStrategy: </strong>Verwendet ein LLM, um die Anfrage des Nutzers zu analysieren und zwischen zwei spezialisierten Suchstrategien zu entscheiden – investitionsorientiert oder marktorientiert.</p></li><li><p><strong>prepareInvestmentSearch: </strong>Extrahiert Filterwerte aus der Abfrage und erstellt eine vordefinierte Vorlage, die finanzielle und finanzierungsbezogene Parameter hervorhebt.</p></li><li><p><strong>prepareMarketSearch</strong>: Extrahiert ebenfalls Filterwerte, baut jedoch dynamisch Parameter auf, die den Markt-, Branchen- und geografischen Kontext betonen.</p></li><li><p><strong>executeSearch: </strong>Sendet die konstruierte Abfrage mit einer Suchvorlage an Elasticsearch und ruft die entsprechenden Start-up-Dokumente ab.</p></li><li><p><strong>VisualizeResults: </strong>Formatiert die Endergebnisse in einer klaren, lesbaren Zusammenfassung, die die wichtigsten Startup-Attribute wie Finanzierung, Branche und Umsatz aufzeigt.</p></li></ul><p>Dieser Fluss umfasst eine <a href="https://langchain-ai.github.io/langgraphjs/how-tos/branching/?h=conditional#how-to-create-branches-for-parallel-node-execution">bedingte Verzweigung</a>, die als „if“-Anweisung fungiert und basierend auf der Eingabe des Nutzers bestimmt, ob der Investitions- oder Marktsuchpfad verwendet wird. Diese vom LLM gesteuerte Entscheidungslogik macht den Workflow adaptiv und kontextsensitiv – ein Mechanismus, den wir in den nächsten Abschnitten genauer untersuchen werden.</p><h3>LangGraph-Status</h3><p>Bevor wir jeden Knoten einzeln betrachten, müssen wir verstehen, wie die Knoten kommunizieren und Daten austauschen. Dafür ermöglicht uns LangGraph, den Workflow-Status zu definieren. Dies definiert den gemeinsamen Status, der zwischen Knoten weitergegeben wird.</p><p>Der Zustand fungiert als gemeinsamer Container, der während des gesamten Workflows Zwischendaten speichert: Er beginnt mit der natürlichsprachlichen Anfrage des Nutzers, speichert dann die ausgewählte Suchstrategie, die vorbereiteten Parameter für Elasticsearch, die abgerufenen Suchergebnisse und schließlich den formatierten Ausgang.</p><p>Diese Struktur ermöglicht es jedem Knoten, den Status zu lesen und zu aktualisieren, wodurch ein konsistenter Informationsfluss vom Nutzer-Eingang bis zur endgültigen Visualisierung gewährleistet wird.</p>const VCState = Annotation.Root({
  input: Annotation&lt;string&gt;(), // User's natural language query
  searchStrategy: Annotation&lt;string&gt;(), // Search strategy chosen by LLM
  searchParams: Annotation&lt;any&gt;(), // Prepared search parameters
  results: Annotation&lt;any[]&gt;(), // Search results
  final: Annotation&lt;string&gt;(), // Final formatted response
});<h3>Einrichten der Anwendung</h3><p>Der gesamte Code in diesem Abschnitt ist im <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch">elasticsearch-labs-Repository</a> zu finden.</p><p>Öffnen Sie ein Terminal in dem Ordner, in dem sich die App befindet, und initialisieren Sie eine Node.js-Anwendung mit folgendem Befehl:</p>npm init -y<p>Nun können wir die notwendigen Abhängigkeiten für dieses Projekt installieren:</p>npm install @elastic/elasticsearch @langchain/langgraph @langchain/openai @langchain/core dotenv zod &amp;&amp; npm install --save-dev @types/node tsx typescript<ul><li><p><strong><code>@elastic/elasticsearch</code></strong>: Hilft uns bei der Bearbeitung von Elasticsearch-Anfragen, z. B. bei der Daten-Ingestion und dem Abrufen von Daten.</p></li><li><p><strong><code>@langchain/langgraph</code></strong>: JS-Abhängigkeit zur Bereitstellung aller LangGraph-Tools.</p></li><li><p><strong><code>@langchain/openai</code></strong>: LLM-Client von OpenAI für LangChain.</p></li><li><p>@langchain/core: Bietet die grundlegenden Bausteine für LangChain-Apps, einschließlich Prompt-Vorlagen.</p></li><li><p><strong><code>dotenv</code></strong>: Notwendige Abhängigkeit zur Verwendung von Umgebungsvariablen in JavaScript.</p></li><li><p><strong><code>zod</code></strong>: Abhängigkeit von Typdaten.</p></li></ul><p><code>@types/node</code> <code>tsx</code> <code>typescript</code> ermöglicht es uns, TypeScript-Code zu schreiben und auszuführen.</p><p>Erstellen Sie nun die folgenden Dateien:</p><ul><li><p><code>elasticsearchSetup</code><a href="http://ingest.ts/"><code>.ts</code></a>: Erstellt die Index-Mappings, lädt die Daten aus einer JSON-Datei und führt den Ingest der Daten in Elasticsearch durch.</p></li><li><p><a href="http://main.ts/"><code>main.ts</code></a>: Wird die LangGraph-Anwendung enthalten.</p></li><li><p><code>.env</code>Datei zum Speichern der Umgebungsvariablen</p></li></ul><p>In der <code>.env</code>-Datei fügen wir die folgenden Umgebungsvariablen hinzu:</p>ELASTICSEARCH_ENDPOINT="your-endpoint-here"
ELASTICSEARCH_API_KEY="your-key-here"
OPENAI_API_KEY="your-key-here"<p>Der OpenAPI-APIKey wird nicht direkt im Code verwendet; stattdessen wird er intern von der Bibliothek <code>@langchain/openai</code> verwendet.</p><p>Die gesamte Logik bezüglich der Erstellung von Mappings, der Erstellung von Suchvorlagen und der Datensatz-Ingestion kann in der Datei <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/elasticsearchSetup.ts"><code>elasticsearchSetup.ts</code></a> gefunden werden. In den nächsten Schritten konzentrieren wir uns auf die <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/main.ts"><code>main.ts</code></a>-Datei. Sie können den Datensatz auch überprüfen, um besser zu verstehen, wie die Daten in <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/dataset.json"><code>dataset.json</code></a> aussehen.</p><h3>LangGraph-App</h3><p>In der <code>main.ts</code>-Datei importieren wir einige notwendige Abhängigkeiten, um die LangGraph-Anwendung zu konsolidieren. In dieser Datei müssen Sie auch die Knotenfunktionen und die Zustandsdeklaration angeben. Die Graphdeklaration erfolgt in den nächsten Schritten anhand einer<code>main</code>-Methode. Die <code>elasticsearchSetup.ts</code>-Datei wird Elasticsearch-Helfer enthalten, die wir in den nächsten Schritten in den Knoten verwenden werden.</p>import { writeFileSync } from "node:fs";
import { StateGraph, Annotation, START, END } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { z } from "zod";
import {
  esClient,
  ingestDocuments,
  createSearchTemplates,
  INDEX_NAME,
  INVESTMENT_FOCUSED_TEMPLATE,
  MARKET_FOCUSED_TEMPLATE,
  createIndex,
} from "./elasticsearchSetup.js";

const llm = new ChatOpenAI({ model: "gpt-4o-mini" });<p>Wie bereits erwähnt, wird der LLM-Client verwendet, um die Elasticsearch-Suchvorlagenparameter basierend auf der Frage des Nutzers zu generieren.</p>async function saveGraphImage(app: any): Promise&lt;void&gt; {
  try {
    const drawableGraph = app.getGraph();
    const image = await drawableGraph.drawMermaidPng();
    const arrayBuffer = await image.arrayBuffer();

    const filePath = "./workflow_graph.png";
    writeFileSync(filePath, new Uint8Array(arrayBuffer));
    console.log(`📊 Workflow graph saved as: ${filePath}`);
  } catch (error: any) {
    console.log("⚠️  Could not save graph image:", error.message);
  }
}<p>Die oben dargelegte Methode erzeugt das Graphbild im PNG-Format und verwendet hinter den Kulissen die <a href="https://mermaid.ink/">Mermaid.INK-API</a>. Dies ist nützlich, wenn Sie sehen möchten, wie die App-Knoten mit einer gestylten Visualisierung zusammenwirken.</p><h3>LangGraph-Knoten</h3><p>Sehen wir uns nun die einzelnen Knoten im Detail an:</p><h3>decideSearchStrategy-Knoten</h3><p>Der <code>decideSearchStrategy</code>-Knoten analysiert die Eingabe und entscheidet, ob eine investitions- oder marktorientierte Suche durchgeführt werden soll. Er verwendet ein LLM mit einem strukturierten Ausgang (definiert mit Zod), um den Abfragetyp zu klassifizieren. Bevor die Entscheidung getroffen wird, werden mithilfe einer Aggregation die verfügbaren Filter aus dem Index abgerufen, um sicherzustellen, dass das Modell über einen aktuellen Kontext zu Branchen, Standorten und Finanzierungsdaten verfügt.</p><p>Um die möglichen Filterwerte zu extrahieren und an das LLM zu senden, verwenden wir eine <a href="https://www.elastic.co/docs/explore-analyze/query-filter/aggregations">Aggregation</a>, um sie direkt aus dem Elasticsearch-Index abzurufen. Diese Logik wird in einer Methode namens <code>getAvailableFilters</code>zugeordnet:</p>async function getAvailableFilters() {
  try {
    const response = await esClient.search({
      index: INDEX_NAME,
      size: 0,
      aggs: {
        industries: {
          terms: { field: "industry", size: 100 },
        },
        locations: {
          terms: { field: "location", size: 100 },
        },
        funding_stages: {
          terms: { field: "funding_stage", size: 20 },
        },
        business_models: {
          terms: { field: "business_model", size: 10 },
        },
        lead_investors: {
          terms: { field: "lead_investor", size: 100 },
        },
        funding_amount_stats: {
          stats: { field: "funding_amount" },
        },
      },
    });

    return response.aggregations;
  } catch (error) {
    console.error("❌ Error getting available filters:", error);
    return {};
  }
}<p>Mit der obigen Aggregationsanfrage erhalten wir die folgenden Ergebnisse:</p>{
  "industries": {
    "doc_count_error_upper_bound": 0,
    "sum_other_doc_count": 0,
    "buckets": [
      {
        "key": "logistics",
        "doc_count": 5
      },
      ...
    ]
  },
  "locations": {
    "doc_count_error_upper_bound": 0,
    "sum_other_doc_count": 0,
    "buckets": [
      {
        "key": "San Francisco, CA",
        "doc_count": 4
      },
      {
        "key": "New York, NY",
        "doc_count": 3
      },
      ...
    ]
  },
  "funding_stages": {
    "doc_count_error_upper_bound": 0,
    "sum_other_doc_count": 0,
    "buckets": [
      {
        "key": "Series A",
        "doc_count": 8
      },
      ...
    ]
  },
  "business_models": {
    "doc_count_error_upper_bound": 0,
    "sum_other_doc_count": 0,
    "buckets": [
      {
        "key": "B2B",
        "doc_count": 13
      },
      ...
    ]
  },
  "lead_investors": {
    "doc_count_error_upper_bound": 0,
    "sum_other_doc_count": 0,
    "buckets": [
      {
        "key": "Battery Ventures",
        "doc_count": 1
      },
      {
        "key": "Benchmark Capital",
        "doc_count": 1
      },
      ...
    ]
  },
  "funding_amount_stats": {
    "count": 20,
    "min": 4500000,
    "max": 35000000,
    "avg": 14075000,
    "sum": 281500000
  }
}<p>Alle Ergebnisse finden Sie <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/responses/aggregationsResponse.json">hier</a>.</p><p>Für beide Strategien verwenden wir eine hybride Suche, um sowohl den strukturierten Teil der Frage (Filter) als auch die subjektiveren Teile (Semantik) zu erkennen. Hier ist ein Beispiel für beide Abfragen unter Verwendung von <a href="https://www.elastic.co/docs/solutions/search/search-templates">Suchvorlagen</a>:</p>await esClient.putScript({
      id: INVESTMENT_FOCUSED_TEMPLATE,
      script: {
        lang: "mustache",
        source: `{
          "size": 5,
          "retriever": {
            "rrf": {
              "retrievers": [
                {
                  "standard": {
                    "query": {
                      "semantic": {
                        "field": "semantic_field",
                        "query": "{{query_text}}"
                      }
                    }
                  }
                },
                {
                  "standard": {
                    "query": {
                      "bool": {
                        "filter": [
                          {"terms": {"funding_stage": {{#join}}{{#toJson}}funding_stage{{/toJson}}{{/join}}}},
                          {"range": {"funding_amount": {"gte": {{funding_amount_gte}}{{#funding_amount_lte}},"lte": {{funding_amount_lte}}{{/funding_amount_lte}}}}},
                          {"terms": {"lead_investor": {{#join}}{{#toJson}}lead_investor{{/toJson}}{{/join}}}},
                          {"range": {"monthly_revenue": {"gte": {{monthly_revenue_gte}}{{#monthly_revenue_lte}},"lte": {{monthly_revenue_lte}}{{/monthly_revenue_lte}}}}}
                        ]
                      }
                    }
                  }
                }
              ],
              "rank_window_size": 100,
              "rank_constant": 20
            }
          }
        }`,
      },
    });<p>Sehen Sie sich die Abfragen an, die in der<a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/elasticsearchSetup.ts#L119"><code>elasticsearchSetup.ts</code></a>-Datei detailliert beschrieben sind. Im folgenden Knoten wird entschieden, welche der beiden Abfragen verwendet wird:</p>// Node 1: Decide search strategy using LLM
async function decideSearchStrategy(state: typeof VCState.State) {
  // Zod schema for specialized search strategy decision
  const SearchDecisionSchema = z.object({
    search_type: z
      .enum(["investment_focused", "market_focused"])
      .describe("Type of specialized search strategy to use"),
    reasoning: z
      .string()
      .describe("Brief explanation of why this search strategy was chosen"),
  });

  const decisionLLM = llm.withStructuredOutput(SearchDecisionSchema);

  // Get dynamic filters from Elasticsearch
  const availableFilters = await getAvailableFilters();

  const prompt = `Query: "${state.input}"
    Available filters: ${JSON.stringify(availableFilters, null, 2)}

    Choose between two specialized search strategies:
    
    - investment_focused: For queries about funding stages, funding amounts, monthly revenue, lead investors, financial performance
    
    - market_focused: For queries about industries, locations, business models, market segments, geographic markets
    
    Analyze the query intent and choose the most appropriate strategy.
  `;

  try {
    const result = await decisionLLM.invoke(prompt);
    console.log(
      `🤔 Search strategy: ${result.search_type} - ${result.reasoning}`
    );

    return {
      searchStrategy: result.search_type,
    };
  } catch (error: any) {
    console.error("❌ Error in decideSearchStrategy:", error.message);
    return {
      searchStrategy: "investment_focused",
    };
  }
}<h3>prepareInvestmentSearch- und prepareMarketSearch-Knoten</h3><p>Beide Knoten verwenden eine gemeinsame Hilfsfunktion, <code>extractFilterValues</code>, die das LLM nutzt, um relevante Filter zu identifizieren, die in den Nutzereingaben erwähnt werden, wie z. B. Branche, Standort, Finanzierungsphase, Geschäftsmodell usw. Wir verwenden dieses Schema, um unsere <a href="https://www.elastic.co/docs/solutions/search/search-templates">Suchvorlage</a> zu erstellen.</p>// Extract all possible filter values from user input
async function extractFilterValues(input: string) {
  const FilterValuesSchema = z.object({
    // Investment-focused filters
    funding_stage: z
      .array(z.string())
      .default([])
      .describe("Funding stage values mentioned in query"),
    funding_amount_gte: z
      .number()
      .default(0)
      .describe("Minimum funding amount in USD"),
    funding_amount_lte: z
      .number()
      .default(100000000)
      .describe("Maximum funding amount in USD"),
    lead_investor: z
      .array(z.string())
      .default([])
      .describe("Lead investor values mentioned in query"),
    monthly_revenue_gte: z
      .number()
      .default(0)
      .describe("Minimum monthly revenue in USD"),
    monthly_revenue_lte: z
      .number()
      .default(10000000)
      .describe("Maximum monthly revenue in USD"),
    industry: z
      .array(z.string())
      .default([])
      .describe("Industry values mentioned in query"),
    location: z
      .array(z.string())
      .default([])
      .describe("Location values mentioned in query"),
    business_model: z
      .array(z.string())
      .default([])
      .describe("Business model values mentioned in query"),
  });

  const extractorLLM = llm.withStructuredOutput(FilterValuesSchema);
  const availableFilters = await getAvailableFilters();

  const extractPrompt = `Extract ALL relevant filter values from: "${input}"
    Available options: ${JSON.stringify(availableFilters, null, 2)}
    Extract only values explicitly mentioned in the query. Leave fields empty if not mentioned.`;

  return await extractorLLM.invoke(extractPrompt);
}<p>Je nach erkannter Absicht wählt der Workflow einen von zwei Pfaden:</p><p><strong>prepareInvestmentSearch:</strong> Erstellt finanzorientierte Suchparameter, einschließlich Finanzierungsphase, Finanzierungsbetrag, Investor und Erneuerungsinformationen. Die gesamte Abfragevorlage finden Sie in der <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/elasticsearchSetup.ts"><code>elasticsearchSetup.ts</code></a>-Datei:</p>// Node 2A: Prepare Investment-Focused Search Parameters 
async function prepareInvestmentSearch(state: typeof VCState.State) {
  console.log(
    "💰 Preparing INVESTMENT-FOCUSED search parameters with financial emphasis..."
  );

  try {
    // Extract all filter values from input
    const values = await extractFilterValues(state.input);

    let searchParams: any = {
      template_id: INVESTMENT_FOCUSED_TEMPLATE,
      query_text: state.input,
      ...values,
    };

    return { searchParams };
  } catch (error) {
    console.error("❌ Error preparing investment-focused params:", error);
    return {
      searchParams: {},
    };
  }
}<p><strong>prepareMarketSearch:</strong> erstellt marktorientierte Parameter, die sich auf Branchen, Geografien und Geschäftsmodelle konzentrieren. Die vollständige Abfrage finden Sie in der Datei <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/elasticsearchSetup.ts"><code>elasticsearchSetup.ts</code></a>:</p>// Node 2B: Prepare Market-Focused Search Parameters
async function prepareMarketSearch(state: typeof VCState.State) {
  console.log(
    "🔍 Preparing MARKET-FOCUSED search parameters with market emphasis..."
  );

  try {
    // Extract all filter values from input
    const values = await extractFilterValues(state.input);

    let searchParams: any = {
      template_id: MARKET_FOCUSED_TEMPLATE,
      query_text: state.input,
      ...values,
    };

    return { searchParams };
  } catch (error) {
    console.error("❌ Error preparing market-focused params:", error);
    return {};
  }
}<h3>executeSearch-Knoten</h3><p>Dieser Knoten nimmt die gesuchten Parameter aus dem Zustand und sendet sie zuerst an Elasticsearch, wobei er die <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-render-search-template">_render-API</a> verwendet, um die Abfrage für Debugging-Zwecke zu visualisieren, und sendet dann eine Anfrage zur Abrufung der Ergebnisse.</p>// Node 3: Execute Search
async function executeSearch(state: typeof VCState.State) {
  const { searchParams } = state;

  try {
    // getting formed query from template for debugging
    const renderedTemplate = await esClient.renderSearchTemplate({
      id: searchParams.template_id,
      params: searchParams,
    });

    console.log(
      "📋 Complete query:",
      JSON.stringify(renderedTemplate.template_output, null, 2)
    );

    const results = await esClient.searchTemplate({
      index: INDEX_NAME,
      id: searchParams.template_id,
      params: searchParams,
    });

    return {
      results: results.hits.hits.map((hit: any) =&gt; hit._source),
    };
  } catch (error: any) {
    console.error(`❌ ${state.searchParams.search_type} search error:`, error);
    return { results: [] };
  }
}<h3>visualizeResults-Knoten</h3><p>Dieser Knoten zeigt schließlich die Elasticsearch-Ergebnisse an.</p>// Node 4: Visualize results
async function visualizeResults(state: typeof VCState.State) {
  const results = state.results || [];

  let formattedResults = `🎯 Found ${results.length} startups matching your criteria:\n\n`;

  results.forEach((startup: any, index: number) =&gt; {
    formattedResults += `${index + 1}. **${startup.company_name}**\n`;
    formattedResults += `   📍 ${startup.location} | 🏢 ${startup.industry} | 💼 ${startup.business_model}\n`;
    formattedResults += `   💰 ${startup.funding_stage} - $${(
      startup.funding_amount / 1000000
    ).toFixed(1)}M\n`;
    formattedResults += `   👥 ${startup.employee_count} employees | 📈 $${(
      startup.monthly_revenue / 1000
    ).toFixed(0)}K MRR\n`;
    formattedResults += `   🏦 Lead: ${startup.lead_investor}\n`;
    formattedResults += `   📝 ${startup.description}\n\n`;
  });

  return {
    final: formattedResults,
  };
}<p>Programmatisch sieht der gesamte Graph so aus:</p>  const workflow = new StateGraph(VCState)
    // Register nodes - these are the processing functions
    .addNode("decideStrategy", decideSearchStrategy)
    .addNode("prepareInvestment", prepareInvestmentSearch)
    .addNode("prepareMarket", prepareMarketSearch)
    .addNode("executeSearch", executeSearch)
    .addNode("visualizeResults", visualizeResults)
    // Define execution flow with conditional branching
    .addEdge(START, "decideStrategy") // Start with strategy decision
    .addConditionalEdges(
      "decideStrategy",
      (state: typeof VCState.State) =&gt; state.searchStrategy, // Conditional function
      {
        investment_focused: "prepareInvestment", // If investment focused -&gt; RRF template preparation
        market_focused: "prepareMarket", // If market focused -&gt; dynamic query preparation
      }
    )
    .addEdge("prepareInvestment", "executeSearch") // Investment prep -&gt; execute
    .addEdge("prepareMarket", "executeSearch") // Market prep -&gt; execute
    .addEdge("executeSearch", "visualizeResults") // Execute -&gt; visualize
    .addEdge("visualizeResults", END); // End workflow<p>Wie Sie sehen können, haben wir eine bedingte Kante, bei der die App entscheidet, welcher „Pfad“ oder Knoten als Nächstes ausführen wird. Dieses Feature ist nützlich, wenn Workflows Verzweigungslogik benötigen, etwa die Wahl zwischen mehreren Tools oder das Einfügen eines Human-in-the-Loop-Schrittes.</p><p>Nachdem wir die Kern-Features von LangGraph verstanden haben, können wir die Anwendung einrichten, in der der Code ausgeführt werden soll:</p><p>Alles wird in einer <code>main</code>-Methode zusammengefasst. Hier deklarieren wir den Graphen mit allen Elementen unter der Variablen „Workflow“:</p>async function main() {
  await createIndex();
  await createSearchTemplates();
  await ingestDocuments();

  // Create the workflow graph with shared state
  const workflow = new StateGraph(VCState)
    // Register nodes - these are the processing functions
    .addNode("decideStrategy", decideSearchStrategy)
    .addNode("prepareInvestment", prepareInvestmentSearch)
    .addNode("prepareMarket", prepareMarketSearch)
    .addNode("executeSearch", executeSearch)
    .addNode("visualizeResults", visualizeResults)
    // Define execution flow with conditional branching
    .addEdge(START, "decideStrategy") // Start with strategy decision
    .addConditionalEdges(
      "decideStrategy",
      (state: typeof VCState.State) =&gt; state.searchStrategy, // Conditional function
      {
        investment_focused: "prepareInvestment", // If investment focused -&gt; RRF template preparation
        market_focused: "prepareMarket", // If market focused -&gt; dynamic query preparation
      }
    )
    .addEdge("prepareInvestment", "executeSearch") // Investment prep -&gt; execute
    .addEdge("prepareMarket", "executeSearch") // Market prep -&gt; execute
    .addEdge("executeSearch", "visualizeResults") // Execute -&gt; visualize
    .addEdge("visualizeResults", END); // End workflow


  const app = workflow.compile();

  await saveGraphImage(app);

  const query =
    "Find startups with Series A or Series B funding between $8M-$25M and monthly revenue above $500K";

  const marketResult = await app.invoke({ input: query });
  console.log(marketResult.final);
}<p>Die Abfragevariable simuliert die in einer hypothetischen Suchleiste eingegebenen Nutzereingaben:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltba7189d5f4e63403/6a1709880e2e49cc3041a076/e8d76909eb2bc1bb62f3ca9a8b3e4b85fcec2893-1600x164.png" alt="" /><p>Aus der natürlichsprachlichen Formulierung „Suche Start-ups mit Series-A- oder Series-B-Finanzierung zwischen 8 und 25 Millionen US-Dollar und einem monatlichen Umsatz von über 500.000 US-Dollar“ werden alle Filter extrahiert.</p><p>Rufen Sie abschließend die Hauptmethode auf:</p>main().catch(console.error);<h3>Ergebnisse</h3>🔍 Checking if index exists...
🏗️ Creating index...
✅ Index created successfully!
Ingesting documents...
✅ Documents ingested successfully!
✅ Investment-focused template created successfully!
✅ Market-focused template created successfully!

📊 Workflow graph saved as: ./workflow_graph.png

🔍 Query: "Find startups with Series A or Series B funding between $8M-$25M and monthly revenue above $500K"

🤔 Search strategy: investment_focused - The query specifically seeks profitable fintech startups with defined funding amounts and high monthly revenue, which aligns closely with financial performance metrics and investment-related criteria.

💰 Preparing INVESTMENT-FOCUSED search parameters with financial emphasis...

📋 Complete query: {
  "size": 5,
  "retriever": {
    "rrf": {
      "retrievers": [
        {
          "standard": {
            "query": {
              "semantic": {
                "field": "semantic_field",
                "query": "Find startups with Series A or Series B funding between $8M-$25M and monthly revenue above $500K"
              }
            }
          }
        },
        {
          "standard": {
            "query": {
              "bool": {
                "filter": [
                  {
                    "terms": {
                      "funding_stage": [
                        "Series A",
                        "Series B"
                      ]
                    }
                  },
                  {
                    "range": {
                      "funding_amount": {
                        "gte": 8000000,
                        "lte": 25000000
                      }
                    }
                  },
                  {
                    "terms": {
                      "lead_investor": []
                    }
                  },
                  {
                    "range": {
                      "monthly_revenue": {
                        "gte": 500000,
                        "lte": 0
                      }
                    }
                  }
                ]
              }
            }
          }
        }
      ],
      "rank_window_size": 100,
      "rank_constant": 20
    }
  }
}
🎯 Found 5 startups matching your criteria:

1. **TechFlow**
   📍 San Francisco, CA | 🏢 logistics | 💼 B2B
   💰 Series A - $8.0M
   👥 45 employees | 📈 $500K MRR
   🏦 Lead: Sequoia Capital
   📝 TechFlow optimizes supply chain operations using AI-powered route optimization and real-time tracking. Founded in 2023, shows remarkable growth with $500K monthly revenue.

2. **DataViz**
   📍 New York, NY | 🏢 enterprise software | 💼 B2B
   💰 Series A - $10.0M
   👥 42 employees | 📈 $450K MRR
   🏦 Lead: Battery Ventures
   📝 DataViz creates intuitive data visualization tools for enterprise customers. No-code platform allows business users to create dashboards without technical expertise.

3. **FinanceAI**
   📍 San Francisco, CA | 🏢 fintech | 💼 B2C
   💰 Series C - $25.0M
   👥 120 employees | 📈 $1200K MRR
   🏦 Lead: Tiger Global Management
   📝 FinanceAI provides AI-powered investment advisory services to retail investors. Uses machine learning to analyze market trends with over 100,000 active users.

4. **UrbanMobility**
   📍 New York, NY | 🏢 logistics | 💼 B2B2C
   💰 Series B - $15.0M
   👥 78 employees | 📈 $750K MRR
   🏦 Lead: Kleiner Perkins
   📝 UrbanMobility revolutionizes urban transportation through autonomous delivery drones and smart logistics hubs. Partners with major retailers for same-day delivery across Manhattan and Brooklyn.

5. **HealthTech Solutions**
   📍 Boston, MA | 🏢 healthcare | 💼 B2B
   💰 Series B - $18.0M
   👥 95 employees | 📈 $900K MRR
   🏦 Lead: General Catalyst
   📝 HealthTech Solutions develops medical devices and software for remote patient monitoring. Comprehensive telehealth platform reducing hospital readmissions by 30%.

✨  Done in 18.80s.<p>Für die gesendete Eingabe wählt die Anwendung den <strong>investitionsorientierten</strong> Pfad, wodurch wir die Elasticsearch-Abfrage sehen, die vom Workflow generiert wird und die Werte und Bereiche aus dem Eingang des Nutzers extrahiert. Wir können auch die an Elasticsearch gesendete Anfrage mit den extrahierten Werten sehen, und schließlich die vom <code>visualizeResults</code>-Knoten formatierten Ergebnisse.</p><p>Testen wir nun den <strong>marktorientierten</strong> Knoten mit der Abfrage „Suche Fintech- und Healthcare-Start-ups in San Francisco, New York oder Boston.“:</p>...

🔍 Query: Find fintech and healthcare startups in San Francisco, New York, or Boston

🤔 Search strategy: market_focused - The query is focused on finding fintech startups in San Francisco that are disrupting traditional banking and payment systems, which pertains to specific industries (fintech) and locations (San Francisco). Thus, a market-focused strategy is more appropriate.

🔍 Preparing MARKET-FOCUSED search parameters with market emphasis...

📋 Complete query: {
  "size": 5,
  "retriever": {
    "rrf": {
      "retrievers": [
        {
          "standard": {
            "query": {
              "semantic": {
                "field": "semantic_field",
                "query": "Find fintech and healthcare startups in San Francisco, New York, or Boston"
              }
            }
          }
        },
        {
          "standard": {
            "query": {
              "bool": {
                "filter": [
                  {
                    "terms": {
                      "industry": [
                        "fintech",
                        "healthcare"
                      ]
                    }
                  },
                  {
                    "terms": {
                      "location": [
                        "San Francisco, CA",
                        "New York, NY",
                        "Boston, MA"
                      ]
                    }
                  },
                  {
                    "terms": {
                      "business_model": []
                    }
                  }
                ]
              }
            }
          }
        }
      ],
      "rank_window_size": 50,
      "rank_constant": 10
    }
  }
}
🎯 Found 5 startups matching your criteria:

1. **FinanceAI**
   📍 San Francisco, CA | 🏢 fintech | 💼 B2C
   💰 Series C - $25.0M
   👥 120 employees | 📈 $1200K MRR
   🏦 Lead: Tiger Global Management
   📝 FinanceAI provides AI-powered investment advisory services to retail investors. Uses machine learning to analyze market trends with over 100,000 active users.

2. **CryptoWallet**
   📍 Miami, FL | 🏢 fintech | 💼 B2C
   💰 Series B - $16.0M
   👥 73 employees | 📈 $820K MRR
   🏦 Lead: Coinbase Ventures
   📝 CryptoWallet provides secure digital wallet solutions for cryptocurrency trading and storage. Multi-chain support with enterprise-grade security features.

...

✨  Done in 7.41s.<h2>Erkenntnisse</h2><p>Während des Schreibprozesses habe ich Folgendes gelernt:</p><ul><li><p>Wir müssen dem LLM die exakten Werte der Filter zeigen, sonst sind wir darauf angewiesen, dass der Nutzer die präzisen Werte eingibt. Bei niedriger Kardinalität ist dieser Ansatz akzeptabel, aber wenn die Kardinalität hoch ist, benötigen wir einen Mechanismus zum Herausfiltern der Ergebnisse.</p></li><li><p>Die Verwendung von Suchvorlagen führt zu deutlich konsistenteren Ergebnissen als die automatische Generierung der Elasticsearch-Abfrage durch das LLM und ist zudem schneller.</p></li><li><p>Bedingte Kanten sind ein leistungsstarker Mechanismus, um Anwendungen mit mehreren Varianten und verzweigten Pfaden zu erstellen.</p></li><li><p>Strukturierte Ausgaben sind bei der Informationsgenerierung mit LLMs äußerst nützlich, da sie vorhersehbare, typsichere Antworten erzwingen. Dies verbessert die Zuverlässigkeit und reduziert Fehlinterpretationen von Prompts.</p></li></ul><p>Die Kombination von semantischem und strukturiertem Suchen durch hybriden Abruf führt zu besseren und relevanteren Ergebnissen, wobei Präzision und Kontextverständnis in Einklang gebracht werden.</p><h2>Fazit</h2><p>In diesem Beispiel kombinieren wir LangGraph.js mit Elasticsearch, um einen dynamischen Workflow zu schaffen, der natürliche Sprachanfragen interpretieren und zwischen finanz- oder marktorientierten Suchstrategien entscheiden kann. Dieser Ansatz reduziert die Komplexität manueller Abfragen und verbessert gleichzeitig die Flexibilität und Genauigkeit für Risikokapitalanalysten.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/ai-agent-workflow-finance-langgraph-elasticsearch</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/ai-agent-workflow-finance-langgraph-elasticsearch</guid>
    <category><![CDATA[KI]]></category>
    <category><![CDATA[Agentische KI]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt013eba5d152f11f3/6a1709892b835f6784f4b1a6/12b6057d84c6356267cd178a3c6c1a5c61123ece-2000x1256.png" length="0" type="image/png"/>
    <pubDate>Fri, 05 Dec 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[KI-gestützte Dashboards: Von der Vision zu Kibana]]></title>
    <description><![CDATA[Generieren Sie ein Dashboard mithilfe eines LLM, um ein Bild zu verarbeiten und in ein Kibana-Dashboard umzuwandeln.
]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/kibana/kibana-lens">Kibana Lens</a> macht das Erstellen von Dashboards per Drag &amp; Drop sehr einfach, aber wenn man Dutzende von Panels benötigt, summieren sich die Klicks. Was wäre, wenn Sie ein Dashboard skizzieren, einen Screenshot davon machen und einen LLM den gesamten Prozess für Sie abschließen lassen könnten?</p><p>In diesem Artikel werden wir genau das tun. Wir werden eine Anwendung erstellen, die ein Bild eines Dashboards aufnimmt, unsere Mappings analysiert und anschließend ein Dashboard generiert, ohne dass wir Kibana überhaupt berühren müssen!</p><p><strong>Schritte</strong>:</p><ol><li><p><a href="https://www.elastic.co/search-labs/blog/ai-powered-dashboards#background-&amp;-application-workflow">Hintergrund und Anwendungsablauf</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/ai-powered-dashboards#prepare-data">Daten vorbereiten</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/ai-powered-dashboards#llm-configuration">LLM-Konfiguration</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/ai-powered-dashboards#application-functions">Anwendungsfunktionen</a></p></li></ol><h2>Hintergrund und Anwendungsablauf</h2><p>Mein erster Gedanke war, den LLM das gesamte NDJSON-Format der in Kibana <a href="https://www.elastic.co/docs/explore-analyze/find-and-organize/saved-objects">gespeicherten Objekte</a> generieren zu lassen und sie dann in Kibana zu importieren.</p><p>Wir haben eine Handvoll Modelle ausprobiert:</p><ul><li><p>Gemini 2.5 Pro</p></li><li><p>GPT o3 / o4-mini-hoch / 4.1</p></li><li><p>Claude 4 Sonett</p></li><li><p>Grok 3</p></li><li><p>Deepseek (Deepthink R1)</p></li></ul><p>Und als Anregungen begannen wir mit ganz einfachen Dingen:</p>You are an Elasticsearch Saved-Object generator (Kibana 9.0).
INPUTS
=====
1. PNG screenshot of a 4-panel dashboard (attached).
2. Index mapping (below) – trimmed down to only the fields present in the screenshot.
3. Example NDJSON of *one* metric visualization (below) for reference.

TASK
====
Return **only** a valid NDJSON array that recreates the dashboard exactly:
* 2 metric panels (Visits, Unique Visitors)
* 1 pie chart (Most used OS)
* 1 vertical bar chart (State Geo Dest)
* Use index pattern `kibana_sample_data_logs`.
* Preserve roughly the same layout (2×2 grid).
* Use `panelIndex` values 1-4 and random `id` strings.
* Kibana version: 9.0<p>Trotz der Durchsicht <a href="https://www.elastic.co/search-labs/blog/function-calling-with-elastic#:~:text=Few%2Dshot%20prompting%20involves%20providing%20examples%20of%20the%20types%20of%20queries%20you%20want%20it%20to%20return%2C%20which%20helps%20in%20increasing%20consistency.">einiger weniger Beispiele</a> und detaillierter Erklärungen zum Aufbau der einzelnen Visualisierungen hatten wir keinen Erfolg. Wenn Sie an diesem Experiment interessiert sind, finden Sie <a href="https://gist.github.com/TomasMurua/a78dc283e115624731beffc98984b70b">hier</a> weitere Informationen.</p><p>Das Ergebnis dieser Vorgehensweise war, dass beim Versuch, die vom LLM erzeugten Dateien in Kibana hochzuladen, diese Meldungen angezeigt wurden:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9ea005966a783057/6a1707d266c4f90e4ef8bf88/2b599443b5613c9f0fc3235581614add5b4b3900-891x98.png" alt="" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5e5632d6d95b998c/6a1707d3a6c2b9441de79661/d87ccfc033bc00ee8188c5cae18043fbca22784c-741x233.png" alt="" /><p>Das bedeutet, dass das generierte JSON ungültig oder schlecht formatiert ist. Die häufigsten Probleme waren, dass der LLM unvollständiges NDJSON erzeugte, Parameter falsch interpretierte oder normales JSON anstelle von NDJSON zurückgab, egal wie sehr wir uns bemühten, das Gegenteil zu erzwingen.</p><p>Inspiriert von <a href="https://www.elastic.co/search-labs/blog/llm-functions-elasticsearch-intelligent-query">diesem Artikel</a> – in dem <a href="https://www.elastic.co/docs/solutions/search/search-templates">Suchvorlagen</a> besser funktionierten als LLM Freestyle – entschieden wir uns, dem LLM Vorlagen zu übergeben, anstatt die vollständige NDJSON-Datei generieren zu lassen und anschließend im Code die vom LLM bereitgestellten Parameter zur Erstellung der Visualisierungen zu verwenden. Dieser Ansatz hat sich bewährt und ist vorhersehbar und erweiterbar, da nun der Code und nicht mehr das LLM die Hauptarbeit übernimmt.</p><p>Der Bewerbungsprozess wird wie folgt ablaufen:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9f7738a4c7ddd0cd/6a1707d52b835f0a25f4b166/52c587cf0cf3517fdd4ee7ab95581dd4f2bce030-725x668.png" alt="" /><p></p><p><em>Der Einfachheit halber lassen wir einige Codeabschnitte weg, aber den vollständigen, lauffähigen Code der Anwendung finden Sie in </em><a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/from-image-idea-to-kibana-dashboard-using-ai/from-image-idea-to-kibana-dashboard-using-ai.ipynb"><em><strong>diesem</strong></em></a><em> Notebook.</em></p><h2>Voraussetzungen</h2><p>Bevor Sie mit der Entwicklung beginnen, benötigen Sie Folgendes:</p><ol><li><p>Python 3.8 oder höher</p></li><li><p>Eine <a href="https://docs.python.org/3/library/venv.html">Venv</a> Python-Umgebung</p></li><li><p>Eine laufende Elasticsearch-Instanz, zusammen mit ihrem Endpunkt und API-Schlüssel</p></li><li><p>Ein OpenAI-API-Schlüssel, der unter dem Umgebungsvariablennamen OPENAI_API_KEY gespeichert ist:</p></li></ol>export OPENAI_API_KEY="your-openai-api-key"<h2>Daten vorbereiten</h2><p>Für die Daten halten wir es einfach und verwenden Elastic-Beispiel-Weblogs. <a href="https://www.elastic.co/docs/manage-data/ingest/sample-data#add-sample-data-sets">Hier</a> erfahren Sie, wie Sie diese Daten in Ihren Cluster importieren.</p><p>Jedes Dokument enthält Angaben zum Host, der die Anfragen an die Anwendung gestellt hat, sowie Informationen zur Anfrage selbst und deren Antwortstatus. Nachfolgend finden Sie ein Beispieldokument:</p>{
    "agent": "Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24",
    "bytes": 8509,
    "clientip": "70.133.115.149",
    "extension": "css",
    "geo": {
        "srcdest": "US:IT",
        "src": "US",
        "dest": "IT",
        "coordinates": {
            "lat": 38.05134111,
            "lon": -103.5106908
        }
    },
    "host": "cdn.elastic-elastic-elastic.org",
    "index": "kibana_sample_data_logs",
    "ip": "70.133.115.149",
    "machine": {
        "ram": 5368709120,
        "os": "osx"
    },
    "memory": null,
    "message": "70.133.115.149 - - [2018-08-30T23:35:31.492Z] \"GET /styles/semantic-ui.css HTTP/1.1\" 200 8509 \"-\" \"Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\"",
    "phpmemory": null,
    "referer": "http://twitter.com/error/john-phillips",
    "request": "/styles/semantic-ui.css",
    "response": 200,
    "tags": [
        "success",
        "info"
    ],
    "@timestamp": "2025-07-03T23:35:31.492Z",
    "url": "https://cdn.elastic-elastic-elastic.org/styles/semantic-ui.css",
    "utc_time": "2025-07-03T23:35:31.492Z",
    "event": {
        "dataset": "sample_web_logs"
    },
    "bytes_gauge": 8509,
    "bytes_counter": 51201128
}<p>Nun holen wir uns die Zuordnungen des soeben geladenen Index, <code>kibana_sample_data_logs</code>:</p>INDEX_NAME = "kibana_sample_data_logs"

es_client = Elasticsearch(
    [os.getenv("ELASTICSEARCH_URL")],
    api_key=os.getenv("ELASTICSEARCH_API_KEY"),
)

result = es_client.indices.get_mapping(index=INDEX_NAME)
index_mappings = result[list(result.keys())[0]]["mappings"]["properties"]<p>Wir werden die Zuordnungen zusammen mit dem Bild übergeben, das wir später laden werden.</p><h2>LLM-Konfiguration</h2><p>Konfigurieren wir das LLM so, dass es <a href="https://python.langchain.com/docs/concepts/structured_outputs/">strukturierte Ausgabe</a> verwendet, um ein Bild einzugeben und ein JSON mit den Informationen zu erhalten, die wir an unsere Funktion übergeben müssen, um die JSON-Objekte zu erzeugen.</p><p>Wir installieren die Abhängigkeiten:</p>pip install elasticsearch pydantic langchain langchain-openai -q<p>Elasticsearch wird uns dabei helfen, die <a href="https://www.elastic.co/docs/manage-data/data-store/mapping">Indexzuordnungen</a> abzurufen. Pydantic ermöglicht es uns, Schemata in Python zu definieren, denen der LLM dann folgen soll, und <a href="https://www.elastic.co/search-labs/integrations/langchain">LangChain</a> ist das Framework, das den Aufruf von LLMs und KI-Tools vereinfacht.</p><p>Wir werden ein Pydantic-Schema erstellen, um die gewünschte Ausgabe des LLM zu definieren. Aus dem Bild müssen wir den Diagrammtyp, das Feld, den Visualisierungstitel und den Dashboard-Titel ablesen:</p>class Visualization(BaseModel):
    title: str = Field(description="The dashboard title")
    type: List[Literal["pie", "bar", "metric"]]
    field: str = Field(
        description="The field that this visualization use based on the provided mappings"
    )


class Dashboard(BaseModel):
    title: str = Field(description="The dashboard title")
    visualizations: List[Visualization]<p>Als Bildeingabe senden wir ein Dashboard, das ich gerade gezeichnet habe:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7870f6421986d11d/6a1707d78b73cb3408189fa3/36441d7b5dc1f3ff2ac2a30710208d57ad41c716-1600x898.jpg" alt="" /><p>Nun deklarieren wir den LLM-Modellaufruf und das Laden des Bildes. Diese Funktion erhält die Zuordnungen des Elasticsearch-Index und ein Bild des Dashboards, das wir generieren möchten.</p><p>Mit <code>with_structured_output</code> können wir unser Pydantic <code>Dashboard</code> Schema als Antwortobjekt verwenden, das der LLM erzeugen wird. Mit <a href="https://docs.pydantic.dev/latest/">Pydantic</a> können wir Datenmodelle mit Validierung definieren, wodurch sichergestellt wird, dass die LLM-Ausgabe der erwarteten Struktur entspricht.</p><p>Um das Bild in Base64 zu konvertieren und als Eingabe zu senden, können Sie einen <a href="https://www.base64-image.de/">Online-Konverter</a> verwenden oder dies <a href="https://www.geeksforgeeks.org/python-convert-image-to-string-and-vice-versa/">im Code</a> erledigen.</p>prompt = f"""
    You are an expert in analyzing Kibana dashboards from images for the version 9.0.0 of Kibana.

    You will be given a dashboard image and an Elasticsearch index mapping.

    Below are the index mappings for the index that the dashboard is based on.
    Use this to help you understand the data and the fields that are available.

    Index Mappings:
    {index_mappings}

    Only include the fields that are relevant for each visualization, based on what is visible in the image.
    """

message = [
    {
        "role": "user",
        "content": [
            {"type": "text", "text": prompt},
            {
                "type": "image",
                "source_type": "base64",
                "data": image_base64,
                "mime_type": "image/png",
            },
        ],
    }
]


try:
    llm = init_chat_model("gpt-4.1-mini")
    llm = llm.with_structured_output(Dashboard)
    dashboard_values = llm.invoke(message)

    print("Dashboard values generated by the LLM successfully")
    print(dashboard_values)
except Exception as e:
    print(f"Failed to analyze image and match fields: {str(e)}")<p>Der LLM verfügt bereits über Kontextinformationen zu Kibana-Dashboards, daher müssen wir nicht alles in der Eingabeaufforderung erklären, sondern nur einige Details, um sicherzustellen, dass er nicht vergisst, dass er mit Elasticsearch und Kibana arbeitet.</p><p>Lassen Sie uns die Aufgabenstellung aufschlüsseln:</p><p>Abschnitt</p><p>Grund</p><p>Sie sind Experte in der Analyse von Kibana-Dashboards anhand von Images für die Version 9.0.0 von Kibana.</p><p>Durch die Verstärkung dieser Funktion wird Elasticsearch und die Verwendung der Elasticsearch-Version unterstützt, wodurch die Wahrscheinlichkeit verringert wird, dass das LLM alte/ungültige Parameter fälschlicherweise annimmt.</p><p>Sie erhalten ein Dashboard-Bild und eine Elasticsearch-Indexzuordnung.</p><p>Wir erklären, dass es sich bei dem Bild um Dashboards handelt, um Fehlinterpretationen seitens des LLM zu vermeiden.</p><p>Nachfolgend finden Sie die Indexzuordnungen für den Index, auf dem das Dashboard basiert. Nutzen Sie diese, um die Daten und die verfügbaren Felder besser zu verstehen. Indexzuordnungen: {index_mappings}</p><p>Es ist entscheidend, die Zuordnungen bereitzustellen, damit das LLM dynamisch gültige Felder auswählen kann. Andernfalls könnten wir die Zuordnungen hier fest codieren, was zu starr wäre, oder uns darauf verlassen, dass das Bild die richtigen Feldnamen enthält, was nicht zuverlässig ist.</p><p>Beschränken Sie sich auf die Felder, die für die jeweilige Visualisierung relevant sind, basierend auf dem, was im Bild sichtbar ist.</p><p>Wir mussten diese Verstärkung hinzufügen, weil manchmal versucht wird, Felder hinzuzufügen, die nicht zum Bild gehören.</p><p>Dies gibt ein Objekt mit einem Array von anzuzeigenden Visualisierungen zurück:</p>"Dashboard values generated by the LLM successfully
title=""Client, Extension, OS, and Response Keyword Analysis""visualizations="[
   "Visualization(title=""Count of Client IP",
   "type="[
      "metric"
   ],
   "field=""clientip"")",
   "Visualization(title=""Extension Keyword Distribution",
   "type="[
      "pie"
   ],
   "field=""extension.keyword"")",
   "Visualization(title=""Most Used OS",
   "type="[
      "bar"
   ],
   "field=""machine.os.keyword"")",
   "Visualization(title=""Response Keyword Distribution",
   "type="[
      "bar"
   ],
   "field=""response.keyword"")"
]<h2>Verarbeitung der LLM-Antwort</h2><p>WirWir haben ein Beispiel-Dashboard mit 2x2-Panels erstellt und es dann mithilfe der <a href="https://www.elastic.co/docs/api/doc/kibana/operation/operation-get-dashboards-dashboard">Get a dashboard API</a> im JSON-Format exportiert. Anschließend haben wir die Panels als Visualisierungsvorlagen (Kreisdiagramm, Balkendiagramm, Metrikdiagramm) gespeichert, in denen wir einige der Parameter ersetzen können, um je nach Fragestellung neue Visualisierungen mit anderen Feldern zu erstellen.</p><p>Die JSON-Vorlagedateien können Sie <a href="https://github.com/Delacrobix/elasticsearch-labs/tree/supporting-blog-content/from-image-idea-to-kibana-dashboard-using-ai/supporting-blog-content/from-image-idea-to-kibana-dashboard-using-ai/templates"><strong>hier</strong></a> einsehen. Beachten Sie, wie wir die Objektwerte, die wir später ersetzen möchten, durch {<code>variable_name</code>}ersetzt haben.
</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc55d69d84a08e668/6a1707d8a2929903acd00fb8/ec7e1ac0cd8b470df13e60940162b56778acb386-315x234.png" alt="" /><p>Anhand der von LLM bereitgestellten Informationen können wir entscheiden, welche Vorlage wir verwenden und welche Werte wir ersetzen.</p><p><code>fill_template_with_analysis</code> empfängt die Parameter für ein einzelnes Panel, einschließlich der JSON-Vorlage der Visualisierung, eines Titels, eines Feldes und der Koordinaten der Visualisierung im Raster.</p><p>Anschließend werden die Werte der Vorlage ersetzt und die endgültige JSON-Visualisierung zurückgegeben.</p>def fill_template_with_analysis(
    template: Dict[str, Any],
    visualization: Visualization,
    grid_data: Dict[str, Any],
):
    template_str = json.dumps(template)
    replacements = {
	 "{visualization_id}": str(uuid.uuid4()),
        "{title}": visualization.title,
        "{x}": grid_data["x"],
        "{y}": grid_data["y"],
    }

    if visualization.field:
        replacements["{field}"] = visualization.field

    for placeholder, value in replacements.items():
        template_str = template_str.replace(placeholder, str(value))

    return json.loads(template_str)<p>Um es einfach zu halten, verwenden wir statische Koordinaten, die wir den vom LLM erstellten Panels zuweisen, und erzeugen so ein 2x2-Raster-Dashboard wie in der obigen Abbildung dargestellt.</p># Filling templates fields
panels = []    
grid_data = [
    {"x": 0, "y": 0},
    {"x": 12, "y": 0},
    {"x": 0, "y": 12},
    {"x": 12, "y": 12},
]


i = 0

for vis in dashboard_values.visualizations:
    for vis_type in vis.type:
        template = templates.get(vis_type, templates.get("bar", {}))
        filled_panel = fill_template_with_analysis(template, vis, grid_data[i])
        panels.append(filled_panel)
        i += 1<p>Je nach dem vom LLM festgelegten Visualisierungstyp wählen wir eine JSON-Dateivorlage aus und ersetzen die relevanten Informationen durch <code>fill_template_with_analysis</code> . Anschließend fügen wir das neue Panel einem Array hinzu, das wir später zum Erstellen des Dashboards verwenden.</p><p>Sobald das Dashboard fertig ist, verwenden wir die <a href="https://www.elastic.co/docs/api/doc/kibana/operation/operation-post-dashboards-dashboard-id">„Create a dashboard“-API</a> , um die neue JSON-Datei an Kibana zu übertragen und das Dashboard zu generieren:
</p>try:
    dashboard_id = str(uuid.uuid4())

    # post request to create the dashboard endpoint
    url = f"{os.getenv('KIBANA_URL')}/api/dashboards/dashboard/{dashboard_id}"

    dashboard_config = {
        "attributes": {
            "title": dashboard_values.title,
            "description": "Generated by AI",
            "timeRestore": True,
            "panels": panels,  # Visualizations with the values generated by the LLM
            "timeFrom": "now-7d/d",
            "timeTo": "now",
        },
    }

    headers = {
        "Content-Type": "application/json",
        "kbn-xsrf": "true",
        "Authorization": f"ApiKey {os.getenv('ELASTICSEARCH_API_KEY')}",
    }

    requests.post(
        url,
        headers=headers,
        json=dashboard_config,
    )

    # Url to the generated dashboard
    dashboard_url = f"{os.getenv('KIBANA_URL')}/app/dashboards#/view/{dashboard_id}"

    print("Dashboard URL: ", dashboard_url)
    print("Dashboard ID: ", dashboard_id)

except Exception as e:
    print(f"Failed to create dashboard: {str(e)}")<p>Um das Skript auszuführen und das Dashboard zu generieren, führen Sie folgenden Befehl in der Konsole aus:</p>python &lt;file_name&gt;.py<p>Das Endergebnis wird folgendermaßen aussehen:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5ceffed004153a4f/6a1707d9a929cf9147ae0901/e909afbf0e47d9a6e0f7bd07dfb2efcfa5cf06ac-921x715.png" alt="" /><h2>Fazit</h2><p>LLMs zeigen ihre ausgeprägten visuellen Fähigkeiten bei der Umwandlung von Text in Code oder der Umwandlung von Bildern in Code. Die Dashboards-API ermöglicht es auch, JSON-Dateien in Dashboards umzuwandeln, und mit einem LLM und etwas Code können wir Bilder in ein Kibana-Dashboard umwandeln.</p><p>Der nächste Schritt besteht darin, die Flexibilität der Dashboard-Visualisierungen durch die Verwendung unterschiedlicher Rastereinstellungen, Dashboard-Größen und -Positionen zu verbessern. Darüber hinaus wäre die Unterstützung komplexerer Visualisierungen und Visualisierungstypen eine sinnvolle Ergänzung dieser Anwendung.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/ai-powered-dashboards</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/ai-powered-dashboards</guid>
    <category><![CDATA[Kibana]]></category>
    <category><![CDATA[KI]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo,Tomás Murúa]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt41727cbee6155a68/6a1707dbb0367dd2fd72bc86/eb60ceb2fbc3941745b21ae3357cbb6ea8fab18c-1443x811.png" length="0" type="image/png"/>
    <pubDate>Wed, 16 Jul 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elasticsearch in JavaScript richtig anwenden, Teil II]]></title>
    <description><![CDATA[Erfahren Sie mehr über Best Practices für die Produktion und darüber, wie Sie den Elasticsearch Node.js-Client in serverlosen Umgebungen ausführen können, um Programmierfehler zu vermeiden. ]]></description>
    <content:encoded><![CDATA[<p>Dies ist der zweite Teil unserer Elasticsearch-in-JavaScript-Serie. Im<a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i"> ersten Teil haben wir gelernt,</a> wie wir unsere Umgebung richtig einrichten, den Node.js-Client konfigurieren, Daten indizieren und suchen. Im zweiten Teil lernen wir, wie man Best Practices für die Produktion implementiert und den Elasticsearch <a href="http://node.js">Node.js</a> -Client in serverlosen Umgebungen ausführt.</p><p>Wir werden Folgendes überprüfen:</p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#production-best-practices">Best Practices für die Produktion</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#error-handling">Fehlerbehandlung</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#testing">Tests</a></p></li></ul></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#serverless-environments">Serverlose Umgebungen</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#running-the-client-on-elastic-serverless">Den Client auf Elastic Serverless ausführen</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#running-the-client-on-function-as-a-service-environment">Ausführen des Clients in einer Function-as-a-Service-Umgebung</a></p></li></ul></li></ul><p><em>Den Quellcode mit den Beispielen finden Sie </em><a href="https://github.com/Delacrobix/JS-client-best-practices_article"><em><strong>hier</strong></em></a><em><strong>.</strong></em></p><h2>Best Practices für die Produktion</h2><h3>Fehlerbehandlung in Elasticsearch</h3><p>Ein nützliches Feature des Elasticsearch-Clients in Node.js ist, dass er Objekte für die möglichen Fehler in Elasticsearch bereitstellt, sodass man diese auf verschiedene Weise validieren und behandeln kann.</p><p>Um <a href="https://www.elastic.co/docs/reference/elasticsearch/clients/javascript/connecting#client-error-handling">sie alle zu sehen</a>, führen Sie folgenden Befehl aus: </p>const { errors } = require('@elastic/elasticsearch')
console.log(errors)<p>Kehren wir zum Suchbeispiel zurück und behandeln wir einige der möglichen Fehler:</p>app.get("/search/lexic", async (req, res) =&gt; {
 ....
  } catch (error) {
    if (error instanceof errors.ResponseError) {
      let errorMessage =
        "Response error!, query malformed or server down, contact the administrator!";

      if (error.body.error.type === "parsing_exception") {
        errorMessage = "Query malformed, make sure mappings are set correctly";
      }

      res.status(error.meta.statusCode).json({
        erroStatus: error.meta.statusCode,
        success: false,
        results: null,
        error: errorMessage,
      });
    }

    res.status(500).json({
      success: false,
      results: null,
      error: error.message,
    });
  }
});<p><code>ResponseError</code> Dies tritt insbesondere dann auf, wenn die Antwort <code>4xx</code> oder <code>5xx</code> lautet, was bedeutet, dass die Anfrage fehlerhaft ist oder der Server nicht verfügbar ist.</p><p>Wir können diese Art von Fehler testen, indem wir fehlerhafte Abfragen generieren, beispielsweise den Versuch, <strong>eine Termabfrage für ein Textfeld durchzuführen:</strong></p><p>Standardfehler:</p> {
    "success": false,
    "results": null,
    "error": "parsing_exception\n\tRoot causes:\n\t\tparsing_exception: [terms] query does not support [visit_details]"
}<p>Benutzerdefinierter Fehler: </p>{
    "erroStatus": 400,
    "success": false,
    "results": null,
    "error": "Response error!, query malformed or server down; contact the administrator!"
}<p>Wir können auch jeden Fehlertyp auf eine bestimmte Weise erfassen und behandeln. Zum Beispiel können wir in einem <code>TimeoutError</code> eine Wiederholungslogik hinzufügen.</p>app.get("/search/semantic", async (req, res) =&gt; {
    try {
  ...
  } catch (error) {
    if (error instanceof errors.TimeoutError) {


     // Retry logic...

      res.status(error.meta.statusCode).json({
        erroStatus: error.meta.statusCode,
        success: false,
        results: null,
        error:
          "The request took more than 10s after 3 retries. Try again later.",
      });
    }
  }
});<h3>Tests</h3><p>Tests sind entscheidend, um die Stabilität der App zu gewährleisten. Um den Code auf eine von Elasticsearch isolierte Weise zu testen, können wir die Bibliothek <a href="https://github.com/elastic/elasticsearch-js-mock">elasticsearch-js-mock</a> beim Erstellen unseres Clusters verwenden.</p><p>Mithilfe dieser Bibliothek können wir einen Client instanziieren, der dem realen Client sehr ähnlich ist, aber auf unsere Konfiguration reagiert, indem wir lediglich die HTTP-Schicht des Clients durch eine simulierte Schicht ersetzen, während der Rest mit dem Original übereinstimmt.</p><p>Wir werden die Mock-Bibliothek und <a href="https://github.com/avajs/ava">AVA</a> für automatisierte Tests installieren.</p><p><code>npm install @elastic/elasticsearch-mock</code></p><p><code>npm install --save-dev ava</code></p><p>Wir werden die Datei <code>package.json</code> so konfigurieren, dass die Tests ausgeführt werden. Stellen Sie sicher, dass es so aussieht:</p>"type": "module",
	"scripts": {
		"test": "ava"
	},
	"devDependencies": {
		"ava": "^5.0.0"
	}<p>Erstellen wir nun eine <code>test.js</code> -Datei und installieren wir unseren Mock-Client:</p>const { Client } = require('@elastic/elasticsearch')
const Mock = require('@elastic/elasticsearch-mock')

const mock = new Mock()
const client = new Client({
  node: 'http://localhost:9200',
  Connection: mock.getConnection()
})<p>Fügen Sie nun ein Mock-Objekt für die semantische Suche hinzu:</p>function createSemanticSearchMock(query, indexName) {
  mock.add(
    {
      method: "POST",
      path: `/${indexName}/_search`,
      body: {
        query: {
          semantic: {
            field: "semantic_field",
            query: query,
          },
        },
      },
    },
    () =&gt; {
      return {
        hits: {
          total: { value: 2, relation: "eq" },
          hits: [
            {
              _id: "1",
              _score: 0.9,
              _source: {
                owner_name: "Alice Johnson",
                pet_name: "Buddy",
                species: "Dog",
                breed: "Golden Retriever",
                vaccination_history: ["Rabies", "Parvovirus", "Distemper"],
                visit_details:
                  "Annual check-up and nail trimming. Healthy and active.",
              },
            },
            {
              _id: "2",
              _score: 0.7,
              _source: {
                owner_name: "Daniel Kim",
                pet_name: "Mochi",
                species: "Rabbit",
                breed: "Mixed",
                vaccination_history: [],
                visit_details:
                  "Nail trimming and general health check. No issues.",
              },
            },
          ],
        },
      };
    }
  );
}<p>Wir können nun einen Test für unseren Code erstellen, um sicherzustellen, dass der Elasticsearch-Teil immer die gleichen Ergebnisse liefert:</p>import test from 'ava';

test("performSemanticSearch must return formatted results correctly", async (t) =&gt; {
  const indexName = "vet-visits";
  const query = "Which pets had nail trimming?";

  createSemanticSearchMock(query, indexName);

  async function performSemanticSearch(esClient, q, indexName = "vet-visits") {
    try {
      const result = await esClient.search({
        index: indexName,
        body: {
          query: {
            semantic: {
              field: "semantic_field",
              query: q,
            },
          },
        },
      });

      return {
        success: true,
        results: result.hits.hits,
      };
    } catch (error) {
      if (error instanceof errors.TimeoutError) {
        return {
          success: false,
          results: null,
          error: error.body.error.reason,
        };
      }

      return {
        success: false,
        results: null,
        error: error.message,
      };
    }
  }

  const result = await performSemanticSearch(esClient, query, indexName);

  t.true(result.success, "The search must be successful");
  t.true(Array.isArray(result.results), "The results must be an array");

  if (result.results.length &gt; 0) {
    t.true(
      "_source" in result.results[0],
      "Each result must have a _source property"
    );
    t.true(
      "pet_name" in result.results[0]._source,
      "Results must include the pet_name field"
    );
    t.true(
      "visit_details" in result.results[0]._source,
      "Results must include the visit_details field"
    );
  }
});<p>Lasst uns die Tests durchführen.</p><p><code>npm run test</code></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt36304e286146f362/6a170559d7c02237b2de638f/42feae845ae8eae03c37ad7ad114e8db35984812-1186x302.png" alt="" /><p>Erledigt! Ab sofort können wir unsere App testen und uns dabei zu 100 % auf den Code konzentrieren, ohne uns von externen Faktoren beeinflussen zu lassen.</p><h2>Serverlose Umgebungen</h2><h3>Wie Sie den Client auf Elastic Serverless ausführen</h3><p>Wir haben die Ausführung von Elasticsearch in der Cloud oder lokal behandelt; der Node.js-Client unterstützt jedoch auch Verbindungen zu <a href="https://www.elastic.co/guide/en/serverless/current/intro.html">Elastic Cloud Serverless</a>.</p><p>Mit Elastic Cloud Serverless können Sie ein Projekt erstellen, bei dem Sie sich keine Gedanken um die Infrastruktur machen müssen, da Elastic dies intern übernimmt. Sie müssen sich nur um die Daten kümmern, die Sie indizieren möchten, und darum, wie lange Sie darauf zugreifen möchten.</p><p>Aus Nutzersicht entkoppelt Serverless Rechenleistung von Speicher und bietet automatische Skalierungsfunktionen sowohl für <a href="https://www.elastic.co/search-labs/blog/elasticsearch-serverless-tier-autoscaling">die Suche</a> als auch <a href="https://www.elastic.co/search-labs/blog/elasticsearch-ingest-autoscaling">für die Indizierung</a>. Dadurch können Sie nur die Ressourcen anbauen, die Sie tatsächlich benötigen.</p><p>Der Client nimmt folgende Anpassungen vor, um eine Verbindung zu Serverless herzustellen:</p><ul><li><p>Deaktiviert das Sniffing und ignoriert alle Sniffing-bezogenen Optionen.</p></li><li><p>Ignoriert alle in der Konfiguration übergebenen Knoten außer dem ersten und ignoriert jegliche Knotenfilter- und Auswahloptionen.</p></li><li><p>Aktiviert Komprimierung und `TLSv1_2_method` (genau wie bei der Konfiguration für Elastic Cloud).</p></li><li><p>Fügt allen Anfragen einen HTTP-Header `elastic-api-version` hinzu</p></li><li><p>Verwendet standardmäßig `CloudConnectionPool` anstelle von `WeightedConnectionPool`.</p></li><li><p>Deaktiviert die vom Anbieter bereitgestellten `content-type`- und `accept`-Header zugunsten der Standard-MIME-Typen.</p></li></ul><p>Um Ihr serverloses Projekt zu verbinden, müssen Sie den Parameter serverMode: serverless verwenden.</p>const { Client } = require('@elastic/elasticsearch')
const client = new Client({
  node: 'ELASTICSEARCH_ENDPOINT',
  auth: { apiKey: 'ELASTICSEARCH_API_KEY' },
  serverMode: "serverless",
});<h3>Wie Sie den Client in einer Function-as-a-Service-Umgebung ausführen</h3><p>In diesem Beispiel haben wir einen Node.js-Server verwendet, aber Sie können auch über eine Function-as-a-Service-Umgebung mit Funktionen wie AWS Lambda, GCP Run usw. eine Verbindung herstellen.</p>'use strict'

const { Client } = require('@elastic/elasticsearch')

const client = new Client({
  // client initialisation
})

exports.handler = async function (event, context) {
  // use the client
}<p>Ein weiteres Beispiel ist die Anbindung an Dienste wie Vercel, das ebenfalls serverlos ist. Sie können sich dieses <a href="https://github.com/elastic/elasticsearch-js/blob/main/docs/examples/proxy/README.md">vollständige Beispiel</a> ansehen, wie das geht, aber der relevanteste Teil des <a href="https://github.com/elastic/elasticsearch-js/blob/main/docs/examples/proxy/api/search.js">Suchendpunkts</a> sieht folgendermaßen aus:</p>const response = await client.search(
  {
    index: INDEX,
    // You could directly send from the browser
    // the Elasticsearch's query DSL, but it will
    // expose you to the risk that a malicious user
    // could overload your cluster by crafting
    // expensive queries.
    query: {
      match: { field: req.body.text },
    },
  },
  {
    headers: {
      Authorization: `ApiKey ${token}`,
    },
  }
);<p>Dieser Endpunkt befindet sich im Ordner /api und wird serverseitig ausgeführt, sodass der Client nur über den Parameter „text“ verfügt, der dem Suchbegriff entspricht.</p><p>Die Konsequenz aus der Verwendung von Function-as-a-Service ist, dass Funktionen, im Gegensatz zu einem Server, der rund um die Uhr läuft, nur die Maschine starten, auf der die Funktion ausgeführt wird, und dass die Maschine nach Beendigung der Funktion in den Ruhemodus wechselt, um weniger Ressourcen zu verbrauchen.</p><p>Diese Konfiguration kann praktisch sein, wenn die Anwendung nicht zu viele Anfragen erhält; andernfalls können die Kosten hoch sein. Sie müssen auch den <a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtime-environment.html">Lebenszyklus der Funktionen</a> und die Laufzeiten berücksichtigen (die in manchen Fällen nur Sekunden betragen können).</p><h2>Fazit</h2><p>In diesem Artikel haben wir gelernt, wie man mit Fehlern umgeht, was in Produktionsumgebungen von entscheidender Bedeutung ist. Wir haben auch das Testen unserer Anwendung unter Verwendung von Mocking für den Elasticsearch-Dienst behandelt. Dies ermöglicht zuverlässige Tests unabhängig vom Zustand des Clusters und erlaubt es uns, uns auf unseren Code zu konzentrieren.</p><p>Zum Schluss demonstrierten wir, wie man einen vollständig serverlosen Stack einrichtet, indem man sowohl Elastic Cloud Serverless als auch eine Vercel-Anwendung bereitstellt.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii</guid>
    <category><![CDATA[Javascript]]></category>
    <category><![CDATA[Grundlagen]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc58be329ffebcd60/6a17043e47d49c0bc62d88ab/70fb0ff949f6db9ac9b8a28ecb4329ab915ebf46-720x420.png" length="0" type="image/png"/>
    <pubDate>Mon, 19 May 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elasticsearch in JavaScript richtig anwenden, Teil 1]]></title>
    <description><![CDATA[Hier wird erklärt, wie man ein produktionsreifes Elasticsearch-Backend in JavaScript erstellt.  

Erkunden Sie, wie Sie Elasticsearch mit JavaScript verwenden, um einen Server mit verschiedenen Such-Endpunkten zu erstellen, der Elasticsearch-Dokumente gemäß den Best Practices für Client/Server abfragt.]]></description>
    <content:encoded><![CDATA[<p>Dies ist der erste Artikel einer Reihe, die die Verwendung von Elasticsearch mit JavaScript behandelt. In dieser Reihe lernen Sie die Grundlagen der Verwendung von Elasticsearch in einer JavaScript-Umgebung kennen und erhalten einen Überblick über die wichtigsten Funktionen und Best Practices zur Erstellung einer Such-App. Am Ende dieses Kurses werden Sie alles wissen, was Sie benötigen, um Elasticsearch mit JavaScript auszuführen.</p><p>In diesem ersten Teil werden wir Folgendes besprechen:</p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#environment">Umfeld</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#frontend,-backend,-or-serverless?">Frontend, Backend oder serverlos?</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#connecting-the-client">Verbindung des Clients</a></p></li></ul></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#indexing-documents">Dokumente indizieren</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#elasticsearch-client">Elasticsearch-Client</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#semantic-mappings">Semantische Zuordnungen</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#bulk-helper">Schüttguthelfer</a></p></li></ul></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#searching-data">Datensuche</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#lexical-query-(/search/lexic?q=%3Cquery-term%3E)">Lexikalische Abfrage</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#semantic-query-(/search/semantic?q=%3Cquery-term%3E)">Semantische Anfrage</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#hybrid-query-(/search/hybrid?q=%3Cquery-term%3E)">Hybridabfrage</a></p></li></ul></li></ul><p><em>Den Quellcode mit den Beispielen finden Sie </em><a href="https://github.com/Delacrobix/JS-client-best-practices_article"><em><strong>hier</strong></em></a><em><strong>.</strong></em></p><h3>Was ist der Elasticsearch Node.js-Client?</h3><p>Der <a href="https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html">Elasticsearch Node.js-Client</a> ist eine JavaScript-Bibliothek, die die HTTP-REST-Aufrufe der Elasticsearch-API in JavaScript umsetzt. Dadurch wird die Handhabung einfacher und es stehen Hilfsfunktionen zur Verfügung, die Aufgaben wie das Indizieren von Dokumenten in Stapeln vereinfachen.</p><h2>Umfeld</h2><h3>Frontend, Backend oder serverlos?</h3><p>Um unsere Such-App mit dem JavaScript-Client zu erstellen, benötigen wir mindestens zwei Komponenten: einen Elasticsearch-Cluster und eine JavaScript-Laufzeitumgebung zum Ausführen des Clients.</p><p>Der JavaScript-Client unterstützt alle Elasticsearch-Lösungen (Cloud, On-Premise und Serverless), und es gibt keine wesentlichen Unterschiede zwischen ihnen, da der Client alle Varianten intern handhabt, sodass Sie sich keine Gedanken darüber machen müssen, welche Sie verwenden sollen.</p><p>Die JavaScript-Laufzeitumgebung muss jedoch vom <strong>Server</strong> und <strong>nicht direkt vom Browser ausgeführt werden.</strong></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd3ec469c83e3a71a/6a17e3d5445de91da44d00b6/92ce6cfd923c8008fa44f617a58193642d9d5879-661x410.png" alt="Elasticsearch in einer JavaScript-Umgebung." /><p>Dies liegt daran, dass beim Aufruf von Elasticsearch über den Browser sensible Informationen wie der Cluster-API-Schlüssel, der Host oder die Abfrage selbst abgerufen werden können. Elasticsearch empfiehlt, <strong>den Cluster niemals direkt dem Internet auszusetzen </strong>und stattdessen eine Zwischenschicht zu verwenden, die all diese Informationen abstrahiert, sodass der Benutzer nur die Parameter sehen kann. <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/es-security-principles.html#security-protect-cluster-traffic">Hier</a> können Sie mehr zu diesem Thema lesen.</p><p>Wir schlagen vor, folgendes Schema zu verwenden:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4d7f215f2e70230a/6a17e3d6fbc5f83de6491a13/a08769f08ec73fe57bf2e961cfdfbb1cdd57919d-972x429.png" alt="Den Elasticsearch Node.js-Client einrichten." /><p>In diesem Fall sendet der Client lediglich die Suchbegriffe und einen Authentifizierungsschlüssel an Ihren Server, während Ihr Server die vollständige Kontrolle über die Abfrage und die Kommunikation mit Elasticsearch behält.</p><h3>Verbindung des Clients</h3><p>Erstellen Sie zunächst einen API-Schlüssel gemäß <a href="https://www.elastic.co/search-labs/tutorials/install-elasticsearch/elastic-cloud">diesen Schritten</a>.</p><p>Im Anschluss an das vorherige Beispiel erstellen wir einen einfachen Express-Server und stellen über einen Client von einem Node.JS-Server aus eine Verbindung zu diesem her.</p><p>Wir werden das Projekt mit NPM initialisieren und den Elasticsearch-Client sowie <a href="https://expressjs.com/">Express installieren.</a> Letzteres ist eine Bibliothek zum Starten von Servern in Node.js. Mit Express können wir über HTTP mit unserem Backend interagieren.</p><p>Lasst uns das Projekt initialisieren:</p><p><code>npm init -y</code></p><p>Abhängigkeiten installieren:</p><p><code>npm install @elastic/elasticsearch express split2 dotenv</code></p><p>Ich erkläre es Ihnen genauer:</p><ul><li><p><a href="https://www.npmjs.com/package/@elastic/elasticsearch"><em><strong>@elastic/elasticsearch</strong></em></a>: Es handelt sich um den offiziellen Node.js-Client.</p></li><li><p><a href="https://www.npmjs.com/package/express"><em><strong>Express</strong></em></a>: Damit können wir einen schlanken Node.js-Server starten, um Elasticsearch bereitzustellen.</p></li><li><p><a href="https://www.npmjs.com/package/split2"><em><strong>split2</strong></em></a>: Teilt Textzeilen in einen Datenstrom auf. Nützlich, um unsere ndjson-Dateien zeilenweise zu verarbeiten.</p></li><li><p><a href="https://www.npmjs.com/package/dotenv"><em><strong>dotenv</strong></em></a>: Ermöglicht die Verwaltung von Umgebungsvariablen mithilfe einer .env-Datei. Datei</p></li></ul><p>Erstelle eine .env-Datei Fügen Sie in der Datei im Stammverzeichnis des Projekts die folgenden Zeilen hinzu:</p>ELASTICSEARCH_ENDPOINT="Your Elasticsearch endpoint"
ELASTICSEARCH_API_KEY="Your Elasticssearch API"<p>Auf diese Weise können wir diese Variablen mithilfe des Pakets <code>dotenv</code> importieren.</p><p>Erstelle eine <code>server.js</code> -Datei:</p>const express = require("express");
const bodyParser = require("body-parser");
const { Client } = require("@elastic/elasticsearch");
 
require("dotenv").config(); //environment variables setup

const ELASTICSEARCH_ENDPOINT = process.env.ELASTICSEARCH_ENDPOINT;
const ELASTICSEARCH_API_KEY = process.env.ELASTICSEARCH_API_KEY;
const PORT = 3000;


const app = express();

app.listen(PORT, () =&gt; {
  console.log("Server running on port", PORT);
});
app.use(bodyParser.json());


let esClient = new Client({
  node: ELASTICSEARCH_ENDPOINT,
  auth: { apiKey: ELASTICSEARCH_API_KEY },  
});

app.get("/ping", async (req, res) =&gt; {
  try {
    const result = await esClient.info();

    res.status(200).json({
      success: true,
      clusterInfo: result,
    });
  } catch (error) {
    console.error("Error getting Elasticsearch info:", error);

    res.status(500).json({
      success: false,
      clusterInfo: null,
      error: error.message,
    });
  }
});<p>Dieser Code richtet einen einfachen Express.js-Server ein, der auf Port 3000 lauscht und sich über einen API-Schlüssel zur Authentifizierung mit einem Elasticsearch-Cluster verbindet. Es beinhaltet einen /ping-Endpunkt, der bei einem GET-Aufruf mithilfe der <code>.info()</code> -Methode des Elasticsearch-Clients grundlegende Informationen vom Elasticsearch-Cluster abfragt. </p><p>Bei erfolgreicher Abfrage werden die Clusterinformationen im JSON-Format zurückgegeben; andernfalls wird eine Fehlermeldung zurückgegeben. Der Server verwendet außerdem die Middleware body-parser, um JSON-Anfragetexte zu verarbeiten.</p><p>Führen Sie die Datei aus, um den Server zu starten:</p><p><code>node server.js</code></p><p>Die Antwort sollte folgendermaßen aussehen:</p>Server running on port 3000<p>Und nun konsultieren wir den Endpunkt <code>/ping</code> , um den Status unseres Elasticsearch-Clusters zu überprüfen.</p>curl http://localhost:3000/ping
{
    "success": true,
    "clusterInfo": {
        "name": "instance-0000000000",
        "cluster_name": "61b7e19eec204d59855f5e019acd2689",
        "cluster_uuid": "BIfvfLM0RJWRK_bDCY5ldg",
        "version": {
            "number": "9.0.0",
            "build_flavor": "default",
            "build_type": "docker",
            "build_hash": "112859b85d50de2a7e63f73c8fc70b99eea24291",
            "build_date": "2025-04-08T15:13:46.049795831Z",
            "build_snapshot": false,
            "lucene_version": "10.1.0",
            "minimum_wire_compatibility_version": "8.18.0",
            "minimum_index_compatibility_version": "8.0.0"
        },
        "tagline": "You Know, for Search"
    }
}<h2>Dokumente indizieren</h2><p>Sobald die Verbindung hergestellt ist, können wir Dokumente mithilfe von Zuordnungen wie <a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text">semantic_text</a> für die semantische Suche und text für Volltextabfragen indizieren. Mit diesen beiden Feldtypen können wir auch <a href="https://www.elastic.co/what-is/hybrid-search">eine hybride Suche</a> durchführen.</p><p>Wir erstellen eine neue <code>load.js</code> -Datei, um die Zuordnungen zu generieren und die Dokumente hochzuladen.</p><h3>Elasticsearch-Client</h3><p>Zuerst müssen wir den Client instanziieren und authentifizieren:</p>const { Client } = require("@elastic/elasticsearch");

const ELASTICSEARCH_ENDPOINT = "cluster/project_endpoint";
const ELASTICSEARCH_API_KEY = "apiKey";

const esClient = new Client({
  node: ELASTICSEARCH_ENDPOINT,
  auth: { apiKey: ELASTICSEARCH_API_KEY },
});<h3>Semantische Zuordnungen</h3><p>Wir erstellen einen Index mit Daten über Tierkliniken. Wir speichern die Informationen über den Besitzer, das Haustier und die Details des Besuchs.</p><p>Die Daten, in denen wir eine Volltextsuche durchführen möchten, wie zum Beispiel Namen und Beschreibungen, werden als Text gespeichert. Die Daten aus Kategorien, wie zum Beispiel die Tierart oder -rasse, werden als Schlüsselwörter gespeichert.</p><p>Zusätzlich kopieren wir die Werte aller Felder in ein semantisches Textfeld, um auch mit diesen Informationen eine semantische Suche durchführen zu können.</p>const INDEX_NAME = "vet-visits";

const createMappings = async (indexName, mapping) =&gt; {
  try {
    const body = await esClient.indices.create({
      index: indexName,
      body: {
        mappings: mapping,
      },
    });

    console.log("Index created successfully:", body);
  } catch (error) {
    console.error("Error creating mapping:", error);
  }
};

await createMappings(INDEX_NAME, {
  properties: {
    owner_name: {
      type: "text",
      copy_to: "semantic_field",
    },
    pet_name: {
      type: "text",
      copy_to: "semantic_field",
    },
    species: {
      type: "keyword",
      copy_to: "semantic_field",
    },
    breed: {
      type: "keyword",
      copy_to: "semantic_field",
    },
    vaccination_history: {
      type: "keyword",
      copy_to: "semantic_field",
    },
    visit_details: {
      type: "text",
      copy_to: "semantic_field",
    },
    semantic_field: {
      type: "semantic_text",
    },
  },
});<h3>Schüttguthelfer</h3><p>Ein weiterer Vorteil des Clients besteht darin, dass wir den <a href="https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/client-helpers.html#bulk-helper">Bulk-Helper</a> verwenden können, um die Indizierung in Batches durchzuführen. Der Bulk-Helper ermöglicht es uns, Dinge wie Parallelverarbeitung, Wiederholungsversuche und die Behandlung jedes Dokuments, das die Funktion durchläuft und erfolgreich oder fehlschlägt, einfach zu handhaben.</p><p>Ein attraktives Merkmal dieses Helfers ist, dass man mit Datenströmen arbeiten kann. Mit dieser Funktion können Sie eine Datei zeilenweise senden, anstatt die gesamte Datei im Speicher zu speichern und sie dann auf einmal an Elasticsearch zu senden.</p><p>Um die Daten in Elasticsearch hochzuladen, erstellen Sie eine Datei namens data.ndjson im Stammverzeichnis des Projekts und fügen Sie die folgenden Informationen hinzu (alternativ können Sie die Datei mit dem Datensatz <a href="https://github.com/Delacrobix/JS-client-best-practices_article/blob/main/data.ndjson">hier</a> herunterladen):</p>{"owner_name":"Alice Johnson","pet_name":"Buddy","species":"Dog","breed":"Golden Retriever","vaccination_history":["Rabies","Parvovirus","Distemper"],"visit_details":"Annual check-up and nail trimming. Healthy and active."}
{"owner_name":"Marco Rivera","pet_name":"Milo","species":"Cat","breed":"Siamese","vaccination_history":["Rabies","Feline Leukemia"],"visit_details":"Slight eye irritation, prescribed eye drops."}
{"owner_name":"Sandra Lee","pet_name":"Pickles","species":"Guinea Pig","breed":"Mixed","vaccination_history":[],"visit_details":"Loss of appetite, recommended dietary changes."}
{"owner_name":"Jake Thompson","pet_name":"Luna","species":"Dog","breed":"Labrador Mix","vaccination_history":["Rabies","Bordetella"],"visit_details":"Mild ear infection, cleaning and antibiotics given."}
{"owner_name":"Emily Chen","pet_name":"Ziggy","species":"Cat","breed":"Mixed","vaccination_history":["Rabies","Feline Calicivirus"],"visit_details":"Vaccination update and routine physical."}
{"owner_name":"Tomás Herrera","pet_name":"Rex","species":"Dog","breed":"German Shepherd","vaccination_history":["Rabies","Parvovirus","Leptospirosis"],"visit_details":"Follow-up for previous leg strain, improving well."}
{"owner_name":"Nina Park","pet_name":"Coco","species":"Ferret","breed":"Mixed","vaccination_history":["Rabies"],"visit_details":"Slight weight loss; advised new diet."}
{"owner_name":"Leo Martínez","pet_name":"Simba","species":"Cat","breed":"Maine Coon","vaccination_history":["Rabies","Feline Panleukopenia"],"visit_details":"Dental cleaning. Minor tartar buildup removed."}
{"owner_name":"Rachel Green","pet_name":"Rocky","species":"Dog","breed":"Bulldog Mix","vaccination_history":["Rabies","Parvovirus"],"visit_details":"Skin rash, antihistamines prescribed."}
{"owner_name":"Daniel Kim","pet_name":"Mochi","species":"Rabbit","breed":"Mixed","vaccination_history":[],"visit_details":"Nail trimming and general health check. No issues."}<p>Wir verwenden split2, um die Dateizeilen zu streamen, während der Bulk-Helper sie an Elasticsearch sendet.</p>const { createReadStream } = require("fs");
const split = require("split2");
 
const indexData = async (filePath, indexName) =&gt; {
  try {
    console.log(`Indexing data from ${filePath} into ${indexName}...`);

    const result = await esClient.helpers.bulk({
      datasource: createReadStream(filePath).pipe(split()),

      onDocument: () =&gt; {
        return {
          index: { _index: indexName },
        };
      },
      onDrop(doc) {
        console.error("Error processing document:", doc);
      },
    });

    console.log("Bulk indexing successful elements:", result.items.length);
  } catch (error) {
    console.error("Error indexing data:", error);
    throw error;
  }
};

await indexData("./data.ndjson", INDEX_NAME);<p>Der obige Code liest eine .ndjson-Datei. Mit der Methode <code>helpers.bulk</code> werden alle JSON-Objekte zeilenweise und in einem Massenindex in einen angegebenen Elasticsearch-Index indiziert. Es streamt die Datei mit <code>createReadStream</code> und <code>split2</code>, richtet Indexierungsmetadaten für jedes Dokument ein und protokolliert alle Dokumente, deren Verarbeitung fehlschlägt. Nach Abschluss des Vorgangs wird die Anzahl der erfolgreich indizierten Elemente protokolliert.</p><p>Alternativ zur <code>indexData</code> -Funktion können Sie die Datei auch direkt über die Benutzeroberfläche von Kibana hochladen und dabei die <a href="https://www.elastic.co/docs/manage-data/ingest/upload-data-files">Funktion „Datendateien hochladen“ verwenden.</a></p><p>Wir führen die Datei aus, um die Dokumente in unseren Elasticsearch-Cluster hochzuladen.</p><p><code>node load.js</code></p>Creating mappings for index vet-visits...
Index created successfully: { acknowledged: true, shards_acknowledged: true, index: 'vet-visits' }
Indexing data from ./data.ndjson into vet-visits...
Bulk indexing completed. Total documents: 10, Failed: 0<h2>Datensuche in Elasticsearch</h2><p>Wir kehren zu unserer <code>server.js</code> -Datei zurück und erstellen verschiedene Endpunkte, um lexikalische, semantische oder hybride Suchen durchzuführen.</p><p>Kurz gesagt, schließen sich diese Sucharten nicht gegenseitig aus, sondern hängen von der Art der Frage ab, die Sie beantworten möchten.</p><p>Abfragetyp</p><p>Anwendungsfall</p><p>Beispielaufgabe</p><p>Lexikalische Abfrage</p><p>Die in der Frage vorkommenden Wörter oder Wortstämme tauchen wahrscheinlich in den Indexdokumenten auf. Tokenische Ähnlichkeit zwischen Frage und Dokumenten.</p><p>Ich suche ein blaues Sport-T-Shirt.</p><p>Semantische Anfrage</p><p>Die in der Frage enthaltenen Wörter werden in den Dokumenten wahrscheinlich nicht vorkommen. Konzeptionelle Ähnlichkeit zwischen Frage und Dokumenten.</p><p>Ich suche Kleidung für kaltes Wetter.</p><p>Hybrid Search</p><p>Die Frage enthält lexikalische und/oder semantische Komponenten. Token- und semantische Ähnlichkeit zwischen Frage und Dokumenten.</p><p>Ich suche ein Kleid in Größe S für eine Strandhochzeit.</p><p>Die <em><strong>lexikalischen </strong></em>Bestandteile der Frage sind wahrscheinlich Teil von Titeln und Beschreibungen oder Kategorienamen, während die <em><strong>semantischen </strong></em>Bestandteile Konzepte sind, die mit diesen Bereichen in Zusammenhang stehen. <em><strong>Blau</strong></em> wird wahrscheinlich ein Kategoriename oder Teil einer Beschreibung sein, <em><strong>Strandhochzeit</strong></em> hingegen eher nicht, kann aber semantisch mit Leinenkleidung in Verbindung gebracht werden.</p><h3>Lexikalische Abfrage (/search/lexic?q=)&lt;query_term&gt;</h3><p>Die lexikalische Suche, auch Volltextsuche genannt, bedeutet die Suche auf der Grundlage der Ähnlichkeit von Wörtern; das heißt, nach einer Analyse werden die Dokumente zurückgegeben, die die gesuchten Wörter enthalten.</p><p><a href="https://www.elastic.co/demo-gallery/lexical-search">Hier</a> finden Sie unser praktisches Tutorial zur lexikalischen Suche.</p>app.get("/search/lexic", async (req, res) =&gt; {
  const { q } = req.query;

  const INDEX_NAME = "vet-visits";

  try {
    const result = await esClient.search({
      index: INDEX_NAME,
      size: 5,
      body: {
        query: {
          multi_match: {
            query: q,
            fields: ["owner_name", "pet_name", "visit_details"],
          },
        },
      },
    });

    res.status(200).json({
      success: true,
      results: result.hits.hits
    });
  } catch (error) {
    console.error("Error performing search:", error);

    res.status(500).json({
      success: false,
      results: null,
      error: error.message,
    });
  }
});<p>Wir testen mit: <em><strong>Krallenschneiden</strong></em></p>curl http://localhost:3000/search/lexic?q=nail%20trimming<p>Antwort:</p>{
    "success": true,
    "results": [
        {
            "_index": "vet-visits",
            "_id": "-RY6RJYBLe2GoFQ6-9n9",
            "_score": 2.7075968,
            "_source": {
                "pet_name": "Mochi",
                "owner_name": "Daniel Kim",
                "species": "Rabbit",
                "visit_details": "Nail trimming and general health check. No issues.",
                "breed": "Mixed",
                "vaccination_history": []
            }
        },
        {
            "_index": "vet-visits",
            "_id": "8BY6RJYBLe2GoFQ6-9n9",
            "_score": 2.560356,
            "_source": {
                "pet_name": "Buddy",
                "owner_name": "Alice Johnson",
                "species": "Dog",
                "visit_details": "Annual check-up and nail trimming. Healthy and active.",
                "breed": "Golden Retriever",
                "vaccination_history": [
                    "Rabies",
                    "Parvovirus",
                    "Distemper"
                ]
            }
        }
    ]
}<h3>Semantische Abfrage (/search/semantic?q=)&lt;query_term&gt;</h3><p>Die semantische Suche findet, anders als die lexikalische Suche, Ergebnisse, die der Bedeutung der Suchbegriffe ähneln, durch Vektorsuche.</p><p><a href="https://www.elastic.co/demo-gallery/semantic-search">Hier</a> finden Sie unser praktisches Tutorial zur semantischen Suche.</p>app.get("/search/semantic", async (req, res) =&gt; {
  const { q } = req.query;

  const INDEX_NAME = "vet-visits";

  try {
    const result = await esClient.search({
      index: INDEX_NAME,
      size: 5,
      body: {
        query: {
          semantic: {
            field: "semantic_field",
            query: q
          },
        },
      },
    });

    res.status(200).json({
      success: true,
      results: result.hits.hits,
    });
  } catch (error) {
    console.error("Error performing search:", error);

    res.status(500).json({
      success: false,
      results: null,
      error: error.message,
    });
  }
});<p>Wir testen mit: <em><strong>Wer hat sich eine Pediküre machen lassen?</strong></em></p>curl http://localhost:3000/search/semantic?q=Who%20got%20a%20pedicure?<p>Antwort:</p>{
    "success": true,
    "results": [
        {
            "_index": "vet-visits",
            "_id": "-RY6RJYBLe2GoFQ6-9n9",
            "_score": 4.861466,
            "_source": {
                "owner_name": "Daniel Kim",
                "pet_name": "Mochi",
                "species": "Rabbit",
                "breed": "Mixed",
                "vaccination_history": [],
                "visit_details": "Nail trimming and general health check. No issues."
            }
        },
        {
            "_index": "vet-visits",
            "_id": "8BY6RJYBLe2GoFQ6-9n9",
            "_score": 4.7152824,
            "_source": {
                "pet_name": "Buddy",
                "owner_name": "Alice Johnson",
                "species": "Dog",
                "visit_details": "Annual check-up and nail trimming. Healthy and active.",
                "breed": "Golden Retriever",
                "vaccination_history": [
                    "Rabies",
                    "Parvovirus",
                    "Distemper"
                ]
            }
        },
        {
            "_index": "vet-visits",
            "_id": "9RY6RJYBLe2GoFQ6-9n9",
            "_score": 1.6717153,
            "_source": {
                "pet_name": "Rex",
                "owner_name": "Tomás Herrera",
                "species": "Dog",
                "visit_details": "Follow-up for previous leg strain, improving well.",
                "breed": "German Shepherd",
                "vaccination_history": [
                    "Rabies",
                    "Parvovirus",
                    "Leptospirosis"
                ]
            }
        },
        {
            "_index": "vet-visits",
            "_id": "9xY6RJYBLe2GoFQ6-9n9",
            "_score": 1.5600781,
            "_source": {
                "pet_name": "Simba",
                "owner_name": "Leo Martínez",
                "species": "Cat",
                "visit_details": "Dental cleaning. Minor tartar buildup removed.",
                "breed": "Maine Coon",
                "vaccination_history": [
                    "Rabies",
                    "Feline Panleukopenia"
                ]
            }
        },
        {
            "_index": "vet-visits",
            "_id": "-BY6RJYBLe2GoFQ6-9n9",
            "_score": 1.2696637,
            "_source": {
                "pet_name": "Rocky",
                "owner_name": "Rachel Green",
                "species": "Dog",
                "visit_details": "Skin rash, antihistamines prescribed.",
                "breed": "Bulldog Mix",
                "vaccination_history": [
                    "Rabies",
                    "Parvovirus"
                ]
            }
        }
    ]
}<h3>Hybrid-Suchanfrage (/search/hybrid?q=)&lt;query_term&gt;</h3><p>Die Hybridsuche ermöglicht es uns, semantische und lexikalische Suche zu kombinieren und so das Beste aus beiden Welten zu erhalten: Sie erhalten die Präzision der Token-Suche zusammen mit der Bedeutungsnähe der semantischen Suche.</p>app.get("/search/hybrid", async (req, res) =&gt; {
  const { q } = req.query;

  const INDEX_NAME = "vet-visits";

  try {
    const result = await esClient.search({
      index: INDEX_NAME,
      body: {
        retriever: {
          rrf: {
            retrievers: [
              {
                standard: {
                  query: {
                    bool: {
                      must: {
                         multi_match: {
             query: q,
            fields: ["owner_name", "pet_name", "visit_details"],
          },
                      },
                    },
                  },
                },
              },
              {
                standard: {
                  query: {
                    bool: {
                      must: {
                        semantic: {
                          field: "semantic_field",
                          query: q,
                        },
                      },
                    },
                  },
                },
              },
            ],
          },
        },
        size: 5,
      },
    });

    res.status(200).json({
      success: true,
      results: result.hits.hits,
    });
  } catch (error) {
    console.error("Error performing search:", error);

    res.status(500).json({
      success: false,
      results: null,
      error: error.message,
    });
  }
});<p>Wir testen mit der Frage: „ <em><strong>Wer hat sich eine Pediküre oder eine Zahnbehandlung gegönnt?“</strong></em></p>curl http://localhost:3000/search/hybrid?q=who%20got%20a%20pedicure%20or%20dental%20treatment<p>Abwehr:</p>{
    "success": true,
    "results": [
        {
            "_index": "vet-visits",
            "_id": "9xY6RJYBLe2GoFQ6-9n9",
            "_score": 0.032522473,
            "_source": {
                "pet_name": "Simba",
                "owner_name": "Leo Martínez",
                "species": "Cat",
                "visit_details": "Dental cleaning. Minor tartar buildup removed.",
                "breed": "Maine Coon",
                "vaccination_history": [
                    "Rabies",
                    "Feline Panleukopenia"
                ]
            }
        },
        {
            "_index": "vet-visits",
            "_id": "-RY6RJYBLe2GoFQ6-9n9",
            "_score": 0.016393442,
            "_source": {
                "pet_name": "Mochi",
                "owner_name": "Daniel Kim",
                "species": "Rabbit",
                "visit_details": "Nail trimming and general health check. No issues.",
                "breed": "Mixed",
                "vaccination_history": []
            }
        },
        {
            "_index": "vet-visits",
            "_id": "8BY6RJYBLe2GoFQ6-9n9",
            "_score": 0.015873017,
            "_source": {
                "pet_name": "Buddy",
                "owner_name": "Alice Johnson",
                "species": "Dog",
                "visit_details": "Annual check-up and nail trimming. Healthy and active.",
                "breed": "Golden Retriever",
                "vaccination_history": [
                    "Rabies",
                    "Parvovirus",
                    "Distemper"
                ]
            }
        },
        {
            "_index": "vet-visits",
            "_id": "9RY6RJYBLe2GoFQ6-9n9",
            "_score": 0.015625,
            "_source": {
                "pet_name": "Rex",
                "owner_name": "Tomás Herrera",
                "species": "Dog",
                "visit_details": "Follow-up for previous leg strain, improving well.",
                "breed": "German Shepherd",
                "vaccination_history": [
                    "Rabies",
                    "Parvovirus",
                    "Leptospirosis"
                ]
            }
        },
        {
            "_index": "vet-visits",
            "_id": "8xY6RJYBLe2GoFQ6-9n9",
            "_score": 0.015384615,
            "_source": {
                "pet_name": "Luna",
                "owner_name": "Jake Thompson",
                "species": "Dog",
                "visit_details": "Mild ear infection, cleaning and antibiotics given.",
                "breed": "Labrador Mix",
                "vaccination_history": [
                    "Rabies",
                    "Bordetella"
                ]
            }
        }
    ]
}<h2>Fazit</h2><p>Im ersten Teil unserer Serie haben wir erklärt, wie man die Umgebung einrichtet und einen Server mit verschiedenen Suchendpunkten erstellt, um die Elasticsearch-Dokumente gemäß den Best Practices für Client/Server abzufragen. Schauen Sie sich <a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i">den zweiten Teil</a> unserer Serie an, in dem Sie Best Practices für die Produktion kennenlernen und erfahren, wie Sie den Elasticsearch Node.js-Client in serverlosen Umgebungen ausführen.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i</guid>
    <category><![CDATA[Javascript]]></category>
    <category><![CDATA[Grundlagen]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt16d00c8a548b32e8/6a17e3d8fbc5f8c740491a19/72200540ed258779d87e53a72ea189f8a138540c-1600x901.png" length="0" type="image/png"/>
    <pubDate>Thu, 15 May 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Verwenden von Ollama mit der Inference-API]]></title>
    <description><![CDATA[Erfahren Sie, wie Sie Ollama mithilfe der Inference API in Elasticsearch integrieren.]]></description>
    <content:encoded><![CDATA[<p>In diesem Artikel erfahren Sie, wie Sie lokale Modelle mithilfe von Ollama mit dem Elasticsearch-Inferenzmodell verbinden und Ihren Dokumenten dann mithilfe von Playground Fragen stellen.</p><p>Elasticsearch ermöglicht Benutzern die Verbindung mit LLMs über die Open <a href="https://www.elastic.co/de/guide/en/elasticsearch/reference/current/inference-apis.html">Inference API</a> und unterstützt Anbieter wie Amazon Bedrock, Cohere, Google AI, Azure AI Studio, HuggingFace – als Service und andere.</p><p><a href="https://ollama.com">Ollama</a> ist ein Tool, mit dem Sie LLM-Modelle mithilfe Ihrer eigenen Infrastruktur (Ihrem lokalen Computer/Server) herunterladen und ausführen können. <a href="https://ollama.com/library">Hier</a> finden Sie eine Liste der verfügbaren Modelle, die mit Ollama kompatibel sind.</p><p>Ollama ist eine großartige Option, wenn Sie verschiedene Open-Source-Modelle hosten und testen möchten, ohne sich Gedanken über die unterschiedlichen Einrichtungsmöglichkeiten der einzelnen Modelle oder über die Erstellung einer API für den Zugriff auf die Modellfunktionen machen zu müssen, da Ollama sich um alles kümmert.</p><p>Da die Ollama-API mit der OpenAI-API kompatibel ist, können wir das Inferenzmodell problemlos integrieren und mit Playground eine RAG-Anwendung erstellen.</p><h2>Voraussetzungen</h2><ol><li><p>Elasticsearch 8.17</p></li><li><p>Kibana 8.17</p></li><li><p>Python</p></li></ol><h2>Schritte</h2><ol><li><p><a href="https://www.elastic.co/de/search-labs/blog/ollama-with-inference-api#setting-up-ollama-llm-server">Einrichten des Ollama LLM-Servers</a></p></li><li><p><a href="https://www.elastic.co/de/search-labs/blog/ollama-with-inference-api#creating-mappings">Erstellen von Zuordnungen</a></p></li><li><p><a href="https://www.elastic.co/de/search-labs/blog/ollama-with-inference-api#indexing-data">Indizierung von Daten</a></p></li><li><p><a href="https://www.elastic.co/de/search-labs/blog/ollama-with-inference-api#asking-questions-using-playground">Fragen stellen mit Playground</a></p></li></ol><h2>Einrichten des Ollama LLM-Servers</h2><p>Wir werden einen LLM-Server einrichten, um ihn mithilfe von Ollama mit unserer Playground-Instanz zu verbinden. Wir müssen:</p><ul><li><p>Laden Sie Ollama herunter und führen Sie es aus.</p></li><li><p>Verwenden Sie ngrok, um über das Internet auf Ihren lokalen Webserver zuzugreifen, der Ollama hostet</p></li></ul><h3>Laden Sie Ollama herunter und führen Sie es aus</h3><p>Um Ollama zu verwenden, müssen wir <a href="https://ollama.com/download">es zuerst herunterladen</a>. Ollama bietet Unterstützung für Linux, Windows und macOS. Laden Sie <a href="https://ollama.com/download">hier einfach die mit Ihrem Betriebssystem kompatible Ollama-Version herunter.</a> Sobald Ollama installiert ist, können wir aus dieser <a href="https://ollama.com/library">Liste</a> unterstützter LLMs ein Modell auswählen. In diesem Beispiel verwenden wir das Modell <a href="https://ollama.com/library/llama3.2">llama3.2</a>, ein allgemeines mehrsprachiges Modell. Im Setup-Prozess aktivieren Sie das Befehlszeilentool für Ollama. Sobald das heruntergeladen ist, können Sie die folgende Zeile ausführen:</p>ollama pull llama3.2<p>Das Ergebnis lautet:</p>pulling manifest
pulling dde5aa3fc5ff... 100% ▕█████████████████████████████████████████████████████████████████████████████████████████▏ 2.0 GB
pulling 966de95ca8a6... 100% ▕█████████████████████████████████████████████████████████████████████████████████████████▏ 1.4 KB
pulling fcc5a6bec9da... 100% ▕█████████████████████████████████████████████████████████████████████████████████████████▏ 7.7 KB
pulling a70ff7e570d9... 100% ▕█████████████████████████████████████████████████████████████████████████████████████████▏ 6.0 KB
pulling 56bb8bd477a5... 100% ▕█████████████████████████████████████████████████████████████████████████████████████████▏   96 B
pulling 34bb5ab01051... 100% ▕█████████████████████████████████████████████████████████████████████████████████████████▏  561 B
verifying sha256 digest
writing manifest
success<p>Nach der Installation können Sie es mit diesem Befehl testen:</p>ollama run llama3.2<p>Stellen wir eine Frage:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc12f240920e897f5/6a17f39425daab32a508a367/ad1eff81c1b04d2a747c3afd0ecbc215e5bd96fd-800x501.gif" alt="Führen Sie Ollama aus und stellen Sie ihm eine Frage" /><p>Wenn das Modell ausgeführt wird, aktiviert Ollama eine API, die standardmäßig auf Port „11434“ ausgeführt wird. Lassen Sie uns eine Anfrage an diese API stellen und dabei der <a href="https://github.com/ollama/ollama/blob/main/docs/api.md">offiziellen Dokumentation</a> folgen:</p>curl http://localhost:11434/api/generate -d '{                                          
  "model": "llama3.2",               
  "prompt": "What is the capital of France?"
}' <p>Dies ist die Antwort, die wir erhalten haben:</p>{"model":"llama3.2","created_at":"2024-11-28T21:48:42.152817532Z","response":"The","done":false}
{"model":"llama3.2","created_at":"2024-11-28T21:48:42.251884485Z","response":" capital","done":false}
{"model":"llama3.2","created_at":"2024-11-28T21:48:42.347365913Z","response":" of","done":false}
{"model":"llama3.2","created_at":"2024-11-28T21:48:42.446837322Z","response":" France","done":false}
{"model":"llama3.2","created_at":"2024-11-28T21:48:42.542367394Z","response":" is","done":false}
{"model":"llama3.2","created_at":"2024-11-28T21:48:42.644580384Z","response":" Paris","done":false}
{"model":"llama3.2","created_at":"2024-11-28T21:48:42.739865362Z","response":".","done":false}
{"model":"llama3.2","created_at":"2024-11-28T21:48:42.834347518Z","response":"","done":true,"done_reason":"stop","context":[128006,9125,128007,271,38766,1303,33025,2696,25,6790,220,2366,18,271,128009,128006,882,128007,271,3923,374,279,6864,315,9822,30,128009,128006,78191,128007,271,791,6864,315,9822,374,12366,13],"total_duration":6948567145,"load_duration":4386106503,"prompt_eval_count":32,"prompt_eval_duration":1872000000,"eval_count":8,"eval_duration":684000000}<p><em>Beachten Sie, dass die spezifische Antwort für diesen Endpunkt ein Streaming ist.</em></p><h3>Endpunkt mit ngrok dem Internet aussetzen</h3><p>Da unser Endpunkt in einer lokalen Umgebung arbeitet, kann von einem anderen Punkt – wie unserer Elastic Cloud-Instanz – nicht über das Internet darauf zugegriffen werden. <a href="https://ngrok.com">ngrok</a> ermöglicht es uns, einen Port mit einer öffentlichen IP freizugeben. Erstellen Sie ein Konto in ngrok und folgen Sie der offiziellen <a href="https://dashboard.ngrok.com/get-started/setup">Einrichtungsanleitung</a>.</p><p>Sobald der ngrok-Agent installiert und konfiguriert wurde, können wir den von Ollama verwendeten Port verfügbar machen:</p>ngrok http 11434 --host-header="localhost:11434"<p><em>Hinweis: Der Header </em><em><code>--host-header="localhost:11434"</code></em><em> garantiert, dass der Header „Host“ in den Anfragen mit „localhost:11434“ übereinstimmt.</em></p><p>Durch Ausführen dieses Befehls wird ein öffentlicher Link zurückgegeben, der funktioniert, solange ngrok und der Ollama-Server lokal ausgeführt werden.</p>Session Status                online                                                                                                                                                                              
Account                       xxxx@yourEmailProvider.com (Plan: Free)                                                                                                                                             
Version                       3.18.4                                                                                                                                                                              
Region                        United States (us)                                                                                                                                                                  
Latency                       561ms                                                                                                                                                                               
Web Interface                 http://127.0.0.1:4040                                                                                                                                                               
Forwarding                    https://your-ngrok-url.ngrok-free.app -&gt; http://localhost:11434                                                                                                                   


Connections                   ttl     opn     rt1     rt5     p50     p90                                                                                                                                         
                              0       0       0.00    0.00    0.00    0.00                                                ```<p>Unter „Weiterleitung“ können wir sehen, dass ngrok eine URL generiert hat. Speichern Sie es für später.</p><p>Versuchen wir erneut, eine HTTP-Anfrage an den Endpunkt zu senden, diesmal unter Verwendung der von ngrok generierten URL:</p>curl https://your-ngrok-endpoint.ngrok-free.app/api/generate -d '{                                          
  "model": "llama3.2",               
  "prompt": "What is the capital of France?"
}'<p>Die Antwort sollte der vorherigen ähnlich sein.</p><h2>Erstellen von Zuordnungen</h2><h3>ELSER-Endpunkt</h3><p>Für dieses Beispiel <a href="https://www.elastic.co/de/guide/en/elasticsearch/reference/current/put-inference-api.html">erstellen wir einen Inferenzendpunkt mithilfe der Elasticsearch-Inferenz-API</a>. Zusätzlich verwenden wir <a href="https://www.elastic.co/de/guide/en/machine-learning/current/ml-nlp-elser.html">ELSER</a> , um die Einbettungen zu generieren.</p>PUT _inference/sparse_embedding/medicines-inference
{
  "service": "elasticsearch",
  "service_settings": {
    "num_allocations": 1,
    "num_threads": 1,
    "model_id": ".elser_model_2_linux-x86_64"
  }
}<p>Stellen wir uns für dieses Beispiel vor, Sie haben eine Apotheke, die zwei Arten von Medikamenten verkauft:</p><ul><li><p>Medikamente, für die ein Rezept erforderlich ist.</p></li><li><p>Medikamente, für die KEIN Rezept erforderlich ist.</p></li></ul><p>Diese Informationen würden in das Beschreibungsfeld jedes Medikaments aufgenommen.</p><p>Das LLM muss dieses Feld interpretieren, daher verwenden wir folgende Datenzuordnungen:</p>PUT medicines
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "copy_to": "semantic_field"
      },
      "semantic_field": {
        "type": "semantic_text",
        "inference_id": "medicines-inference"
      },
      "text_description": {
        "type": "text",
        "copy_to": "semantic_field"
      }
    }
  }
}<p>Das Feld <code>text_description</code> speichert den Klartext der Beschreibungen, während <code>semantic_field</code>, ein Feldtyp vom Typ <a href="https://www.elastic.co/de/guide/en/elasticsearch/reference/current/semantic-text.html">„semantic_text“</a> , die von ELSER generierten Einbettungen speichert.</p><p>Die Eigenschaft <a href="https://www.elastic.co/de/guide/en/elasticsearch/reference/current/copy-to.html">copy_to</a> kopiert den Inhalt aus den Feldern name und <code>text_description</code> in das semantische Feld, sodass die Einbettungen für diese Felder generiert werden.</p><h2>Indizierung von Daten</h2><p>Lassen Sie uns nun die Daten mithilfe der <a href="https://www.elastic.co/de/guide/en/elasticsearch/reference/current/docs-bulk.html">_bulk-API</a> indizieren.</p>POST _bulk
{"index":{"_index":"medicines"}}
{"id":1,"name":"Paracetamol","text_description":"An analgesic and antipyretic that does NOT require a prescription."}
{"index":{"_index":"medicines"}}
{"id":2,"name":"Ibuprofen","text_description":"A nonsteroidal anti-inflammatory drug (NSAID) available WITHOUT a prescription."}
{"index":{"_index":"medicines"}}
{"id":3,"name":"Amoxicillin","text_description":"An antibiotic that requires a prescription."}
{"index":{"_index":"medicines"}}
{"id":4,"name":"Lorazepam","text_description":"An anxiolytic medication that strictly requires a prescription."}
{"index":{"_index":"medicines"}}
{"id":5,"name":"Omeprazole","text_description":"A medication for stomach acidity that does NOT require a prescription."}
{"index":{"_index":"medicines"}}
{"id":6,"name":"Insulin","text_description":"A hormone used in diabetes treatment that requires a prescription."}
{"index":{"_index":"medicines"}}
{"id":7,"name":"Cold Medicine","text_description":"A compound formula to relieve flu symptoms available WITHOUT a prescription."}
{"index":{"_index":"medicines"}}
{"id":8,"name":"Clonazepam","text_description":"An antiepileptic medication that requires a prescription."}
{"index":{"_index":"medicines"}}
{"id":9,"name":"Vitamin C","text_description":"A dietary supplement that does NOT require a prescription."}
{"index":{"_index":"medicines"}}
{"id":10,"name":"Metformin","text_description":"A medication used for type 2 diabetes that requires a prescription."}<p>Abwehr:</p>{
   "errors": false,
   "took": 34732020848,
   "items": [
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "mYoeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 0,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "mooeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 1,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "m4oeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 2,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "nIoeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 3,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "nYoeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 4,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "nooeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 5,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "n4oeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 6,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "oIoeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 7,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "oYoeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 8,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "oooeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 9,
     	"_primary_term": 1,
     	"status": 201
   	}
 	}
   ]
 }<h2>Fragen stellen mit Playground</h2><p><a href="https://www.elastic.co/de/guide/en/kibana/current/playground.html">Playground</a> ist ein Kibana-Tool, mit dem Sie mithilfe von Elasticsearch-Indizes und einem LLM-Anbieter schnell ein RAG-System erstellen können. Sie können diesen <a href="https://www.elastic.co/de/search-labs/blog/playground-connectors-data-chat">Artikel</a> lesen, um mehr darüber zu erfahren.</p><h3>Verbinden des lokalen LLM mit Playground</h3><p>Wir müssen zunächst einen Connector erstellen, der die soeben erstellte öffentliche URL verwendet. Gehen Sie in Kibana zu <strong>„Suchen &gt; Playground“</strong> und klicken Sie dann auf „Mit einem LLM verbinden“.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt22148eabfabf6d3f/6a17f3963e9e459f97ba15c6/1854f0808f8150e359fe62ba5d901d32a88d477c-1600x867.png" alt="Verbinden des lokalen LLM mit Playground für Ollama" /><p>Durch diese Aktion wird auf der linken Seite der Kibana-Oberfläche ein Menü angezeigt. Klicken Sie dort auf „OpenAI“.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6e28194d9012f141/6a17f39725daab500a08a36b/c83d3c4d7035a518124ad7d22b38764db57b6800-933x1007.png" alt="Wählen Sie einen Connector aus: Öffnen Sie AI Ollama" /><p>Wir können jetzt mit der Konfiguration des OpenAI-Connectors beginnen.</p><p>Gehen Sie zu „Connector-Einstellungen“ und wählen Sie für den OpenAI-Anbieter „Andere (OpenAI-kompatible Dienste)“ aus:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9984dce6f78a7c08/6a17f3990b0bed0b7add36c4/ecfcdc4b575c309bd55b4e61ca0ddb348aa84f64-917x268.png" alt="Legen Sie die Connector-Einstellungen für die Verwendung von Ollama mit der Inference-API fest" /><p>Nun konfigurieren wir die übrigen Felder. In diesem Beispiel nennen wir unser Modell "medicines-llm". Verwenden Sie im URL-Feld die von ngrok generierte URL (/v1/chat/completions). Wählen Sie im Feld „Standardmodell“ die Option „llama3.2“ aus. Wir benötigen keinen API-Schlüssel, geben Sie einfach einen beliebigen Text ein, um fortzufahren:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8a24b93a39d380fb/6a17f39b96142a15c7eb1c3c/5d3b5027c8096cbe49fb740d70aa24e849611a9d-916x688.png" alt="Einstellungen hinzufügen" /><p>Klicken Sie auf „Speichern“ und fügen Sie die Indexmedikamente hinzu, indem Sie auf „Datenquellen hinzufügen“ klicken:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4f107a54d5be25f9/6a17f39d4b055deb9d432338/525113da59e902c8235f62bde8fb62371a63e11b-1579x753.png" alt="Fügen Sie Datenquellen hinzu, um Ihren Dokumenten mithilfe von Playground Fragen zu stellen" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt03fb36fe05dcdbf5/6a17f39ebe608602f40048be/96138de0bbe2c2ac619f64889d3487df62739ca4-466x805.png" alt="Abfragedaten hinzufügen" /><p>Großartig! Wir haben jetzt Zugriff auf Playground mithilfe des LLM, das wir lokal als RAG-Engine ausführen.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb1b27580107259b3/6a17f3a096142abefceb1c40/cfb48b33c70f4534ab77eb01f58008237f65e6f4-1600x851.png" alt="Modelleinstellungen im Playground auswählen" /><p>Bevor wir es testen, fügen wir dem Agenten spezifischere Anweisungen hinzu und erhöhen die Anzahl der an das Modell gesendeten Dokumente auf 10, damit die Antwort möglichst viele Dokumente enthält. Das Kontextfeld ist <code>semantic_field</code> und enthält dank der Eigenschaft „copy_to“ den Namen und die Beschreibung der Medikamente.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4fbc97dc87c6fc62/6a17f3a1e8fbce052c3a1aa0/0c57c9c0e1a0e7b58fffdd3ef81d67d41e2990c4-580x806.png" alt="Modelleinstellungen im Elastic Playground" /><p>Stellen wir nun die Frage: <em><strong>Kann ich Clonazepam ohne Rezept kaufen?</strong></em> und sehen Sie, was passiert:</p><p>Wie erwartet haben wir die richtige Antwort erhalten.</p><h3>Wie geht es weiter?</h3><p>Der nächste Schritt besteht darin, Ihre eigene Anwendung zu erstellen! Playground bietet ein Code-Skript in Python, das Sie auf Ihrem Computer ausführen und an Ihre Anforderungen anpassen können. Beispielsweise indem Sie es hinter einen <a href="https://fastapi.tiangolo.com/">FastAPI</a> -Server stellen, um einen QA-Medizin-Chatbot zu erstellen, der von Ihrer Benutzeroberfläche genutzt wird.</p><p>Sie finden diesen Code, indem Sie oben rechts im Playground auf die Schaltfläche <em><strong>„Code anzeigen“</strong></em> klicken:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt42fe193b8aa08830/6a17f3a33e9e4569e8ba15ca/816bfd0e5f936ad65dbe719d5df10714e550a40b-380x121.png" alt="Schaltfläche „Code anzeigen“" /><p>Und Sie verwenden die <em><strong>Endpunkte und API-Schlüssel,</strong></em> um die im Code erforderliche Umgebungsvariable <code>ES_API_KEY</code> zu generieren.</p><p>Für dieses spezielle Beispiel lautet der Code wie folgt:</p>## Install the required packages
## pip install -qU elasticsearch openai
import os
from elasticsearch import Elasticsearch
from openai import OpenAI
es_client = Elasticsearch(
    "https://your-deployment.us-central1.gcp.cloud.es.io:443",
    api_key=os.environ["ES_API_KEY"]
)
openai_client = OpenAI(
    api_key=os.environ["OPENAI_API_KEY"],
)
index_source_fields = {
    "medicines": [
        "semantic_field"
    ]
}
def get_elasticsearch_results():
    es_query = {
        "retriever": {
            "standard": {
                "query": {
                    "nested": {
                        "path": "semantic_field.inference.chunks",
                        "query": {
                            "sparse_vector": {
                                "inference_id": "medicines-inference",
                                "field": "semantic_field.inference.chunks.embeddings",
                                "query": query
                            }
                        },
                        "inner_hits": {
                            "size": 2,
                            "name": "medicines.semantic_field",
                            "_source": [
                                "semantic_field.inference.chunks.text"
                            ]
                        }
                    }
                }
            }
        },
        "size": 3
    }
    result = es_client.search(index="medicines", body=es_query)
    return result["hits"]["hits"]
def create_openai_prompt(results):
    context = ""
    for hit in results:
        inner_hit_path = f"{hit['_index']}.{index_source_fields.get(hit['_index'])[0]}"
        ## For semantic_text matches, we need to extract the text from the inner_hits
        if 'inner_hits' in hit and inner_hit_path in hit['inner_hits']:
            context += '\n --- \n'.join(inner_hit['_source']['text'] for inner_hit in hit['inner_hits'][inner_hit_path]['hits']['hits'])
        else:
            source_field = index_source_fields.get(hit["_index"])[0]
            hit_context = hit["_source"][source_field]
            context += f"{hit_context}\n"
    prompt = f"""
  Instructions:
  - You are an assistant specializing in answering questions about the sale of medicines.
  - Answer questions truthfully and factually using only the context presented.
  - If you don't know the answer, just say that you don't know, don't make up an answer.
  - You must always cite the document where the answer was extracted using inline academic citation style [], using the position.
  - Use markdown format for code examples.
  - You are correct, factual, precise, and reliable.
  Context:
  {context}
  """
    return prompt
def generate_openai_completion(user_prompt, question):
    response = openai_client.chat.completions.create(
        model="gpt-3.5-turbo",
        messages=[
            {"role": "system", "content": user_prompt},
            {"role": "user", "content": question},
        ]
    )
    return response.choices[0].message.content
if __name__ == "__main__":
    question = "my question"
    elasticsearch_results = get_elasticsearch_results()
    context_prompt = create_openai_prompt(elasticsearch_results)
    openai_completion = generate_openai_completion(context_prompt, question)
    print(openai_completion)<p>Damit es mit Ollama funktioniert, müssen Sie den OpenAI-Client so ändern, dass er eine Verbindung zum Ollama-Server statt zum OpenAI-Server herstellt. Die vollständige Liste der OpenAI-Beispiele und kompatiblen Endpunkte finden Sie hier.</p>openai_client = OpenAI(
    # you can use http://localhost:11434/v1/ if running this code locally.
    base_url='https://your-ngrok-url.ngrok-free.app/v1/',
    # required but ignored
    api_key='ollama',
)<p>Und ändern Sie das Modell auch in llama3.2, wenn Sie die Vervollständigungsmethode aufrufen:</p>def generate_openai_completion(user_prompt, question):
    response = openai_client.chat.completions.create(
        model="llama3.2",
        messages=[
            {"role": "system", "content": user_prompt},
            {"role": "user", "content": question},
        ]
    )
    return response.choices[0].message.content<p>Fügen wir unsere Frage hinzu: <em><strong>Kann ich Clonazepam ohne Rezept kaufen? </strong></em>Zur Elasticsearch-Abfrage:</p>def get_elasticsearch_results():
    es_query = {
        "retriever": {
            "standard": {
                "query": {
                    "nested": {
                        "path": "semantic_field.inference.chunks",
                        "query": {
                            "sparse_vector": {
                                "inference_id": "medicines-inference",
                                "field": "semantic_field.inference.chunks.embeddings",
                                "query": "Can I buy Clonazepam without a prescription?"
                            }
                        },
                        "inner_hits": {
                            "size": 2,
                            "name": "medicines.semantic_field",
                            "_source": [
                                "semantic_field.inference.chunks.text"
                            ]
                        }
                    }
                }
            }
        },
        "size": 3
    }
    result = es_client.search(index="medicines", body=es_query)
    return result["hits"]["hits"]<p>Und auch zum Abschlussaufruf mit einigen Ausdrucken, damit wir bestätigen können, dass wir die Elasticsearch-Ergebnisse als Teil des Fragenkontexts senden:</p>if __name__ == "__main__":
    question = "Can I buy Clonazepam without a prescription?"
    elasticsearch_results = get_elasticsearch_results()
    context_prompt = create_openai_prompt(elasticsearch_results)
    print("========== Context Prompt START ==========")
    print(context_prompt)
    print("========== Context Prompt END ==========")
    print("========== Ollama Completion START ==========")
    openai_completion = generate_openai_completion(context_prompt, question)
    print(openai_completion)
    print("========== Ollama Completion END ==========")<p>Lassen Sie uns nun den Befehl ausführen</p><p><code>pip install -qU elasticsearch openai</code></p><p><code>python main.py</code></p><p>Sie sollten ungefähr Folgendes sehen:</p>========== Context Prompt START ==========
  Instructions:
  - You are an assistant specializing in answering questions about the sale of medicines.
  - Answer questions truthfully and factually using only the context presented.
  - If you don't know the answer, just say that you don't know, don't make up an answer.
  - You must always cite the document where the answer was extracted using inline academic citation style [], using the position.
  - Use markdown format for code examples.
  - You are correct, factual, precise, and reliable.
  Context:
  Clonazepam
 ---
An antiepileptic medication that requires a prescription.A nonsteroidal anti-inflammatory drug (NSAID) available WITHOUT a prescription.
 ---
IbuprofenAn anxiolytic medication that strictly requires a prescription.
 ---
Lorazepam


========== Context Prompt END ==========
========== Ollama Completion START ==========
No, you cannot buy Clonazepam over-the-counter (OTC) without a prescription [1]. It is classified as a controlled substance in the United States due to its potential for dependence and abuse. Therefore, it can only be obtained from a licensed healthcare provider who will issue a prescription for this medication.
========== Ollama Completion END ==========<h2>Fazit</h2><p>In diesem Artikel können wir die Leistungsfähigkeit und Vielseitigkeit von Tools wie Ollama sehen, wenn wir sie zusammen mit der Elasticsearch-Inferenz-API und Playground verwenden.</p><p>Nach einigen einfachen Schritten hatten wir eine funktionierende RAG-Anwendung mit einem Chat, der ein LLM verwendete, das kostenlos in unserer eigenen Infrastruktur lief. Dies ermöglicht uns außerdem eine bessere Kontrolle über Ressourcen und vertrauliche Informationen und gibt uns außerdem Zugriff auf eine Vielzahl von Modellen für unterschiedliche Aufgaben.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/ollama-with-inference-api</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/ollama-with-inference-api</guid>
    <category><![CDATA[KI]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd9c8eb0fc946920e/6a17f3a46864a4b2fbb688f0/399b9ef527be633845fb6505b68132cc03bc9e09-1150x628.png" length="0" type="image/png"/>
    <pubDate>Fri, 14 Feb 2025 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>