<?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[Gustavo Llermaly - 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[Gustavo Llermaly - 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/gustavo-llermaly</link>
    </image>
    <link>https://www.elastic.co/de/search-labs/author/gustavo-llermaly</link>
    <atom:link href="https://www.elastic.co/de/search-labs/rss/author/gustavo-llermaly.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[de]]></language>
    <lastBuildDate>Wed, 23 Sep 2026 04:32:50 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Überwachung der Kibana-Dashboard-Ansichten mit Elastic Workflows]]></title>
    <description><![CDATA[Erfahren Sie, wie Sie mit Elastic Workflows alle 30 Minuten Kibana-Dashboard-Ansichtsmetriken erfassen und in Elasticsearch indexieren können, um benutzerdefinierte Analysen und Visualisierungen auf Basis Ihrer eigenen Daten zu erstellen.]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/kibana">Kibana</a> erfasst, wie oft jedes Dashboard angezeigt wird, aber diese Daten werden in keinem integrierten Dashboard nativ angezeigt. In diesem Artikel werden wir <strong>Elastic Workflows</strong> nutzen, um diese Daten alle 30 Minuten automatisch zu sammeln und in Elasticsearch zu indexieren, damit wir darauf unsere eigenen Analysen aufbauen können.</p><p><a href="https://www.elastic.co/docs/explore-analyze/workflows">Elastic Workflows</a> ist eine in Kibana integrierte Automatisierungs-Engine, mit der Sie mehrstufige Prozesse mithilfe einer einfachen YAML-Konfiguration definieren können. Jeder Workflow kann über einen Zeitplan, ein Ereignis oder als Tool in <a href="https://www.elastic.co/docs/explore-analyze/ai-features/elastic-agent-builder">Elastic Agent Builder</a> ausgelöst werden, und jeder Schritt kann Kibana-APIs aufrufen, Elasticsearch abfragen oder Daten umwandeln.</p><p>Wir verwenden die Anzahl der Dashboard-Ansichten als konkretes Beispiel, aber das gleiche Muster gilt für jede Metrik, die über die Kibana-API für gespeicherte Objekte bereitgestellt wird.</p><h2>Voraussetzungen</h2><ul><li><p><a href="https://www.elastic.co/cloud">Elastic Cloud</a> oder <a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed">selbstverwalteter </a>Cluster (Version 9.3)</p></li><li><p><a href="https://www.elastic.co/docs/explore-analyze/workflows/get-started#workflows-prerequisites">Workflows aktiviert</a> (Erweiterte Einstellungen)</p></li></ul><h2>Schritt 1: Untersuchen Sie die Rohdaten in <a href="https://www.elastic.co/docs/explore-analyze/query-filter/tools/console">Dev Tools</a></h2><p>Bevor wir etwas bauen, sollten wir uns erst einmal darüber klar werden, welche Daten wir haben. Kibana speichert den Großteil seiner Konfiguration und Metadaten als <a href="https://www.elastic.co/docs/api/doc/kibana/group/endpoint-saved-objects">gespeicherte Objekte</a> in einem separaten internen Index. Kibana erfasst auf diese Weise unter anderem die Anzahl der Dashboard-Aufrufe, indem ein spezieller Typ gespeicherter Objekte namens „Nutzungszähler“ verwendet wird. Sie können sie direkt in Dev Tools abfragen:</p>GET kbn:/api/saved_objects/_find?type=usage-counter&amp;filter=usage-counter.attributes.domainId:"dashboard"%20and%20usage-counter.attributes.counterType:"viewed"&amp;per_page=10000<p>Die Reaktion sieht in etwa wie folgt aus:</p>{
  "page": 1,
  "per_page": 10000,
  "total": 1,
  "saved_objects": [
    {
      "type": "usage-counter",
      "id": "dashboard:346f3c64-ebca-484d-9d57-ec600067d596:viewed:server:20260310",
      "attributes": {
        "domainId": "dashboard",
        "counterName": "346f3c64-ebca-484d-9d57-ec600067d596",
        "counterType": "viewed",
        "source": "server",
        "count": 1
      },
      ...
    }
  ]<p>Das Feld <code>counterName</code> ist die Dashboard-ID und <code>count</code> ist die kumulierte Anzahl der Aufrufe für dieses Dashboard an diesem bestimmten Tag. Kibana erstellt ein Zählerobjekt pro Dashboard pro Tag; Sie können das Datumssuffix in der Objekt-ID sehen (...viewed:server:20260310). Die Anzahl wächst im Laufe des Tages, wenn Nutzer das Dashboard öffnen.</p><p>Anstatt dieses tägliche Dokumentmodell in unserem Index zu replizieren, erstellen wir pro Workflow-Ausführung ein Dokument. Jedes Dokument zeichnet auf, wie viele Aufrufe dieses Dashboard für den Tag zum Zeitpunkt der Erfassung angesammelt hatte.</p><h2>Schritt 2: Den Zielindex erstellen</h2><p>Wir benötigen einen Index, um unsere Snapshots der Dashboard-Ansicht zu speichern. Der folgende Befehl erstellt sie mit expliziten Mappings, sodass wir später aggregieren und visualisieren können. Führen Sie dazu in Dev Tools den folgenden Befehl aus:</p>PUT dashboard-views
{
  "mappings": {
    "properties": {
      "captured_at": {
        "type": "date"
      },
      "dashboard_id": {
        "type": "keyword"
      },
      "dashboard_name": {
        "type": "keyword"
      },
      "view_count": {
        "type": "integer"
      }
    }
  }
}<p>Die Verwendung von <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/keyword"><code>keyword</code></a> Mappings für IDs und Namen ermöglicht <a href="https://www.elastic.co/docs/explore-analyze/query-filter/aggregations">Aggregationen</a>. Die Verwendung von <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/number"><code>integer</code></a> für <code>view_count</code> ist ein sicherer Standardwert, da Kibana den Zähler täglich zurücksetzt und das Erreichen des 32-Bit-Limits (mehr als 2 Milliarden Aufrufe an einem einzigen Tag) daher keine realistische Sorge darstellt. Es unterstützt weiterhin numerische Operationen wie <a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-metrics-max-aggregation"><code>max</code></a>, <a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-metrics-avg-aggregation"><code>avg</code></a>und <a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-metrics-min-aggregation"><code>min</code></a> und andere.</p><h2>Schritt 3: Den Workflow erstellen</h2><p>Gehen Sie zu <strong>Stack Management &gt; Workflows &gt; Neuer Workflow</strong>, und fügen Sie die folgende Workflow-YAML-Konfiguration ein:</p>name: dashboard-views-ingestion
triggers:
  - type: scheduled
    with:
      every: 30m

steps:
  - name: fetch_dashboard_views
    type: kibana.request
    with:
      method: GET
      path: &gt;-
        /api/saved_objects/_find?type=usage-counter&amp;per_page=10000&amp;filter=usage-counter.attributes.domainId:"dashboard"%20and%20usage-counter.attributes.counterType:"viewed"

  - name: index_each_dashboard
    type: foreach
    foreach: "{{ steps.fetch_dashboard_views.output.saved_objects }}"
    steps:
      - name: fetch_dashboard_name
        type: kibana.request
        with:
          method: GET
          path: /api/saved_objects/dashboard/{{ foreach.item.attributes.counterName }}
        on-failure:
          continue: true

      - name: index_doc
        type: elasticsearch.request
        with:
          method: POST
          path: /dashboard-views/_doc
          body:
            dashboard_id: "{{ foreach.item.attributes.counterName }}"
            dashboard_name: "{{ steps.fetch_dashboard_name.output.attributes.title }}"
            view_count: "${{ foreach.item.attributes.count | plus: 0 }}"
            captured_at: "{{ execution.startedAt | date: '%Y-%m-%dT%H:%M:%SZ' }}"<p>Im folgenden Abschnitt werfen wir Schritt für Schritt einen Blick auf den Workflow.</p><h3>So funktioniert der Workflow</h3><h4>Auslöser</h4><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7672aa533b4bc9ed/6a17dc5b420229d07c29f4d4/5670991d65c64ee833924225c2d375a1be868b13-325x162.png" alt=" Geplante Auslöser" /><p>Der Workflow wird alle 30 Minuten mit einem geplanten Auslöser ausgeführt. Dadurch erhalten wir Zeitreihendaten, ohne die API zu überlasten.</p><h4>fetch_dashboard_views</h4><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltab2a16f1f11304ea/6a17dc5d25daab26f608a117/66eaec147c3d01c524c67cf1c7f663ac56a3259d-812x215.png" alt=" Dashboard abrufen" /><p>Verwendet <code>kibana.request</code> , um die Kibana-API für gespeicherte Objekte aufzurufen. Es ist keine Authentifizierungskonfiguration erforderlich: Die Workflow-Engine fügt automatisch die korrekten Header basierend auf dem Ausführungskontext hinzu.</p><h4>index_each_dashboard (foreach)</h4><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte6b2611b0216555e/6a17dc5f445de95b584cffe5/aad45e8aed8dc81ded6260cd6199ff78dcffe3b4-1892x290.png" alt="Indexieren Sie jedes Dashboard" /><p>Iteriert über das Array <a href="https://www.elastic.co/docs/api/doc/kibana/group/endpoint-saved-objects"><code>saved_objects</code></a>, das vom vorherigen Schritt zurückgegeben wurde. Das aktuelle Element in jeder Iteration ist als <code>foreach.item</code> verfügbar. Innerhalb der Schleife führen wir zwei verschachtelte Schritte für jedes Dashboard aus.</p><p><strong>1. </strong><strong><code>fetch_dashboard_name</code></strong><strong>:</strong></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb3733b3c24629abf/6a17dc60a292993fe3d02b75/db21ec5094b743018b9cd66c5052681f14c7d7e3-1999x431.png" alt=": Dashboard-Name abrufen" /><p>Ermittelt den für Menschen lesbaren Dashboard-Titel durch Aufruf von <code>GET /api/saved_objects/dashboard/{id}</code>. Wir fügen <code>on-failure: continue: true</code> hinzu, damit die Schleife fortgesetzt wird, anstatt dass die gesamte Ausführung fehlschlägt, falls ein Dashboard gelöscht wurde, aber noch Aufrufzähler vorhanden sind.</p><p><strong>2. </strong><strong><code>index_doc</code></strong><strong>:</strong></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt385c1f29d717c280/6a17dc62faa91353cb93c759/f49dd0c9f0817bb1e1e5d9f4a2b05d13ef331054-1999x626.png" alt=" Elasticsearch-Anfrage" /><p>Indexiert jedes Dokument mit <code>POST /dashboard-views/_doc</code> (ohne eine explizite ID), wodurch Elasticsearch IDs automatisch generieren kann. Dadurch wird bei jedem Durchlauf ein neues Dokument erstellt, wodurch eine Historie der Aufrufzahlen im Laufe der Zeit aufgebaut wird, anstatt den vorherigen Snapshot zu überschreiben.</p><p>Zwei Dinge, die es zu beachten gilt:</p><ul><li><p>Das Feld <code>captured_at</code> verwendet den Datumsfilter, um den Zeitstempel als <a href="https://www.iso.org/iso-8601-date-and-time-format.html">ISO 8601</a> zu formatieren. Ohne ihn erscheint der Wert als JavaScript-Datumszeichenfolge, wie <code>Tue Mar 10 2026 05:03:47 GMT+0000</code>, die Elasticsearch nicht als Datum abbildet.</p></li><li><p>Der Ausdruck <code>view_count</code> verwendet die Syntax <code>${{ }}</code> mit <code>| plus: 0</code>, um den numerischen Typ beizubehalten. Die Verwendung von <code>{{ }}</code> würde es als Zeichenfolge darstellen, was mathematische Operationen im Dashboard verhindern würde.</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3b94bb6c22c0253e/6a17dc6425daab37a508a11b/6d48c8784d5df6192e8b5175e69dbab5098194bc-919x774.png" alt="" /><p><em>Die Benutzeroberfläche ermöglicht eine komfortable Fehlerbehebung jedes einzelnen Workflow-Schritts.</em></p><h2>Schritt 4: Das Statistik-Dashboard erstellen</h2><p>Sobald der Workflow einige Male ausgeführt wurde und Daten gesammelt wurden, erstellen Sie in Kibana ein neues Dashboard mithilfe der Data View „Dashboard-Ansichten“.</p><p>Einige Panels für den Anfang:</p><ul><li><p><strong>Top-Dashboards nach Ansichten:</strong> Verwenden Sie ein <a href="https://www.elastic.co/docs/explore-analyze/visualize/charts/bar-charts"><strong>Balkendiagramm</strong></a> mit <code>dashboard_name</code> auf der X-Achse und <code>last_value(view_count)</code> auf der Y-Achse. Hier wird die aktuelle tägliche Anzahl der Aufrufe pro Dashboard angezeigt.</p></li><li><p><strong>Ansichten im Zeitverlauf:</strong> Verwenden Sie ein <a href="https://www.elastic.co/docs/explore-analyze/visualize/charts/line-charts"><strong>Liniendiagramm</strong></a> mit <code>captured_at</code> auf der X-Achse und <code>last_value(view_count)</code> auf der Y-Achse, unterteilt nach <code>dashboard_name</code>. Da jede Ausführung ein neues Dokument anhängt, verwenden Sie den letzten Wert, um die Spitzenanzahl pro Zeit-Buckets zu erhalten, anstatt Duplikate zu summieren.</p></li><li><p><strong>Aktueller Snapshot:</strong> Verwenden Sie eine <a href="https://www.elastic.co/docs/explore-analyze/visualize/charts/tables"><strong>Datentabelle</strong></a> mit den neuesten <code>captured_at</code>, um die aktuellsten Aufrufzahlen aller Dashboards anzuzeigen.</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt18d0390e0526b215/6a17dc65a292991da7d02b79/e245b95f67daf76a2aaf4cb9df2c75ef4cfef582-1462x747.png" alt="" /><p>Da jeder Workflow ein neues Dokument erstellt, können Sie nach Zeitbereich filtern, um die Aktivität in bestimmten Zeiträumen zu analysieren, Vergleiche von Woche zu Woche anzustellen oder Alerts zu erstellen, wenn ein Dashboard unter einen Ansichtsschwellenwert fällt.</p><h2><strong>Fazit</strong></h2><p>Elastic Workflows eignet sich gut für diese Art von periodischer Datenerhebung, da sowohl die Quelle (Kibana API) als auch das Ziel (Elasticsearch) native sind, was bedeutet, dass keine Zugangsdaten verwaltet werden. Die Workflow-Engine verwaltet die Authentifizierung für die Schritte <code>kibana.request</code> und <code>elasticsearch.request</code> automatisch, sodass Sie nur die Logik schreiben müssen.</p><h2><strong>Ressourcen</strong></h2><ul><li><p><a href="https://www.elastic.co/docs/explore-analyze/workflows">Elastic-Workflows</a></p></li><li><p><a href="https://www.elastic.co/docs/api/doc/kibana/">Kibana-API</a></p></li></ul>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/monitor-kibana-dashboard-views-elastic-workflows</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/monitor-kibana-dashboard-views-elastic-workflows</guid>
    <category><![CDATA[Entwicklererfahrung]]></category>
    <dc:creator><![CDATA[Gustavo Llermaly]]></dc:creator>
    <pubDate>Fri, 03 Apr 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elastic Open Web Crawler als Code]]></title>
    <description><![CDATA[Erfahren Sie, wie Sie GitHub Actions verwenden, um Elastic Open Crawler-Konfigurationen zu verwalten, sodass Änderungen, die wir in das Repository übertragen, automatisch auf die bereitgestellte Instanz des Crawlers angewendet werden.]]></description>
    <content:encoded><![CDATA[<p>Mit <a href="https://github.com/elastic/crawler">Elastic Open Web Crawler</a> und seiner CLI-gesteuerten Architektur lassen sich versionierte Crawler-Konfigurationen und eine CI/CD-Pipeline mit lokalen Tests jetzt recht einfach realisieren.</p><p>Traditionell war die Verwaltung von Webcrawlern ein manueller, fehleranfälliger Prozess. Dabei ging es um das direkte Bearbeiten von Konfigurationen in der Benutzeroberfläche sowie um das Klonen von Crawl-Konfigurationen, das Zurücksetzen von Einstellungen, die Versionsverwaltung und vieles mehr. Die Behandlung von Crawler-Konfigurationen als Code löst dieses Problem, indem sie die gleichen Vorteile bietet, die wir von der Softwareentwicklung erwarten: Wiederholbarkeit, Nachvollziehbarkeit und Automatisierung.</p><p>Dieser Workflow erleichtert es, den Open Web Crawler in Ihre CI/CD-Pipeline für Rollbacks, Backups und Migrationen einzubinden – Aufgaben, die mit früheren Elastic Crawlern wie dem Elastic Web Crawler oder dem App Search Crawler wesentlich schwieriger waren.</p><p>In diesem Artikel erfahren Sie, wie Sie:</p><ul><li><p>Verwalten Sie unsere Crawl-Konfigurationen mit GitHub.</p></li><li><p>Eine lokale Testumgebung für Pipelines vor der Bereitstellung einrichten</p></li><li><p>Wir erstellen eine Produktionsumgebung, um den Webcrawler jedes Mal mit neuen Einstellungen auszuführen, wenn wir Änderungen an unseren Hauptzweig übertragen.</p></li></ul><p>Das Projekt-Repository finden Sie <a href="https://github.com/llermaly/elastic-open-crawler-as-code"><em><strong>hier</strong></em></a><em><strong>. </strong></em><em>Zum Zeitpunkt der Erstellung dieses Dokuments verwende ich Elasticsearch 9.1.3 und Open Web Crawler 0.4.2.</em></p><h2>Voraussetzungen</h2><ul><li><p>Docker Desktop</p></li><li><p>Elasticsearch-Instanz</p></li><li><p>Virtuelle Maschine mit SSH-Zugriff (z. B. AWS EC2) und installiertem Docker</p></li></ul><h2>Schritte</h2><ol><li><p>Ordnerstruktur</p></li><li><p>Raupenkonfiguration</p></li><li><p>Docker-Compose-Datei (lokale Umgebung)</p></li><li><p>GitHub Actions</p></li><li><p>Lokale Tests</p></li><li><p>Bereitstellung in der Produktionsumgebung</p></li><li><p>Änderungen vornehmen und erneut bereitstellen</p></li></ol><h2>Ordnerstruktur</h2><p>Für dieses Projekt werden wir folgende Dateistruktur verwenden:</p>├── docker-compose.yml # Local elasticsearch + crawler
├── config/crawler-config.yml # Crawler config
├── .github/workflows/deploy.yml # GH Action to deploy changes
├── local.sh # Script to run our local crawler<h2>Raupenkonfiguration</h2><p>Unter <code>crawler-config.yml,</code> wird Folgendes eingetragen:</p>output_sink: elasticsearch
output_index: web-crawl-index
max_crawl_depth: 1

elasticsearch:
  host: ${ES_HOST}
  api_key: ${ES_API_KEY}
     
domains:
  - url: https://web-scraping.dev
    seed_urls:
      - https://web-scraping.dev/product/1
      - https://web-scraping.dev/product/2
      - https://web-scraping.dev/product/3<p>Dies führt einen Crawl von <a href="https://web-scraping.dev/products">https://web-scraping.dev/products</a> durch, einer simulierten Website für Produkte. Wir werden nur die ersten drei Produktseiten durchsuchen. Die Einstellung <code>max_crawl_depth</code> verhindert, dass der Crawler mehr Seiten als die als <code>seed_urls</code> definierten Seiten entdeckt, indem er die darin enthaltenen Links nicht öffnet.</p><p>Elasticsearch <code>host</code> und <code>api_key</code> werden dynamisch befüllt, abhängig von der Umgebung, in der das Skript ausgeführt wird.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7d37b966aafbd3c1/6a17ef9842022946e929f6b9/f9831034e1c4ccb554d37bdd188f2824338355a0-890x624.png" alt="Die Produktseite für „Schokoladenbox“ von der Domain web-scraping.dev, einer Testseite für Web-Scraping. Auf der Seite werden der Produkttitel, das Bild, die Beschreibung sowie die HTML-Elemente für Preis- und Kaufbuttons angezeigt." /><h2>Docker-Compose-Datei (lokale Umgebung)</h2><p>Für die lokale Umgebung <code>docker-compose.yml,</code> werden wir den Crawler und einen einzelnen Elasticsearch-Cluster + Kibana bereitstellen, damit wir unsere Crawling-Ergebnisse <em><strong>vor</strong></em> der Bereitstellung in der Produktionsumgebung einfach visualisieren können.</p>services:
  es01:
    image: docker.elastic.co/elasticsearch/elasticsearch:9.1.3
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
      - ES_JAVA_OPTS=-Xms1g -Xmx1g
    ports:
      - "9200:9200"
    networks: [esnet]
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9200"]
      interval: 5s
      timeout: 5s
      retries: 10

  kibana:
    image: docker.elastic.co/kibana/kibana:9.1.3
    environment:
      - ELASTICSEARCH_HOSTS=http://es01:9200
    ports:
      - "5601:5601"
    networks: [esnet]
    depends_on: [es01]

  crawler:
    image: docker.elastic.co/integrations/crawler:0.4.2
    environment:
      - ES_HOST=http://es01:9200
      - CRAWLER_JRUBY_OPTS=--server
    container_name: crawler
    volumes:
      - ./config:/home/app/config
    networks: [esnet]
    entrypoint: ["/home/app/bin/crawler", "crawl", "/home/app/config/crawl-config-final.yml"]
    stdin_open: true
    tty: true

networks:
  esnet:
    driver: bridge<p>Beachten Sie, wie der Crawler wartet, bis Elasticsearch bereit ist, ausgeführt zu werden.</p><h2>GitHub Actions</h2><p>Nun müssen wir eine GitHub-Aktion erstellen, die die neuen Einstellungen kopiert und den Crawler bei jedem Push auf den Hauptzweig in unserer virtuellen Maschine ausführt. Dadurch wird sichergestellt, dass wir immer die aktuellste Konfiguration im Einsatz haben, ohne manuell in die virtuelle Maschine eingreifen zu müssen, um Dateien zu aktualisieren und den Crawler auszuführen. Wir werden AWS EC2 als Anbieter virtueller Maschinen verwenden.</p><p>Der erste Schritt besteht darin, den Host (<code>VM_HOST</code>), den Maschinenbenutzer (<code>VM_USER</code>), den SSH-RSA-Schlüssel (<code>VM_KEY</code>), den Elasticsearch-Host (<code>ES_HOST</code>) und den Elasticsearch-API-Schlüssel (<code>ES_API_KEY</code>) zu den GitHub Action Secrets hinzuzufügen:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb5a0fcfff9b7f997/6a17ef9a6df731d7d40a0fdf/e1075bc54151b4b94eac2a6bd2682e9997e6c709-1106x707.png" alt="Eine Konfigurationsseite für „Aktionen, Geheimnisse und Variablen“, auf der Repository-Geheimnisse wie VM_HOST, VM_KEY und VM_USER über eine webbasierte Oberfläche angezeigt werden." /><p>Auf diese Weise kann die Aktion auf unseren Server zugreifen, um die neuen Dateien zu kopieren und den Crawl auszuführen.</p><p>Nun erstellen wir unsere <code>.github/workflows/deploy.yml</code> -Datei:</p>name: Deploy

on:
  push:
    branches: [main]

jobs:
  Deploy:
    name: Deploy to EC2
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v5

      - name: Deploy crawler
        env:
          HOSTNAME: ${{ secrets.VM_HOST }}
          USER_NAME: ${{ secrets.VM_USER }}
          PRIVATE_KEY: ${{ secrets.VM_KEY }}
          ES_HOST: ${{ secrets.ES_HOST }}
          ES_API_KEY: ${{ secrets.ES_API_KEY }}
        run: |
          # Save private key
          echo "$PRIVATE_KEY" &gt; private_key
          chmod 600 private_key

          # Generate final config locally
          envsubst &lt; config/crawler-config.yml &gt; config/crawl-config-final.yml

          # Copy the config folder to VM
          scp -o StrictHostKeyChecking=no -i private_key -r config ${USER_NAME}@${HOSTNAME}:~/config

          # SSH into VM and run crawler
          ssh -o StrictHostKeyChecking=no -i private_key ${USER_NAME}@${HOSTNAME} &lt;&lt; EOF
            docker run --rm \
              -v ~/config:/config \
              docker.elastic.co/integrations/crawler:latest jruby \
              bin/crawler crawl /config/crawl-config-final.yml
          EOF<p>Diese Aktion führt jedes Mal die folgenden Schritte aus, wenn wir Änderungen an der Crawler-Konfigurationsdatei vornehmen:</p><ol><li><p>Tragen Sie den Elasticsearch-Host und den API-Schlüssel in die YAML-Konfiguration ein.</p></li><li><p>Kopieren Sie den Konfigurationsordner auf unsere VM</p></li><li><p>Stellen Sie über SSH eine Verbindung zu unserer VM her.</p></li><li><p>Führen Sie den Crawl mit der Konfiguration aus, die wir gerade aus dem Repository kopiert haben.</p></li></ol><h2>Lokale Tests</h2><p>Um unseren Crawler lokal zu testen, haben wir ein Bash-Skript erstellt, das den Elasticsearch-Host mit dem lokalen Docker-Repository befüllt und einen Crawl startet. Sie können <code>./local.sh</code> eingeben, um es auszuführen.</p>#!/bin/bash

# Exit on any error
set -e

# Load environment variables
export ES_HOST="http://es01:9200"

# Generate final crawler config
envsubst &lt; ./config/crawler-config.yml &gt; ./config/crawl-config-final.yml

# Bring everything up
docker compose up --build<p>Schauen wir uns die Kibana DevTools an, um zu bestätigen, dass<code> web-crawler-index</code> korrekt befüllt wurde:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt989660368fe14db8/6a17ef9b9da390c79ce46562/18551635e8265866e389a9632c4e4540958e4468-990x723.png" alt="Kibana DevTools-Code zur Bestätigung, dass der Webcrawler-Index korrekt eingerichtet ist." /><h2>Bereitstellung in der Produktionsumgebung</h2><p>Jetzt sind wir bereit, die Änderungen auf den Hauptzweig zu übertragen. Dadurch wird der Crawler in Ihrer virtuellen Maschine bereitgestellt und beginnt, Protokolle an Ihre Serverless Elasticsearch-Instanz zu senden.</p>git add .
git commit -m "First commit"
git push<p>Dadurch wird die GitHub-Aktion ausgelöst, die das Bereitstellungsskript innerhalb der virtuellen Maschine ausführt und mit dem Crawling beginnt.</p><p>Sie können die Ausführung der Aktion überprüfen, indem Sie zum GitHub-Repository gehen und den Tab „Aktionen“ aufrufen:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt986f1a4e4f3288d2/6a17ef9c7f6f1584fdc09c10/67ba3a7164d7a8049fe5661264820826cb18ed64-667x325.png" alt="Die Aktionen werden im EC2-Tab eines GitHub-Repositorys bereitgestellt." /><h2>Änderungen vornehmen und erneut bereitstellen</h2><p>Vielleicht ist Ihnen aufgefallen, dass die <code>price</code> jedes Produkts Teil des Body-Felds des Dokuments ist. Ideal wäre es, den Preis in einem separaten Feld zu speichern, damit wir Filter darauf anwenden können.</p><p>Fügen wir diese Änderung zur Datei <code>crawler.yml</code> hinzu, um <a href="https://github.com/elastic/crawler/blob/main/docs/features/EXTRACTION_RULES.md">Extraktionsregeln</a> zu verwenden, mit denen der Preis aus der CSS-Klasse <code>product-price</code> extrahiert werden kann:</p>output_sink: elasticsearch
output_index: web-crawl-index
max_crawl_depth: 1

elasticsearch:
  host: ${ES_HOST}
  api_key: ${ES_API_KEY}
     
  # Index ingest pipeline to process documents before indexing          
  pipeline_enabled: true
  pipeline: pricing-pipeline

domains:
  - url: https://web-scraping.dev
    seed_urls:
      - https://web-scraping.dev/product/1
      - https://web-scraping.dev/product/2
      - https://web-scraping.dev/product/3
    extraction_rulesets:
      - url_filters:
          - type: ends
            pattern: /product/*
        rules:
          - action: extract
            field_name: price
            selector: .product-price
            join_as: string
            source: html<p>Wir sehen auch, dass der Preis ein Dollarzeichen (<code>$</code>) enthält, das wir entfernen müssen, wenn wir Bereichsabfragen ausführen wollen. Dafür können wir eine Ingest-Pipeline verwenden. Beachten Sie, dass wir in unserer neuen Crawler-Konfigurationsdatei oben darauf verweisen:</p>PUT _ingest/pipeline/pricing-pipeline
{
  "processors": [
    {
      "script": {
        "source": """
                ctx['price'] = ctx['price'].replace("$","")
            """
      }
    }
  ]
}<p>Wir können diesen Befehl in unserem Elasticsearch-Produktionscluster ausführen. Da die Entwicklungsversion ephemer ist, können wir die Pipeline-Erstellung in die <code>docker-compose.yml</code> -Datei integrieren, indem wir den folgenden Dienst hinzufügen. Beachten Sie, dass wir dem Crawler-Dienst auch eine <code>depends_on</code> hinzugefügt haben, damit er erst startet, nachdem die Pipeline erfolgreich erstellt wurde.</p> crawler:
    image: docker.elastic.co/integrations/crawler:0.4.2
    environment:
      - ES_HOST=http://es01:9200
      - CRAWLER_JRUBY_OPTS=--server
    container_name: crawler
    volumes:
      - ./config:/home/app/config
    networks: [esnet]
    entrypoint: ["/home/app/bin/crawler", "crawl", "/home/app/config/crawl-config-final.yml"]
    depends_on:
      pipeline-init:
        condition: service_completed_successfully
    stdin_open: true
    tty: true  


  pipeline-init:
    image: curlimages/curl:latest
    depends_on:
      es01:
        condition: service_healthy
    networks: [esnet]
    entrypoint: &gt;
        sh -c "
        echo 'Creating ingest pipeline...';
        curl -s -X PUT http://es01:9200/_ingest/pipeline/pricing-pipeline \\
          -H 'Content-Type: application/json' \\
          -d '{\"processors\":[{\"script\":{\"source\":\"ctx.price = ctx.price.replace(\\\"$\\\", \\\"\\\")\"}}]}';
        echo 'Pipeline created!';
        "<p>Führen wir nun <code>`./local.sh`</code> aus, um die Änderung lokal zu sehen:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0f390aedf67cb4fe/6a17ef9eaf47b62bd2cde05b/dc1801599344a9f69f072b07ff828c4ba3815d7b-738x473.png" alt="Führen Sie `./local.sh` aus, um die Preisänderung lokal zu sehen." /><p>Großartig! Jetzt lasst uns die Veränderung vorantreiben:</p>git add crawler-config.yml
git commit -m "added price CSS selector"
git push<p>Um sicherzustellen, dass alles funktioniert, können Sie Ihre Produktions-Kibana-Datei überprüfen. Dort sollten die Änderungen sichtbar sein und der Preis als neues Feld ohne Dollarzeichen angezeigt werden.</p><h2>Fazit</h2><p>Mit dem Elastic Open Web Crawler können Sie Ihren Crawler als Code verwalten. Das bedeutet, dass Sie die gesamte Pipeline – von der Entwicklung bis zur Bereitstellung – automatisieren und beispielsweise ephemere lokale Umgebungen und Tests anhand der gecrawlten Daten programmatisch hinzufügen können.</p><p>Sie sind eingeladen, das offizielle Repository zu klonen und mit der Indizierung Ihrer eigenen Daten mithilfe dieses Workflows zu beginnen. In <a href="https://www.elastic.co/search-labs/blog/semantic-search-open-crawler">diesem Artikel</a> erfahren Sie auch, wie Sie eine semantische Suche auf den vom Crawler erzeugten Indizes durchführen.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elastic-open-crawler-config-as-code</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elastic-open-crawler-config-as-code</guid>
    <category><![CDATA[Index-Daten]]></category>
    <category><![CDATA[Operativer Betrieb]]></category>
    <dc:creator><![CDATA[Gustavo Llermaly]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt00e7c95012dc38cc/6a17efa0fbc5f80dad491b93/0ac41f55c85ad3f647cb0e0d750ed80bacd397f3-1036x581.png" length="0" type="image/png"/>
    <pubDate>Mon, 22 Sep 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Erstellen einer Such-App mit Blazor und Elasticsearch]]></title>
    <description><![CDATA[Lernen Sie, wie Sie mit Blazor und Elasticsearch eine Suchanwendung erstellen und wie Sie den Elasticsearch .NET-Client für die hybride Suche verwenden.]]></description>
    <content:encoded><![CDATA[<p>In diesem Artikel erfahren Sie, wie Sie Ihre C#-Kenntnisse nutzen können, um eine Suchanwendung mit Blazor und Elasticsearch zu erstellen. Wir werden den <a href="https://www.elastic.co/guide/en/elasticsearch/client/net-api/current/introduction.html">Elasticsearch .NET-</a> Client verwenden, um <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/full-text-queries.html">Volltext-</a>, <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-search.html">semantische</a> und <a href="https://www.elastic.co/search-labs/tutorials/search-tutorial/vector-search/hybrid-search">hybride</a> Suchabfragen durchzuführen.</p><p><strong>HINWEIS:</strong> Wenn Sie mit der älteren Version des Elasticsearch C#-Clients <a href="https://www.elastic.co/guide/en/elasticsearch/client/net-api/7.17/nest.html">NEST</a> vertraut sind, lesen Sie diesen <a href="https://www.elastic.co/search-labs/blog/net-client-evolution">Blogbeitrag</a> über die Einstellung des NEST-Clients und neue Funktionen. <em>NEST war die vorherige Generation des .NET-Clients, der durch den .NET-Client ersetzt wurde.</em><code>Elastic.Clients.Elasticsearch package</code></p><p></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltca8d1da68641fac0/6a17f65763173044d4585bfb/18f890286ca122cd97286c09ebf740208b802d0b-650x395.png" alt="Erstellen Sie eine Blazor-App: Esre mit Blazor-Diagramm" /><ul><li><p><a href="https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor#what-is-blazor?">Was ist Blazor?</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor#what-is-esre?">Was ist ESRE?</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor#configuring-elser">ELSER konfigurieren</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor#indexing-data">Indexierungsdaten</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor#building-the-app-with-blazor-&amp;-elasticsearch">Gebäudeanwendung</a></p></li></ul><h2>Was ist Blazor?</h2><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt91278ae422883e28/6a17f6592f4a5c3213fa8a95/622741915d016b68bf94f742332d10d736d60052-707x461.png" alt="Blazor-Server" /><p><a href="https://dotnet.microsoft.com/en-us/apps/aspnet/web-apps/blazor">Blazor</a> ist ein Open-Source-Webframework auf Basis von HTML, CSS und C#, das von Microsoft entwickelt wurde, um Entwicklern die Erstellung von Webanwendungen zu ermöglichen, die auf dem Client oder dem Server ausgeführt werden können. Blazor ermöglicht es Ihnen außerdem, wiederverwendbare Komponenten zu erstellen, um Anwendungen schneller zu entwickeln; es ermöglicht Entwicklern, die HTML-Ansicht und Aktionen in C# innerhalb derselben Datei zu erstellen, was dazu beiträgt, lesbaren und sauberen Code zu erhalten. Mit Blazor Hybrid können Sie außerdem native mobile Apps erstellen, die über .NET-Code auf die Funktionen der nativen Plattform zugreifen.</p><p>Einige der Merkmale, die Blazor zu einem großartigen Framework machen:</p><ul><li><p>Serverseitige und clientseitige Rendering-Optionen</p></li><li><p>Wiederverwendbare UI-Komponenten</p></li><li><p>Echtzeit-Updates mit SignalR</p></li><li><p>Integrierte Zustandsverwaltung</p></li><li><p>Eingebautes Routingsystem</p></li><li><p>Strenge Typisierung und Kompilierzeitprüfungen</p></li></ul><h3>Warum Blazer?</h3><p>Blazor bietet gegenüber anderen Frameworks und Bibliotheken mehrere Vorteile: Es ermöglicht Entwicklern, C# sowohl für Client- als auch für Servercode zu verwenden, bietet eine starke Typisierung und Kompilierzeitprüfung, was die Zuverlässigkeit erhöht. Es integriert sich nahtlos in das .NET-Ökosystem, ermöglicht die Wiederverwendung von .NET-Bibliotheken und -Tools und bietet robuste Unterstützung beim Debuggen.</p><h2>Was ist ESRE?</h2><p><a href="https://www.elastic.co/elasticsearch/elasticsearch-relevance-engine">Elasticsearch Relevance Engine ™ (ESRE)</a> ist eine Sammlung von <a href="https://www.elastic.co/guide/en/esre/current/learn.html">Tools zum Erstellen von Suchanwendungen</a> mithilfe von maschinellem Lernen und künstlicher Intelligenz auf Basis der leistungsstarken Elasticsearch-Suchmaschine.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf313ba986de01b93/6a17d851505ac306fcad8975/4c0f2645ed1c27fe3ef61a1a9126adadfd8d5368-721x421.png" alt="esre" /><p>Um mehr über ESRE zu erfahren, können Sie unseren aufschlussreichen Blogbeitrag <a href="https://www.elastic.co/search-labs/blog/introducing-elasticsearch-relevance-engine-esre">hier</a>lesen.</p><h2>ELSER konfigurieren</h2><p>Um <a href="https://www.elastic.co/elasticsearch/elasticsearch-relevance-engine">die ESRE-</a> Funktionen von Elastic optimal zu nutzen, werden wir <a href="https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-elser.html">ELSER</a> als unseren Modellanbieter verwenden.</p><p><em>Hinweis: Für die Nutzung der ELSER-Modelle von Elasticsearch benötigen Sie eine Platinum- oder Enterprise-Lizenz sowie einen dedizierten Machine-Learning-Knoten (ML-Knoten) mit mindestens 4 GB Speicherplatz. Weitere Informationen finden Sie</em> <a href="https://www.elastic.co/guide/en/machine-learning/8.15/ml-nlp-elser.html#elser-req"><em>hier.</em></a></p><p>Beginnen Sie mit der Erstellung des Inferenzendpunkts:</p>PUT _inference/sparse_embedding/my-elser-model
{
  "service": "elser",
  "service_settings": {
    "num_allocations": 1,
    "num_threads": 1
  }
}<p>Wenn Sie ELSER zum ersten Mal verwenden, kann beim Laden des Modells im Hintergrund ein 502 Bad Gateway-Fehler auftreten. Den Status des Modells können Sie in <code>Machine Learning &gt; Trained Models</code> in Kibana überprüfen. Sobald die Bereitstellung abgeschlossen ist, können Sie mit dem nächsten Schritt fortfahren.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltee295a3516338302/6a17f65b414c640deb94531d/a7ff94b72d337cde892165d744b9f42fba702a87-1440x649.png" alt="Überprüfung trainierter Modelle" /><h2>Indexierungsdaten</h2><p>Sie können den Datensatz <a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/esre-with-blazor/books.zip">hier</a> herunterladen und die Daten anschließend mit Kibana importieren. Gehen Sie dazu auf die Startseite und klicken Sie auf „Daten hochladen“. Laden Sie anschließend die Datei hoch und klicken Sie auf <code>Import</code>. Wechseln Sie abschließend zum Reiter <code>Advanced</code> und fügen Sie die folgenden Zuordnungen ein:</p>{
   "properties":{
      "authors":{
         "type":"keyword"
      },
      "categories":{
         "type":"keyword"
      },
      "longDescription":{
         "type":"semantic_text",
         "inference_id":"my-elser-model",
         "model_settings":{
            "task_type":"sparse_embedding"
         }
      },
      "pageCount":{
         "type":"integer"
      },
      "publishedDate":{
         "type":"date"
      },
      "shortDescription":{
         "type":"text"
      },
      "status":{
         "type":"keyword"
      },
      "thumbnailUrl":{
         "type":"keyword"
      },
      "title":{
         "type":"text"
      }
   }
}<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0f07300647edd2ca/6a17f65dfaa913172a93ca0c/1aea0b9c51e275f89339f5d463fcaef799fc3943-1235x1083.png" alt="Importdaten" /><p>Wir werden einen Index erstellen, der semantische und Volltextabfragen ausführen kann. Der Feldtyp <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-text.html">semantic_text</a> kümmert sich um die Datensegmentierung und -einbettung. <em>Beachten Sie, dass wir als indizieren</em> <em><code>longDescription</code></em>  <em><code>semantic_text</code></em><em>. Sie können copy_to verwenden, wenn Sie ein Feld sowohl als als</em> <em><code>semantic_text</code></em> <em>auch als text` indizieren</em> möchten .</p><h2>Entwicklung der App mit Blazor &amp; Elasticsearch</h2><h3>API-Schlüssel</h3><p>Als Erstes müssen wir einen API-Schlüssel erstellen, um unsere Anfragen an Elasticsearch zu authentifizieren. Der API-Schlüssel sollte schreibgeschützt sein und nur Abfragen des Index <code>books-blazor</code> erlauben.</p>POST /_security/api_key
{
  "name": "books-blazor-key",
  "role_descriptors": {
    "books-blazor-reader": {
      "indices": [
        {
          "names": ["books-blazor"],
          "privileges": ["read"]
        }
      ]
    }
  }
}<p>Sie werden etwa Folgendes sehen:
</p>{
  "id": "XXXXXXXXXXXXXXXXXXXXXXXX",
  "name": "books-blazor-key",
  "api_key": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  "encoded": "XXXXXXXXXXXXXXXXXXXXXXXX=="
}<p>Speichern Sie den Wert des Antwortfelds <code>encoded</code> , da er später benötigt wird. Wenn Sie <a href="https://www.elastic.co/cloud/">Elastic Cloud</a> nutzen, benötigen Sie außerdem Ihre Cloud-ID. (Sie finden sie <a href="https://www.elastic.co/search-labs/tutorials/install-elasticsearch/elastic-cloud#finding-your-cloud-id">hier</a>).</p><h4>Das Blazor-Projekt wird erstellt</h4><p>Beginnen Sie mit der Installation von Blazor und der Erstellung eines Beispielprojekts gemäß den <a href="https://dotnet.microsoft.com/en-us/learn/aspnet/blazor-tutorial/install">offiziellen Anweisungen</a>.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb5c20b896fe1e98e/6a17f65f6317301566585bff/f5720867d960c91fd1b3c4ae9be174e062e6b2fc-975x830.png" alt="Blazor-Tutorial – Erstellen des Blazor-Projekts" /><p>Sobald das Projekt erstellt ist, sollte die Ordnerstruktur und die Dateistruktur wie folgt aussehen:</p>BlazorApp/
|-- BlazorApp.csproj
|-- BlazorApp.sln
|-- Program.cs
|-- appsettings.Development.json
|-- appsettings.json
|-- Properties/
|   `-- launchSettings.json
|-- Components/
|   |-- App.razor
|   |-- Routes.razor
|   |-- _Imports.razor
|   |-- Layout/
|   |   |-- MainLayout.razor
|   |   |-- MainLayout.razor.css
|   |   |-- NavMenu.razor
|   |   `-- NavMenu.razor.css
|   `-- Pages/
|       |-- Counter.razor
|       |-- Error.razor
|       |-- Home.razor
|       `-- Weather.razor
|-- wwwroot/
|-- bin/
`-- obj/ <p>Die Template-Anwendung enthält <a href="https://blog.getbootstrap.com/2021/08/04/bootstrap-5-1-0/">Bootstrap v5.1.0.</a> zum Styling.</p><p>Schließen Sie die Projekteinrichtung ab, indem Sie <a href="https://www.elastic.co/guide/en/elasticsearch/client/net-api/8.0/installation.html">den Elasticsearch .NET</a> -Client installieren:</p>dotnet add package Elastic.Clients.Elasticsearch<p>Sobald Sie diesen Schritt abgeschlossen haben, sollte Ihre Seite wie folgt aussehen:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3b90e394ded7ff56/6a17f660faa913d49f93ca10/ef9d2186c2cf49e78a307c4aa69c51632e84578d-940x529.png" alt="Blazor Hallo Welt" /><h3>Ordnerstruktur</h3><p>Nun werden wir unsere Ordner wie folgt organisieren:</p>BlazorApp/
|-- Components/
|   |-- Pages/
|   |   |-- Search.razor
|   |   `-- Search.razor.css
|   `-- Elasticsearch/
|       |-- SearchBar.razor
|       |-- Results.razor
|       `-- Facet.razor
|-- Models/
|   |-- Book.cs
|   `-- Response.cs
`-- Services/
    `-- ElasticsearchService.cs<p>Dateitypen erklärt:</p><ul><li><p>Components/Pages/Search.razor: Hauptseite mit Suchleiste, Suchergebnissen und Filtern.</p></li><li><p>Components/Pages/Search.razor.css: Seitenstile.</p></li><li><p>Components/Elasticsearch/SearchBar.razor: Suchleistenkomponente.</p></li><li><p>Components/Elasticsearch/Results.razor: Ergebniskomponente.</p></li><li><p>Components/Elasticsearch/Facet.razor: Filterkomponente.</p></li><li><p>Components/Svg/GlassIcon.razor: Suchsymbol.</p></li><li><p>Components/_Imports.razor: Dadurch werden alle Komponenten importiert.</p></li><li><p>Models/Book.cs: Hier wird das Schema der Buchfelder gespeichert.</p></li><li><p>Models/Response.cs: Hier wird das Antwortschema gespeichert, einschließlich der Suchergebnisse, Facetten und der Gesamtzahl der Treffer.</p></li><li><p>Services/ElasticsearchService.cs: Elasticsearch-Dienst. Es übernimmt die Verbindung und die Abfragen an Elasticsearch.</p></li></ul><h4>Erstkonfiguration</h4><p>Fangen wir mit Aufräumarbeiten an.</p><p>Löschen Sie die Dateien:</p><ul><li><p>Komponenten/Seiten/Counter.razor</p></li><li><p>Komponenten/Seiten/Wetter.razor</p></li><li><p>Komponenten/Seiten/Startseite.razor</p></li><li><p>Komponenten/Layout/NavMenu.razor</p></li><li><p>Components/Layout/NavMenu.razor.css</p></li></ul><p>Überprüfen Sie die Datei <code>/Components/_Imports.razor</code> . Sie sollten folgende Importe haben:</p>@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using BlazorApp
@using BlazorApp.Components<h4>Integration von Elastic in das Projekt</h4><p>Importieren wir nun die Elasticsearch-Komponenten:</p>@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using BlazorApp
@using BlazorApp.Components
@using BlazorApp.Components.Elasticsearch @* &lt;--- Add this line *@<p>Wir werden die Standard-Seitenleiste entfernen, um mehr Platz für unsere Anwendung zu schaffen, indem wir sie aus der Datei <code>/Components/Layout/MainLayout.razor</code> löschen:</p>@inherits LayoutComponentBase

&lt;div class="page"&gt;
    &lt;main&gt;
        &lt;article class="content"&gt;
            @Body
        &lt;/article&gt;
    &lt;/main&gt;
&lt;/div&gt;

&lt;div id="blazor-error-ui"&gt;
    An unhandled error has occurred.
    &lt;a href="" class="reload"&gt;Reload&lt;/a&gt;
    &lt;a class="dismiss"&gt;🗙&lt;/a&gt;
&lt;/div&gt;<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt441012c5aeac85f8/6a17f661ec0f8949f15a67ac/55bfff607f9167a532f83ad074b479364a346c6e-816x472.png" alt="Navigationsleiste aus dem Layout entfernt" /><p>Geben wir nun die Elasticsearch-Zugangsdaten für die <a href="https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-8.0&amp;tabs=linux#secret-manager">Benutzergeheimnisse</a> ein:</p>dotnet user-secrets init
dotnet user-secrets set ElasticsearchCloudId "your Cloud ID"
dotnet user-secrets set ElasticsearchApiKey "your API Key"<p>Bei diesem Ansatz speichert .NET 8 sensible Daten an einem separaten Ort außerhalb des Projektordners und ermöglicht den Zugriff über die <code>IConfiguration</code> -Schnittstelle. Diese Variablen stehen jedem .NET-Projekt zur Verfügung, das dieselben Benutzergeheimnisse verwendet.</p><p>Als Nächstes modifizieren wir die Datei <code>Program.cs</code> , um die Geheimnisse zu lesen und den Elasticsearch-Client einzubinden:</p><p>Zuerst müssen die benötigten Bibliotheken importiert werden:</p>using BlazorApp.Services;
using Elastic.Clients.Elasticsearch;
using Elastic.Transport;<ul><li><p>BlazorApp.Services: enthält den Elasticsearch-Dienst.</p></li><li><p>Elastic.Clients.Elasticsearch: importiert die Elasticsearch-Client-Bibliothek für .NET 8.</p></li><li><p>Elastic.Transport: importiert die Elasticsearch-Transportbibliothek, die es uns ermöglicht, die ApiKey-Klasse zur Authentifizierung unserer Anfragen zu verwenden.</p></li></ul><p>Zweitens fügen Sie den folgenden Code vor der Zeile <code>var app = builder.Build()</code> ein:</p>// Initialize the Elasticsearch client.
builder.Services.AddScoped(sp =&gt;
{
    // Getting access to the configuration service to read the Elasticsearch credentials.
    var configuration = sp.GetRequiredService&lt;IConfiguration&gt;();
    var cloudId = configuration["ElasticsearchCloudId"];
    var apiKey = configuration["ElasticsearchApiKey"];

    if (string.IsNullOrEmpty(cloudId) || string.IsNullOrEmpty(apiKey))
    {
        throw new InvalidOperationException(
            "Elasticsearch credentials are missing in configuration."
        );
    }

    var settings = new ElasticsearchClientSettings(cloudId, new ApiKey(apiKey)).EnableDebugMode();
    return new ElasticsearchClient(settings);
});<p>Dieser Code liest die Elasticsearch-Zugangsdaten aus den Benutzergeheimnissen und erstellt eine Elasticsearch-Clientinstanz.</p><p>Nach der Initialisierung des Elasticsearch-Clients fügen Sie die folgende Zeile hinzu, um den Elasticsearch-Dienst zu registrieren:</p>builder.Services.AddScoped&lt;ElasticsearchService&gt;();<p>Der nächste Schritt besteht darin, die Suchlogik in der Datei <code>/Services/ElasticsearchService.cs</code> zu implementieren:</p><p>Zuerst müssen die benötigten Bibliotheken und Modelle importiert werden:</p>using BlazorApp.Models;
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.QueryDsl;<p>Zweitens fügen Sie die Klasse <code>ElasticsearchService</code>, den Konstruktor und die Variablen hinzu:</p>namespace BlazorApp.Services
{
    public class ElasticsearchService
    {
        private readonly ElasticsearchClient _client;

        // The logger is used to log information, warnings and errors about the Elasticsearch service and requests.
        private readonly ILogger&lt;ElasticsearchService&gt; _logger;

        public ElasticsearchService(
            ElasticsearchClient client,
            ILogger&lt;ElasticsearchService&gt; logger
        )
        {
            _client = client ?? throw new ArgumentNullException(nameof(client));
            _logger = logger;
        }
    }
}<h4>Suche konfigurieren</h4><p>Nun erstellen wir unsere Suchlogik:</p>private static Action&lt;RetrieverDescriptor&lt;BookDoc&gt;&gt; BuildHybridQuery(
    string searchTerm,
    Dictionary&lt;string, List&lt;string&gt;&gt; selectedFacets
)
{
    var filters = BuildFilters(selectedFacets);

    return retrievers =&gt;
        retrievers.Rrf(rrf =&gt;
            rrf.RankWindowSize(50)
                .RankConstant(20)
                .Retrievers(
                    retrievers =&gt;
                        retrievers.Standard(std =&gt;
                            std.Query(q =&gt;
                                q.Bool(b =&gt;
                                    b.Must(m =&gt;
                                            m.MultiMatch(mm =&gt;
                                                mm.Query(searchTerm)
                                                    .Fields(
                                                        new[]
                                                        {
                                                            "title",
                                                            "shortDescription",
                                                        }
                                                    )
                                            )
                                        )
                                        .Filter(filters.ToArray())
                                )
                            )
                        ),
                    retrievers =&gt;
                        retrievers.Standard(std =&gt;
                            std.Query(q =&gt;
                                q.Bool(b =&gt;
                                    b.Must(m =&gt;
                                            m.Semantic(sem =&gt;
                                                sem.Field("longDescription")
                                                    .Query(searchTerm)
                                            )
                                        )
                                        .Filter(filters.ToArray())
                                )
                            )
                        )
                )
        );
}

public static List&lt;Action&lt;QueryDescriptor&lt;BookDoc&gt;&gt;&gt; BuildFilters(
    Dictionary&lt;string, List&lt;string&gt;&gt; selectedFacets
)
{
    var filters = new List&lt;Action&lt;QueryDescriptor&lt;BookDoc&gt;&gt;&gt;();

    if (selectedFacets != null)
    {
        foreach (var facet in selectedFacets)
        {
            foreach (var value in facet.Value)
            {
                var field = facet.Key.ToLower();
                if (!string.IsNullOrEmpty(field))
                {
                    filters.Add(m =&gt; m.Term(t =&gt; t.Field(new Field(field)).Value(value)));
                }
            }
        }
    }

    return filters;
}<ul><li><p><code>BuildFilters</code> wird die Filter für die Suchanfrage anhand der vom Benutzer ausgewählten Facetten erstellen.</p></li><li><p><code>BuildHybridQuery</code> wird eine <a href="https://www.elastic.co/search-labs/tutorials/search-tutorial/vector-search/hybrid-search">hybride Suchanfrage</a> erstellen, die Volltext- und semantische Suche kombiniert.</p></li></ul><p>Als Nächstes fügen Sie die Suchmethode hinzu:</p>public async Task&lt;ElasticResponse&gt; SearchBooksAsync(
    string searchTerm,
    Dictionary&lt;string, List&lt;string&gt;&gt; selectedFacets
)
{
    try
    {
        _logger.LogInformation($"Performing search for: {searchTerm}");

        // Retrieve the hybrid query with filters applied.
        var retrieverQuery = BuildHybridQuery(searchTerm, selectedFacets);

        var response = await _client.SearchAsync&lt;BookDoc&gt;(s =&gt;
            s.Index("elastic-blazor-books")
                .Retriever(retrieverQuery)
                .Aggregations(aggs =&gt;
                    aggs.Add("Authors", agg =&gt; agg.Terms(t =&gt; t.Field(p =&gt; p.Authors)))
                        .Add(
                            "Categories",
                            agg =&gt; agg.Terms(t =&gt; t.Field(p =&gt; p.Categories))
                        )
                        .Add("Status", agg =&gt; agg.Terms(t =&gt; t.Field(p =&gt; p.Status)))
                )
        );

        if (response.IsValidResponse)
        {
            _logger.LogInformation($"Found {response.Documents.Count} documents");

            var hits = response.Total;
            var facets =
                response.Aggregations != null
                    ? FormatFacets(response.Aggregations)
                    : new Dictionary&lt;string, Dictionary&lt;string, long&gt;&gt;();

            var elasticResponse = new ElasticResponse
            {
                TotalHits = hits,
                Documents = response.Documents.ToList(),
                Facets = facets,
            };

            return elasticResponse;
        }
        else
        {
            _logger.LogWarning($"Invalid response: {response.DebugInformation}");
            return new ElasticResponse();
        }
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Error performing search");
        return new ElasticResponse();
    }
}

public static Dictionary&lt;string, Dictionary&lt;string, long&gt;&gt; FormatFacets(
    Elastic.Clients.Elasticsearch.Aggregations.AggregateDictionary aggregations
)
{
    var facets = new Dictionary&lt;string, Dictionary&lt;string, long&gt;&gt;();

    foreach (var aggregation in aggregations)
    {
        if (
            aggregation.Value
            is Elastic.Clients.Elasticsearch.Aggregations.StringTermsAggregate termsAggregate
        )
        {
            var facetName = aggregation.Key;
            var facetDictionary = ConvertFacetDictionary(
                termsAggregate.Buckets.ToDictionary(b =&gt; b.Key, b =&gt; b.DocCount)
            );
            facets[facetName] = facetDictionary;
        }
    }

    return facets;
}

private static Dictionary&lt;string, long&gt; ConvertFacetDictionary(
    Dictionary&lt;Elastic.Clients.Elasticsearch.FieldValue, long&gt; original
)
{
    var result = new Dictionary&lt;string, long&gt;();
    foreach (var kvp in original)
    {
        result[kvp.Key.ToString()] = kvp.Value;
    }
    return result;
}<ul><li><p><code>SearchBooksAsync</code>: führt die Suche mit Hilfe der Hybridabfrage durch und gibt die Ergebnisse einschließlich der Aggregationen zum Aufbau der Facetten zurück.</p></li><li><p><code>FormatFacets</code>: formatiert die Aggregationsantwort in ein Wörterbuch.</p></li><li><p><code>ConvertFacetDictionary</code>: wird das Facettenwörterbuch in ein besser lesbares Format umwandeln.</p></li></ul><p>Im nächsten Schritt werden die Modelle erstellt, die die Daten repräsentieren, die in der <code>hits</code> der Elasticsearch-Abfrage zurückgegeben werden und als Ergebnisse auf unserer Suchseite angezeigt werden.</p><p>Wir beginnen damit, die Datei <code>/Models/Book.cs</code> zu erstellen und Folgendes hinzuzufügen:</p>namespace BlazorApp.Models
{
    public class BookDoc
    {
        public string? Title { get; set; }
        public int? PageCount { get; set; }
        public string? PublishedDate { get; set; }
        public string? ThumbnailUrl { get; set; }
        public string? ShortDescription { get; set; }
        public LongDescription? LongDescription { get; set; }
        public string? Status { get; set; }
        public List&lt;string&gt;? Authors { get; set; }
        public List&lt;string&gt;? Categories { get; set; }
    }

    public class LongDescription
    {
        public string? Text { get; set; }
    }
}<p>Anschließend wird die Elastic-Antwort in der Datei <code>/Models/Response.cs</code> eingerichtet und Folgendes hinzugefügt:</p>namespace BlazorApp.Models
{
    public class ElasticResponse
    {
        public ElasticResponse()
        {
            Documents = new List&lt;BookDoc&gt;();
            Facets = new Dictionary&lt;string, Dictionary&lt;string, long&gt;&gt;();
        }

        public long TotalHits { get; set; }
        public List&lt;BookDoc&gt; Documents { get; set; }
        public Dictionary&lt;string, Dictionary&lt;string, long&gt;&gt; Facets { get; set; }
    }
}<h4>Konfigurieren einer grundlegenden Benutzeroberfläche</h4><p>Als Nächstes fügen Sie die Komponente SearchBar hinzu. Fügen Sie in der Datei <code>/Components/Elasticsearch/SearchBar.razor</code> Folgendes hinzu:</p>@using System.Threading.Tasks

&lt;form @onsubmit="SubmitSearch"&gt;
  &lt;div class="input-group mb-3"&gt;
    &lt;input type="text" @bind-value="searchTerm" class="form-control" placeholder="Enter search term..." /&gt;
    &lt;button type="submit" class="btn btn-primary input-btn"&gt;
      &lt;span class="input-group-svg"&gt;
        Search
      &lt;/span&gt;
    &lt;/button&gt;
  &lt;/div&gt;
&lt;/form&gt;

@code {
  [Parameter]
  public EventCallback&lt;string&gt; OnSearch { get; set; }

  private string searchTerm = "";

  private async Task SubmitSearch()
  {
    await OnSearch.InvokeAsync(searchTerm);
  }
}<p>Diese Komponente enthält eine Suchleiste und eine Schaltfläche zur Durchführung der Suche.</p><p>Blazor bietet große Flexibilität, da es die dynamische Generierung von HTML mithilfe von C#-Code innerhalb derselben Datei ermöglicht.</p><p>Anschließend erstellen wir in der Datei <code>/Components/Elasticsearch/Results.razor</code> die Ergebniskomponente, die die Suchergebnisse anzeigt:</p>@using BlazorApp.Models

@if (SearchResults != null &amp;&amp; SearchResults.Any())
{
  &lt;div class="row"&gt;
  @foreach (var result in SearchResults)
    {
      &lt;div class="col-12 mb-3"&gt;
        &lt;div class="card"&gt;
          &lt;div class="row g-0"&gt;
            &lt;div class="col-md-3 image-container"&gt;
              @if (!string.IsNullOrEmpty(result?.ThumbnailUrl))
              {
                &lt;img src="@result?.ThumbnailUrl" class="img-fluid rounded-start" alt="Thumbnail"&gt;
              }
              else
              {
                &lt;div class="placeholder"&gt;
                  @result?.Title
                &lt;/div&gt;
              }
            &lt;/div&gt;

            &lt;div class="col-md-9"&gt; &lt;!-- Adjusted to use the remaining 75% --&gt;
              &lt;div class="card-body"&gt;
                &lt;h4 class="card-title"&gt;
                  @result?.Title
                &lt;/h4&gt;

                &lt;div class="details-container"&gt;
                  &lt;div class=""&gt;

                    @if (result?.Authors?.Any() == true)
                    {
                      &lt;p class="card-text p-first"&gt;
                        Authors: &lt;small class="text-muted"&gt;@string.Join(", ", result.Authors)&lt;/small&gt;
                      &lt;/p&gt;
                    }

                    @if (result?.Categories?.Any() == true)
                    {
                      &lt;p class="card-text p-second"&gt;
                        Categories: &lt;small class="text-muted"&gt;@string.Join(", ", result.Categories)&lt;/small&gt;
                      &lt;/p&gt;
                    }
                  &lt;/div&gt;
                  &lt;div class="numPages-status"&gt;
                    @if (result?.PageCount != null)
                    {
                      &lt;p class="card-text p-first"&gt;
                        Pages: &lt;small class="text-muted"&gt;@result.PageCount&lt;/small&gt;
                      &lt;/p&gt;
                    }

                    @if (result?.Status != null)
                    {
                      &lt;p class="card-text p-second"&gt;
                        Status: &lt;small class="text-muted"&gt;@result.Status&lt;/small&gt;
                      &lt;/p&gt;
                    }
                  &lt;/div&gt;
                &lt;/div&gt;

                &lt;div class="long-text-container"&gt;
                  &lt;p class="card-text"&gt;&lt;small class="text-muted"&gt;@result?.LongDescription?.Text&lt;/small&gt;&lt;/p&gt;
                &lt;/div&gt;
                @if (!string.IsNullOrEmpty(result?.PublishedDate))
                {
                  &lt;div class="date-container"&gt;
                    &lt;p class="card-text"&gt;
                      Published Date: &lt;small class="text-muted small-date"&gt;@FormatDate(result.PublishedDate)&lt;/small&gt;
                    &lt;/p&gt;
                  &lt;/div&gt;
                }
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    }
  &lt;/div&gt;
}
else if (SearchResults != null)
{
  &lt;p&gt;No results found.&lt;/p&gt;
}

@code {
  [Parameter]
  public List&lt;BookDoc&gt; SearchResults { get; set; } = new List&lt;BookDoc&gt;();

  private string FormatDate(string? date)
  {
    if (DateTime.TryParse(date, out DateTime parsedDate))
    {
      return parsedDate.ToString("MMMM dd, yyyy");
    }
    return "";
  }
}<p>Abschließend müssen wir noch Facetten erstellen, um die Suchergebnisse zu filtern.</p><p><em>Hinweis: Facetten sind Filter, mit denen Nutzer Suchergebnisse anhand bestimmter Attribute oder Kategorien wie Produkttyp, Preisspanne oder Marke eingrenzen können. Diese Filter werden üblicherweise als anklickbare Optionen, oft in Form von Kontrollkästchen, dargestellt, um Nutzern die Suche zu erleichtern und relevante Ergebnisse schneller zu finden. In Elasticsearch werden Facetten mithilfe von</em> <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html"><em>Aggregationen</em></a>erstellt<em>.</em></p><p>Wir richten Facetten ein, indem wir den folgenden Code in die Datei <code>/Components/Elasticsearch/Facet.razor</code> einfügen:</p>@if (Facets != null)
{
  &lt;div class="facets-container"&gt;
  @foreach (var facet in Facets)
    {
      &lt;h3&gt;@facet.Key&lt;/h3&gt;
      @foreach (var option in facet.Value)
      {
        &lt;div&gt;
          &lt;input type="checkbox" checked="@IsFacetSelected(facet.Key, option.Key)"
            @onclick="() =&gt; ToggleFacet(facet.Key, option.Key)" /&gt;
          @option.Key (@option.Value)
        &lt;/div&gt;
      }
    }
  &lt;/div&gt;
}


@code {
  [Parameter]
  public Dictionary&lt;string, Dictionary&lt;string, long&gt;&gt;? Facets { get; set; }

  [Parameter]
  public EventCallback&lt;Dictionary&lt;string, List&lt;string&gt;&gt;&gt; OnFacetChanged { get; set; }

  private Dictionary&lt;string, List&lt;string&gt;&gt; selectedFacets = new();

  private void ToggleFacet(string facetName, string facetValue)
  {
    if (!selectedFacets.TryGetValue(facetName, out var facetValues))
    {
      facetValues = selectedFacets[facetName] = new List&lt;string&gt;();
    }

    if (!facetValues.Remove(facetValue))
    {
      facetValues.Add(facetValue);
    }

    OnFacetChanged.InvokeAsync(selectedFacets);
  }

  private bool IsFacetSelected(string facetName, string facetValue)
  {
    return selectedFacets.ContainsKey(facetName) &amp;&amp; selectedFacets[facetName].Contains(facetValue);
  }
}<p>Diese Komponente liest Daten aus einer <code>terms</code> -Aggregation der Felder <code>author</code>, <code>categories</code> und <code>status</code> und erzeugt anschließend eine Liste von Filtern, die an Elasticsearch zurückgesendet werden.</p><p>Nun fügen wir alles zusammen.</p><p>In der Datei <code>/Components/Pages/Search.razor</code> :</p>@page "/"
@rendermode InteractiveServer
@using BlazorApp.Models
@using BlazorApp.Services
@inject ElasticsearchService ElasticsearchService
@inject ILogger&lt;Search&gt; Logger

&lt;PageTitle&gt;Search&lt;/PageTitle&gt;

&lt;div class="top-row px-4 "&gt;

    &lt;div class="searchbar-container"&gt;
        &lt;h4&gt;Semantic Search with Elasticsearch and Blazor&lt;/h4&gt;

        &lt;SearchBar OnSearch="PerformSearch" /&gt;
    &lt;/div&gt;

    &lt;a href="https://www.elastic.co/search-labs/esre-with-blazor" target="_blank"&gt;About&lt;/a&gt;
&lt;/div&gt;

&lt;div class="px-4"&gt;

    &lt;div class="search-details-container"&gt;
        &lt;p role="status"&gt;Current search term: @currentSearchTerm&lt;/p&gt;
        &lt;p role="status"&gt;Total results: @totalResults&lt;/p&gt;
    &lt;/div&gt;

    &lt;div class="results-facet-container"&gt;
        &lt;div class="facets-container"&gt;
            &lt;Facet Facets="facets" OnFacetChanged="OnFacetChanged" /&gt;
        &lt;/div&gt;
        &lt;div class="results-container"&gt;
            &lt;Results SearchResults="searchResults" /&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;

@code {
    private string currentSearchTerm = "";
    private long totalResults = 0;
    private List&lt;BookDoc&gt; searchResults = new List&lt;BookDoc&gt;();
    private Dictionary&lt;string, Dictionary&lt;string, long&gt;&gt; facets = new Dictionary&lt;string, Dictionary&lt;string, long&gt;&gt;();
    private Dictionary&lt;string, List&lt;string&gt;&gt; selectedFacets = new Dictionary&lt;string, List&lt;string&gt;&gt;();

    protected override async Task OnInitializedAsync()
    {
        await PerformSearch();
    }

    private async Task PerformSearch(string searchTerm = "")
    {
        try
        {
            currentSearchTerm = searchTerm;

            var response = await ElasticsearchService.SearchBooksAsync(currentSearchTerm, selectedFacets);
            if (response != null)
            {
                searchResults = response.Documents;
                facets = response.Facets;
                totalResults = response.TotalHits;
            }
            else
            {
                Logger.LogWarning("Search response is null.");
            }

            StateHasChanged();
        }
        catch (Exception ex)
        {
            Logger.LogError(ex, "Error performing search.");
        }
    }

    private async Task OnFacetChanged(Dictionary&lt;string, List&lt;string&gt;&gt; newSelectedFacets)
    {
        selectedFacets = newSelectedFacets;
        await PerformSearch(currentSearchTerm);
    }
}<p>Unsere Seite funktioniert!</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc750e55fff43de4b/6a17f6636864a4ab08b68931/5f0f25cb029a34df7f577a9f307278226ed07c3f-816x473.png" alt="Blazor-Seitenbeispiel" /><p>Wie Sie sehen können, ist die Seite funktional, aber es fehlen ihr die Stile. Fügen wir etwas CSS hinzu, damit es übersichtlicher und responsiver aussieht.</p><p>Fangen wir an, die Layoutstile zu ersetzen. In der Datei <code>Components/Layout/MainLayout.razor.css</code> :</p>.page {
  position: relative;
  display: flex;
  flex-direction: column;
}

main {
  flex: 1;
}

#blazor-error-ui {
  background: lightyellow;
  bottom: 0;
  box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
  display: none;
  left: 0;
  padding: 0.6rem 1.25rem 0.7rem 1.25rem;
  position: fixed;
  width: 100%;
  z-index: 1000;
}

#blazor-error-ui .dismiss {
  cursor: pointer;
  position: absolute;
  right: 0.75rem;
  top: 0.5rem;
}<p>Fügen Sie die Stile für die Suchseite in der Datei <code>Components/Pages/Search.razor.css</code> hinzu:</p>.input-group .input-group-svg {
  background: transparent;
  border: transparent;
  pointer-events: none;
}

.results-facet-container {
  display: flex;
  margin-top: 1rem;
  overflow-x: auto;
}

.search-details-container {
  display: flex;
  justify-content: space-between;
  margin-top: 1rem;
}

.searchbar-container {
  padding-top: 2rem;
  display: flex;
  flex-direction: column; 
  flex-grow: 1;
  height: 100%;
  max-width: 100%; 
}

.searchbar-container h4 {
  margin: 0;
}

.top-row {
  margin-top: -1.1rem;
  position: relative; 
  background-color: hsl(216, 29%, 67%);
  border-bottom: 1px solid #d6d5d5;
  display: flex;
  align-items: center;
  height: 100%;
  padding: 0 1rem;
}

.top-row a {
  margin-left: auto;
  margin-top: -4rem; 
  color: #000000;
  text-decoration: none;
}

.top-row a:hover {
  text-decoration: underline;
}

@media (max-width: 640.98px) {
  .top-row {
    justify-content: space-between;
  }

  .top-row ::deep a,
  .top-row ::deep .btn-link {
    margin-left: 0;
  }
}

@media (min-width: 641px) {
  .top-row.auth ::deep a:first-child {
    flex: 1;
    text-align: right;
    width: 0;
  }

  .top-row,
  article {
    padding-left: 2rem !important;
    padding-right: 1.5rem !important;
  }
}<p>Unsere Seite sieht jetzt besser aus:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3ecc49f404f18591/6a17f6654b055d959b432384/3dadd7ade2dd9070300a4e0514a4da2ae9cc9fb9-817x473.png" alt="Blazor-Seite nach dem Hinzufügen von Stilen für die Suchseite" /><p>Geben wir dem Ganzen noch den letzten Schliff:</p><p>Erstellen Sie die folgenden Dateien:</p><ul><li><p>Komponenten/Elasticsearch/Facet.razor.css</p></li><li><p>Components/Elasticsearch/Results.razor.css</p></li></ul><p>Und fügen Sie die Stile für <code>Facet.razor.css</code> hinzu:</p>.facets-container {
  font-size: 15px;
  margin-right: 4rem;
  overflow-x: auto;
  white-space: nowrap;
  max-width: 300px;
}

.results-facet-container {
  display: flex;
  margin-top: 1rem;
  overflow-x: auto;
}

.results-facet-container &gt; * {
  flex-shrink: 0;
}<p>Für <code>Results.razor.css</code>:</p>.image-container {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  padding: 1rem;
  box-sizing: border-box;
}

.image-container img {
  max-width: 100%;
  height: auto;
  border-radius: 0.5rem;
}

.placeholder {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  width: 100%;
  background-color: #f0f0f0;
  border: 1px solid #ccc;
  font-size: 0.9rem;
  color: #888;
  text-align: center;
  padding: 1rem;
  border-radius: 0.5rem;
}

.card-body {
  padding: 1rem;
}

.details-container {
  display: flex;
  justify-content: space-between;
  padding: 1.5rem 0;
}

.date-container {
  margin-top: 1rem;
  display: flex;
  justify-content: flex-end;
}

.date-container .small-date {
  font-weight: bold;
}<p>Endergebnis:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt78bbd8e6f9e23988/6a17f6664b055d2ed8432388/3e68ec4a38775fdfbeaa1a990e6a1e11dfb081d8-816x472.png" alt="Endergebnis der Erstellung der Blazor-App-Seite" /><p>Um die Anwendung auszuführen, können Sie folgenden Befehl verwenden:</p><p><code>dotnet watch</code></p><p>Du hast es geschafft! Sie können nun mithilfe der Suchleiste in Ihrem Elasticsearch-Index nach Büchern suchen und die Ergebnisse nach Autor, Kategorie und Status filtern.</p><h3>Durchführung der Volltext- und semantischen Suche</h3><p>Standardmäßig führt unsere App eine <a href="https://www.elastic.co/search-labs/tutorials/search-tutorial/vector-search/hybrid-search">hybride Suche</a> durch, die sowohl <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html">Volltext-</a> als auch <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-semantic-query.html">semantische Suche</a> nutzt. Sie können die Suchlogik ändern, indem Sie zwei separate Methoden erstellen, eine für die Volltextsuche und eine weitere für die semantische Suche, und dann eine Methode auswählen, um die Abfrage basierend auf der Eingabe des Benutzers zu erstellen.</p><p>Fügen Sie der Klasse <code>ElasticsearchService</code> in der Datei <code>/Services/ElasticsearchService.cs</code> die folgenden Methoden hinzu:</p>private static Action&lt;QueryDescriptor&lt;BookDoc&gt;&gt; BuildSemanticQuery(
    string searchTerm,
    Dictionary&lt;string, List&lt;string&gt;&gt; selectedFacets
)
{
    var filters = BuildFilters(selectedFacets);

    return query =&gt;
        query.Bool(b =&gt;
            b.Must(m =&gt; m.Semantic(sem =&gt; sem.Field("longDescription").Query(searchTerm)))
                .Filter(filters.ToArray())
        );
}

private static Action&lt;QueryDescriptor&lt;BookDoc&gt;&gt; BuildMultiMatchQuery(
    string searchTerm,
    Dictionary&lt;string, List&lt;string&gt;&gt; selectedFacets
)
{
    var filters = BuildFilters(selectedFacets);

    if (string.IsNullOrEmpty(searchTerm))
    {
        return query =&gt; query.Bool(b =&gt; b.Filter(filters.ToArray()));
    }

    return query =&gt;
        query.Bool(b =&gt;
            b.Should(m =&gt;
                    m.MultiMatch(mm =&gt;
                        mm.Query(searchTerm).Fields(new[] { "title", "shortDescription" })
                    )
                )
                .Filter(filters.ToArray())
        );
}<p>Beide Methoden funktionieren ähnlich wie die <code>BuildHybridQuery</code> -Methode, führen aber nur eine Volltext- oder semantische Suche durch.</p><p>Sie können die <code>SearchBooksAsync</code> -Methode so anpassen, dass sie die ausgewählte Suchmethode verwendet:</p>public async Task&lt;ElasticResponse&gt; SearchBooksAsync(
    string searchTerm,
    Dictionary&lt;string, List&lt;string&gt;&gt; selectedFacets
)
{
    try
    {
        _logger.LogInformation($"Performing search for: {searchTerm}");
        
        // Modify the query builder to use the selected search method.
        var multiMatchQuery = BuildMultiMatchQuery(searchTerm, selectedFacets); // For full text search
        var semanticQuery = BuildSemanticQuery(searchTerm, selectedFacets); // For semantic search

        // In this case we will not use retrievers, but you can add them if you want to use them.
        var response = await _client.SearchAsync&lt;BookDoc&gt;(s =&gt;
            s.Index("elastic-blazor-books")
                .Query(multiMatchQuery) // Change this line to use different search methods, for example: .Query(semanticQuery) for semantic search
                .Aggregations(aggs =&gt;
                    aggs.Add("Authors", agg =&gt; agg.Terms(t =&gt; t.Field(p =&gt; p.Authors)))
                        .Add(
                            "Categories",
                            agg =&gt; agg.Terms(t =&gt; t.Field(p =&gt; p.Categories))
                        )
                        .Add("Status", agg =&gt; agg.Terms(t =&gt; t.Field(p =&gt; p.Status)))
                )
        );

        if (response.IsValidResponse)
        {
            _logger.LogInformation($"Found {response.Documents.Count} documents");

            var hits = response.Total;
            var facets =
                response.Aggregations != null
                    ? FormatFacets(response.Aggregations)
                    : new Dictionary&lt;string, Dictionary&lt;string, long&gt;&gt;();

            var elasticResponse = new ElasticResponse
            {
                TotalHits = hits,
                Documents = response.Documents.ToList(),
                Facets = facets,
            };

            return elasticResponse;
        }
        else
        {
            _logger.LogWarning($"Invalid response: {response.DebugInformation}");
            return new ElasticResponse();
        }
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Error performing search");
        return new ElasticResponse();
    }
}<p>Die vollständige Bewerbung finden Sie <a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/esre-with-blazor">hier.</a></p><h2>Fazit</h2><p>Blazor ist ein effektives Framework, mit dem Sie Webanwendungen in C# erstellen können. Elasticsearch ist eine leistungsstarke Suchmaschine, mit der Sie Suchanwendungen erstellen können. Durch die Kombination beider können Sie auf einfache Weise robuste Suchanwendungen erstellen und die Leistungsfähigkeit von ESRE nutzen, um in kurzer Zeit ein <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-search.html">semantisches Sucherlebnis</a> zu schaffen.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor</guid>
    <category><![CDATA[Vektordatenbank]]></category>
    <category><![CDATA[.NET]]></category>
    <dc:creator><![CDATA[Gustavo Llermaly]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte7424ac5f0b223b4/6a17f668414c641971945323/7ba0d6bec908bfcae966b7f38626fabd682c6f3d-1200x628.png" length="0" type="image/png"/>
    <pubDate>Wed, 09 Oct 2024 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>