<?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/jp/search-labs/author/gustavo-llermaly</link>
    </image>
    <link>https://www.elastic.co/jp/search-labs/author/gustavo-llermaly</link>
    <atom:link href="https://www.elastic.co/jp/search-labs/rss/author/gustavo-llermaly.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[jp]]></language>
    <lastBuildDate>Mon, 28 Sep 2026 03:58:48 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Elastic Workflowsを使用したKibanaのダッシュボード表示の監視]]></title>
    <description><![CDATA[Elastic Workflowsを使用して、Kibanaのダッシュボードのビューメトリクスを30分ごとに収集し、それらをElasticsearchにインデックスする方法を学びましょう。これにより、独自のデータ上にカスタム分析と可視化を構築できます。]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/kibana">Kibana</a>は各ダッシュボードが何回閲覧されたかを追跡しますが、そのデータは組み込みのダッシュボードにはネイティブに公開されていません。この記事では、<strong>Elastic Workflows</strong>を使って30分ごとにそのデータを自動収集し、Elasticsearchにインデックス化して、その上に独自の分析を構築します。</p><p><a href="https://www.elastic.co/docs/explore-analyze/workflows">Elastic Workflows</a>はKibana内部に組み込まれた自動化エンジンで、シンプルなYAML構成を使用して多段階プロセスを定義できます。各ワークフローはスケジュールやイベント、または<a href="https://www.elastic.co/docs/explore-analyze/ai-features/elastic-agent-builder">Elastic Agent Builder</a>のツールとしてトリガーでき、各ステップでKibana APIを呼び出し、Elasticsearchをクエリし、データを変換できます。</p><p>具体的な例としてダッシュボードのビュー数を使用しますが、同じパターンがKibanaの保存済みオブジェクトAPIを通じて提供されるすべてのメトリックにも当てはまります。</p><h2>要件</h2><ul><li><p><a href="https://www.elastic.co/cloud">Elastic Cloud</a>または<a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed">セルフマネージド</a>クラスター（バージョン9.3を実行）</p></li><li><p><a href="https://www.elastic.co/docs/explore-analyze/workflows/get-started#workflows-prerequisites">ワークフローが有効</a>（詳細設定）</p></li></ul><h2>ステップ1：<a href="https://www.elastic.co/docs/explore-analyze/query-filter/tools/console">開発ツール</a>で生データを調べます</h2><p>何かを作る前に、どんなデータがあるのかを理解しましょう。Kibanaはほとんどの設定やメタデータを専用の内部インデックスに<a href="https://www.elastic.co/docs/api/doc/kibana/group/endpoint-saved-objects">保存済みオブジェクト</a>として保存しています。Kibanaがこの方法で追跡している項目の一つに、使用量カウンターと呼ばれる、特別な保存オブジェクトタイプを使ったダッシュボードの閲覧数があります。次のように、開発ツールから直接クエリできます。</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>応答は下記のようになります。</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><code>counterName</code>フィールドはダッシュボードIDであり、<code>count</code>はそのダッシュボードに対する当日中の累積閲覧数です。Kibanaは、1つのダッシュボードにつき1日に1つのカウンターオブジェクトを作成します。オブジェクトIDに日付サフィックス（...views:server:20260310）が表示されます。ユーザーがダッシュボードを開くにつれて、その数は一日を通して増加していきます。</p><p>この日常的なドキュメントモデルをインデックスで複製するのではなく、ワークフローの実行ごとに1つのドキュメントを作成します。各ドキュメントは、キャプチャの瞬間におけるその日のダッシュボードの累積ビュー数を記録します。</p><h2>ステップ2：宛先インデックスの作成</h2><p>ダッシュボードビューのスナップショットを格納するためのインデックスが必要です。次のコマンドは明示的なマッピングで作成し、後で集約や可視化ができるようにします。Dev Toolsで実行してください：</p>PUT dashboard-views
{
  "mappings": {
    "properties": {
      "captured_at": {
        "type": "date"
      },
      "dashboard_id": {
        "type": "keyword"
      },
      "dashboard_name": {
        "type": "keyword"
      },
      "view_count": {
        "type": "integer"
      }
    }
  }
}<p>IDと名前に<a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/keyword"><code>keyword</code></a>マッピングを使用すると、<a href="https://www.elastic.co/docs/explore-analyze/query-filter/aggregations">アグリゲーション</a>が可能になります。<code>view_count</code>のために<a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/number"><code>integer</code></a>を使用することは安全なデフォルト設定です。Kibanaはカウンターを毎日リセットするため、32ビットの制限（1日で20億回以上のビュー）に達することは現実的な懸念事項ではありません。数値演算も引き続きサポートしており、<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>、<a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-metrics-min-aggregation"><code>min</code></a>などが含まれます。</p><h2>ステップ3: ワークフローを作成する</h2><p><strong>Stack Management &gt; Workflows &gt; New Workflow</strong>に移動し、次のワークフローのYAML構成を貼り付けます。</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>次のセクションでは、ワークフローをステップごとに分解していきます。</p><h3>ワークフローの仕組み</h3><h4>トリガー</h4><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7672aa533b4bc9ed/6a17dc5b420229d07c29f4d4/5670991d65c64ee833924225c2d375a1be868b13-325x162.png" alt=" スケジュールされたトリガー" /><p>ワークフローは30分ごとにスケジュールされたトリガーで実行されます。これにより、APIに負担をかけずに時系列データが得られます。</p><h4>ダッシュボードビューを取得</h4><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltab2a16f1f11304ea/6a17dc5d25daab26f608a117/66eaec147c3d01c524c67cf1c7f663ac56a3259d-812x215.png" alt=" ダッシュボードを取得" /><p><code>kibana.request</code>を使ってKibanaの保存済みオブジェクトAPIを呼び出します。認証の設定は不要です。ワークフローエンジンは実行コンテキストに基づいて適切なヘッダーを自動的に添付します。</p><h4>index_each_dashboard (foreach)</h4><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte6b2611b0216555e/6a17dc5f445de95b584cffe5/aad45e8aed8dc81ded6260cd6199ff78dcffe3b4-1892x290.png" alt="各ダッシュボードをインデックス化" /><p>前のステップで返された <a href="https://www.elastic.co/docs/api/doc/kibana/group/endpoint-saved-objects"><code>saved_objects</code></a> 配列を反復します。各反復の現在の項目は <code>foreach.item</code>として利用可能です。ループ内では、各ダッシュボードごとに2つの入れ子手順を実行します。</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="：ダッシュボード名を取得する" /><p>人間が読み取れるダッシュボードのタイトルを <code>GET /api/saved_objects/dashboard/{id}</code> を呼び出して解決します。<code>on-failure: continue: true</code> を追加することで、ダッシュボードが削除されてもビューカウンターが残っている場合、ループが継続して全体の実行が失敗しないようにします。</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のリクエスト" /><p>各ドキュメントを<code>POST /dashboard-views/_doc</code>（明示的なIDなし）でインデックス化することで、Elasticsearchが自動でIDを生成します。これにより、毎回の実行時に新しいドキュメントが作成され、以前のスナップショットを上書きするのではなく、時間の経過とともにビューカウントの履歴が構築されます。</p><p>次の2点にご注意ください。</p><ul><li><p><code>captured_at</code>フィールドは日付フィルターを使ってタイムスタンプを<a href="https://www.iso.org/iso-8601-date-and-time-format.html">ISO 8601</a>としてフォーマットします。それなしでは、値はJavaScriptの日付文字列として出力され（例：<code>Tue Mar 10 2026 05:03:47 GMT+0000</code>）、Elasticsearchは日付としてマップしません。</p></li><li><p><code>view_count</code>は数値型を保持するために<code>${{ }}</code>構文を<code>| plus: 0</code>と共に用います。<code>{{ }}</code>を使うと文字列としてレンダリングされ、ダッシュボードでの計算操作ができなくなります。</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3b94bb6c22c0253e/6a17dc6425daab37a508a11b/6d48c8784d5df6192e8b5175e69dbab5098194bc-919x774.png" alt="" /><p><em>UIでは、各ワークフローステップを快適にデバッグできます。</em></p><h2>ステップ4：統計ダッシュボードを構築</h2><p>ワークフローが数回実行されてデータが収集されたら、dashboard-views Data viewを使用してKibanaで新しいダッシュボードを作成します。</p><p>まずは以下のパネルから始めましょう：</p><ul><li><p><strong>トップのダッシュボード（閲覧数別）：</strong> X軸に<code>dashboard_name</code>、Y軸に<code>last_value(view_count)</code>を持つ<a href="https://www.elastic.co/docs/explore-analyze/visualize/charts/bar-charts"><strong>棒グラフ</strong></a>を使用します。これはダッシュボードごとの現在の日次閲覧数を示しています。</p></li><li><p><strong>時間経過に伴うビュー：</strong>X軸に<code>captured_at</code>、Y軸に<code>last_value(view_count)</code>を用いた<a href="https://www.elastic.co/docs/explore-analyze/visualize/charts/line-charts"><strong>折れ線グラフ</strong></a>を使用し、<code>dashboard_name</code>で分類されます。各実行が新しいドキュメントを追加するため、重複を合計するのではなく、最後の値を使用して時間バケットごとのピークカウントを取得します。</p></li><li><p><strong>現在のスナップショット：</strong>すべてのダッシュボードの最新のビュー数を表示するには、最新の<code>captured_at</code> を含む<a href="https://www.elastic.co/docs/explore-analyze/visualize/charts/tables"><strong>データテーブル</strong></a>を使用します。</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt18d0390e0526b215/6a17dc65a292991da7d02b79/e245b95f67daf76a2aaf4cb9df2c75ef4cfef582-1462x747.png" alt="" /><p>各ワークフローが新しいドキュメントを作成するため、時間範囲でフィルタリングして特定の期間のアクティビティを分析したり、週ごとの比較を行ったり、ダッシュボードのビューしきい値を下回ったときにアラートを設定したりすることができます。</p><h2><strong>まとめ</strong></h2><p>Elastic Workflowsは、ソース（Kibana API）と送信先（Elasticsearch）の両方がネイティブであるため、認証管理が全く必要なく、この種の定期的なデータ収集に適しています。ワークフローエンジンは<code>kibana.request</code>と<code>elasticsearch.request</code>のステップで認証を自動的に処理するため、記述するのはロジックだけです。</p><h2><strong>資料</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[開発者エクスペリエンス]]></category>
    <dc:creator><![CDATA[Gustavo Llermaly]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltef604bbb6dee6be0/6a17dc67a29299db23d02b7d/0ed94ce00962287b5507f45c92ecb60fdcbf2718-1280x720.png" length="0" type="image/png"/>
    <pubDate>Fri, 03 Apr 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elastic Open Web Crawler をコードとして]]></title>
    <description><![CDATA[GitHub Actions を使用して Elastic Open Crawler の構成を管理する方法を学びます。これにより、リポジトリに変更をプッシュするたびに、その変更がクローラーのデプロイされたインスタンスに自動的に適用されます。]]></description>
    <content:encoded><![CDATA[<p><a href="https://github.com/elastic/crawler">Elastic Open Web Crawler</a>とその CLI 駆動型アーキテクチャを使用すると、バージョン管理されたクローラー構成とローカル テストを備えた CI/CD パイプラインを実現するのが非常に簡単になります。</p><p>従来、クローラーの管理は手動で行われ、エラーが発生しやすいプロセスでした。これには、UI で直接構成を編集することや、クロール構成の複製、ロールバック、バージョン管理などに苦労することが含まれていました。クローラー構成をコードとして扱うことで、ソフトウェア開発で期待されるのと同じ利点（再現性、追跡可能性、自動化）が得られ、この問題が解決されます。</p><p>このワークフローにより、ロールバック、バックアップ、移行などのタスクを CI/CD パイプラインに Open Web Crawler を簡単に組み込むことができるようになります。これらのタスクは、Elastic Web Crawler や App Search Crawler などの以前の Elastic Crawler では非常に困難でした。</p><p>この記事では、次の方法を学習します。</p><ul><li><p>GitHubを使用してクロール設定を管理する</p></li><li><p>デプロイ前にパイプラインをテストするためのローカルセットアップを用意する</p></li><li><p>メインブランチに変更をプッシュするたびに、新しい設定でウェブクローラーを実行するための本番環境設定を作成します。</p></li></ul><p>プロジェクトのリポジトリは<a href="https://github.com/llermaly/elastic-open-crawler-as-code"><em><strong>こちらです</strong></em></a><em><strong>。</strong></em><em>執筆時点では、Elasticsearch 9.1.3とOpen Web Crawler 0.4.2を使用しています。</em></p><h2>要件</h2><ul><li><p>Dockerデスクトップ</p></li><li><p>Elasticsearchインスタンス</p></li><li><p>SSH アクセス（AWS EC2 など）と Docker がインストールされた仮想マシン</p></li></ul><h2>ステップ</h2><ol><li><p>フォルダ構造</p></li><li><p>クローラー構成</p></li><li><p>Docker-compose ファイル (ローカル環境)</p></li><li><p>Githubアクション</p></li><li><p>ローカルテスト</p></li><li><p>本番環境へのデプロイ</p></li><li><p>変更と再展開</p></li></ol><h2>フォルダ構造</h2><p>このプロジェクトでは、次のファイル構造になります。</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>クローラー構成</h2><p><code>crawler-config.yml,</code>の下に次の内容を入力します。</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>これは、製品の模擬サイトである<a href="https://web-scraping.dev/products">https://web-scraping.dev/products</a>からクロールします。最初の 3 つの製品ページのみをクロールします。<code>max_crawl_depth</code>設定により、クローラーは<code>seed_urls</code>として定義されたページよりも多くのページを検出することがなくなり、それらのページ内のリンクを開かなくなります。</p><p>Elasticsearch <code>host</code>と<code>api_key</code>は、スクリプトを実行している環境に応じて動的に設定されます。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7d37b966aafbd3c1/6a17ef9842022946e929f6b9/f9831034e1c4ccb554d37bdd188f2824338355a0-890x624.png" alt="Web スクレイピングをテストするための模擬サイト、web-scraping.dev ドメインの「Box of Chocolate Candy」の製品ページ。このページには、製品のタイトル、画像、説明、価格と購入ボタンの HTML 要素が表示されます。" /><h2>Docker-compose ファイル (ローカル環境)</h2><p>ローカル<code>docker-compose.yml,</code>には、クローラーと単一の Elasticsearch クラスター + Kibana をデプロイして、本番環境にデプロイする<em><strong>前に</strong></em>クロール結果を簡単に視覚化できるようにします。</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>Elasticsearch の実行準備ができるまでクローラーが待機する方法に注意してください。</p><h2>Githubアクション</h2><p>ここで、新しい設定をコピーし、メインにプッシュするたびに仮想マシンでクローラーを実行する GitHub アクションを作成する必要があります。これにより、手動で仮想マシンにアクセスしてファイルを更新し、クローラーを実行する必要がなくなり、常に最新の構成が展開されます。仮想マシンプロバイダーとして AWS EC2 を使用します。</p><p>最初のステップは、ホスト ( <code>VM_HOST</code> )、マシン ユーザー ( <code>VM_USER</code> )、SSH RSA キー ( <code>VM_KEY</code> )、Elasticsearch ホスト ( <code>ES_HOST</code> )、Elasticsearch API キー ( <code>ES_API_KEY</code> ) を GitHub Action シークレットに追加することです。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb5a0fcfff9b7f997/6a17ef9a6df731d7d40a0fdf/e1075bc54151b4b94eac2a6bd2682e9997e6c709-1106x707.png" alt="「アクション、シークレット、変数」構成ページでは、Web ベースのインターフェース内で VM_HOST、VM_KEY、VM_USER などのリポジトリ シークレットが表示されます。" /><p>この方法により、アクションはサーバーにアクセスして新しいファイルをコピーし、クロールを実行できるようになります。</p><p>それでは、 <code>.github/workflows/deploy.yml</code>ファイルを作成しましょう。</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>このアクションは、クローラー構成ファイルに変更をプッシュするたびに、次の手順を実行します。</p><ol><li><p>yml config に Elasticsearch ホストと API キーを入力します。</p></li><li><p>configフォルダをVMにコピーします</p></li><li><p>SSH経由でVMに接続します</p></li><li><p>リポジトリからコピーした設定でクロールを実行します</p></li></ol><h2>ローカルテスト</h2><p>クローラーをローカルでテストするために、Docker からローカルのものを Elasticsearch ホストに入力してクロールを開始する bash スクリプトを作成しました。<code>./local.sh</code>を実行して実行できます。</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>Kibana DevTools を見て、 <code> web-crawler-index</code>が正しく入力されていることを確認しましょう。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt989660368fe14db8/6a17ef9b9da390c79ce46562/18551635e8265866e389a9632c4e4540958e4468-990x723.png" alt="Web クローラー インデックスが正しく設定されていることを確認するための Kibana DevTools コード。" /><h2>本番環境へのデプロイ</h2><p>これで、メイン ブランチにプッシュする準備が整いました。これにより、仮想マシンにクローラーがデプロイされ、Serverless Elasticsearch インスタンスにログの送信が開始されます。</p>git add .
git commit -m "First commit"
git push<p>これにより、GitHub アクションがトリガーされ、仮想マシン内でデプロイ スクリプトが実行され、クロールが開始されます。</p><p>アクションが実行されたかどうかを確認するには、GitHub リポジトリにアクセスして「アクション」タブにアクセスします。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt986f1a4e4f3288d2/6a17ef9c7f6f1584fdc09c10/67ba3a7164d7a8049fe5661264820826cb18ed64-667x325.png" alt="アクションは GitHub リポジトリの EC2 タブにデプロイされます。" /><h2>変更と再展開</h2><p>お気づきかもしれませんが、各製品の<code>price</code>はドキュメントの本文フィールドの一部です。価格を別のフィールドに保存して、それに対してフィルターを実行できるようにするのが理想的です。</p><p><a href="https://github.com/elastic/crawler/blob/main/docs/features/EXTRACTION_RULES.md">抽出ルール</a>を使用して<code>product-price</code> CSS クラスから価格を抽出するために、 <code>crawler.yml</code>ファイルに次の変更を追加してみましょう。</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>また、価格にはドル記号 ( <code>$</code> ) が含まれていますが、範囲クエリを実行する場合はこれを削除する必要があります。そのために、取り込みパイプラインを使用できます。上記の新しいクローラー構成ファイルでこれを参照していることに注意してください。</p>PUT _ingest/pipeline/pricing-pipeline
{
  "processors": [
    {
      "script": {
        "source": """
                ctx['price'] = ctx['price'].replace("$","")
            """
      }
    }
  ]
}<p>このコマンドは本番環境の Elasticsearch クラスターで実行できます。開発用の場合は一時的なものなので、次のサービスを追加することで、パイプライン作成部分を<code>docker-compose.yml</code>ファイルにすることができます。パイプラインが正常に作成された後にクローラー サービスが起動するように、 <code>depends_on</code>もクローラー サービスに追加されていることに注意してください。</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>次に、 <code>`./local.sh`</code>を実行してローカルで変更を確認してみましょう。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0f390aedf67cb4fe/6a17ef9eaf47b62bd2cde05b/dc1801599344a9f69f072b07ff828c4ba3815d7b-738x473.png" alt="`./local.sh` を実行して、ローカルでの価格の変化を確認します。" /><p>素晴らしい！変更をプッシュしてみましょう。</p>git add crawler-config.yml
git commit -m "added price CSS selector"
git push<p>すべてが機能していることを確認するには、本番環境の Kibana をチェックします。変更が反映され、価格がドル記号なしの新しいフィールドとして表示されるはずです。</p><h2>まとめ</h2><p>Elastic Open Web Crawler を使用すると、クローラーをコードとして管理できるため、開発からデプロイメントまでのパイプライン全体を自動化したり、一時的なローカル環境を追加したり、クロールされたデータに対してプログラムでテストを実行したりすることができます。</p><p>公式リポジトリのクローンを作成し、このワークフローを使用して独自のデータのインデックス作成を開始してください。クローラーによって生成されたインデックスに対してセマンティック検索を実行する方法については、<a href="https://www.elastic.co/search-labs/blog/semantic-search-open-crawler">この記事</a>を読むこともできます。</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[データのインデキシング]]></category>
    <category><![CDATA[運用]]></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[BlazorとElasticsearchを使用した検索アプリの構築]]></title>
    <description><![CDATA[Blazor と Elasticsearch を使用して検索アプリケーションを構築する方法と、ハイブリッド検索に Elasticsearch .NET クライアントを使用する方法を学習します。]]></description>
    <content:encoded><![CDATA[<p>この記事では、C# スキルを活用して Blazor と Elasticsearch を使用した検索アプリケーションを構築する方法を学習します。<a href="https://www.elastic.co/guide/en/elasticsearch/client/net-api/current/introduction.html">Elasticsearch .NET</a>クライアントを使用して、<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/full-text-queries.html">フルテキスト</a>、<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-search.html">セマンティック</a>、<a href="https://www.elastic.co/search-labs/tutorials/search-tutorial/vector-search/hybrid-search">ハイブリッド</a>検索クエリを実行します。</p><p><strong>注:</strong> Elasticsearch C# クライアント<a href="https://www.elastic.co/guide/en/elasticsearch/client/net-api/7.17/nest.html">NEST</a>の古いバージョンに精通している場合は、NEST クライアントの廃止と新機能に関するこちらの<a href="https://www.elastic.co/search-labs/blog/net-client-evolution">ブログ投稿を</a>お読みください。<em>NESTは.NETクライアントの前世代であり、</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="Blazor アプリの構築: Blazor ダイアグラムを使用した esre" /><ul><li><p><a href="https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor#what-is-blazor?">Blazor とは何ですか?</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor#what-is-esre?">ESRE とは何ですか?</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor#configuring-elser">ELSER の設定</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor#indexing-data">データのインデックス作成</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">建築申請</a></p></li></ul><h2>Blazor とは何ですか?</h2><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt91278ae422883e28/6a17f6592f4a5c3213fa8a95/622741915d016b68bf94f742332d10d736d60052-707x461.png" alt="Blazorサーバー" /><p><a href="https://dotnet.microsoft.com/en-us/apps/aspnet/web-apps/blazor">Blazor は</a>、開発者がクライアントまたはサーバー上で実行される Web アプリケーションを構築できるようにするために Microsoft によって作成された、オープン ソースの HTML、CSS、および C# ベースの Web フレームワークです。Blazor を使用すると、再利用可能なコンポーネントを作成してアプリケーションをより速く構築することもできます。開発者は、同じファイル内に C# で HTML ビューとアクションを構築できるため、読みやすくクリーンなコードを維持できます。さらに、Blazor Hybrid を使用すると、.NET コードを介してネイティブ プラットフォーム機能にアクセスするネイティブ モバイル アプリを構築できます。</p><p>Blazor を優れたフレームワークにする機能の一部を以下に示します。</p><ul><li><p>サーバー側とクライアント側のレンダリングオプション</p></li><li><p>再利用可能なUIコンポーネント</p></li><li><p>SignalRによるリアルタイム更新</p></li><li><p>組み込みの状態管理</p></li><li><p>組み込みルーティングシステム</p></li><li><p>強力な型付けとコンパイル時のチェック</p></li></ul><h3>Blazor を選ぶ理由</h3><p>Blazor は他のフレームワークやライブラリに比べていくつかの利点があります。開発者はクライアント コードとサーバー コードの両方に C# を使用できるため、強力な型指定とコンパイル時のチェックが可能になり、信頼性が向上します。.NET エコシステムとシームレスに統合され、.NET ライブラリとツールの再利用を可能にし、強力なデバッグ サポートを提供します。</p><h2>ESRE とは何ですか?</h2><p><a href="https://www.elastic.co/elasticsearch/elasticsearch-relevance-engine">Elasticsearch Relevance Engine ™ (ESRE)</a>は、強力な Elasticsearch 検索エンジンをベースに機械学習と人工知能を使用して<a href="https://www.elastic.co/guide/en/esre/current/learn.html">検索アプリケーションを構築するためのツール</a>セットです。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf313ba986de01b93/6a17d851505ac306fcad8975/4c0f2645ed1c27fe3ef61a1a9126adadfd8d5368-721x421.png" alt="エスレ" /><p>ESREについて詳しく知りたい方は、<a href="https://www.elastic.co/search-labs/blog/introducing-elasticsearch-relevance-engine-esre">こちらの</a>ブログ記事をご覧ください。</p><h2>ELSER の設定</h2><p>Elastic の<a href="https://www.elastic.co/elasticsearch/elasticsearch-relevance-engine">ESRE</a>機能を活用するために、モデルプロバイダーとして<a href="https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-elser.html">ELSER を</a>使用します。</p><p><em>ElasticsearchのELSERモデルを使用するには、PlatinumまたはEnterpriseライセンスと、最低4GBの専用機械学習（ML）ノードが必要です。詳細は</em><a href="https://www.elastic.co/guide/en/machine-learning/8.15/ml-nlp-elser.html#elser-req"><em>こちらをご覧ください。</em></a></p><p>まず推論エンドポイントを作成します。</p>PUT _inference/sparse_embedding/my-elser-model
{
  "service": "elser",
  "service_settings": {
    "num_allocations": 1,
    "num_threads": 1
  }
}<p>ELSER を初めて使用する場合は、モデルがバックグラウンドで読み込まれるときに 502 Bad Gateway エラーが発生する可能性があります。Kibana の<code>Machine Learning &gt; Trained Models</code>でモデルのステータスを確認できます。デプロイが完了したら、次のステップに進むことができます。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltee295a3516338302/6a17f65b414c640deb94531d/a7ff94b72d337cde892165d744b9f42fba702a87-1440x649.png" alt="訓練済みモデルのチェック" /><h2>データのインデックス作成</h2><p><a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/esre-with-blazor/books.zip">ここから</a>データセットをダウンロードし、Kibana を使用してデータをインポートできます。これを行うには、ホームページにアクセスして「データのアップロード」をクリックします。次に、ファイルをアップロードして<code>Import</code>をクリックします。最後に、 <code>Advanced</code>タブに移動して、次のマッピングを貼り付けます。</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="データのインポート" /><p>セマンティック クエリとフルテキスト クエリを実行できるインデックスを作成します。<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-text.html">semantic_text</a>フィールド タイプは、データのチャンク化と埋め込みを処理します。として インデックス付けしていることに注意してください 。フィールドを<em> と text` の</em><em> 両方としてインデックス付けしたい場合は、 copy_to を使用できます</em><em> 。</em><em><code>longDescription</code></em><em><code>semantic_text</code></em><em><code>semantic_text</code></em></p><h2>BlazorとElasticsearchを使ったアプリ構築</h2><h3>APIキー</h3><p>最初に行う必要があるのは、Elasticsearch へのリクエストを認証するための API キーを作成することです。API キーは読み取り専用であり、 <code>books-blazor</code>インデックスのクエリのみに許可される必要があります。</p>POST /_security/api_key
{
  "name": "books-blazor-key",
  "role_descriptors": {
    "books-blazor-reader": {
      "indices": [
        {
          "names": ["books-blazor"],
          "privileges": ["read"]
        }
      ]
    }
  }
}<p>次のような画面が表示されます。
</p>{
  "id": "XXXXXXXXXXXXXXXXXXXXXXXX",
  "name": "books-blazor-key",
  "api_key": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  "encoded": "XXXXXXXXXXXXXXXXXXXXXXXX=="
}<p>後で必要になるので、 <code>encoded</code>応答フィールドの値を保存します。<a href="https://www.elastic.co/cloud/">Elastic Cloud</a>で実行している場合は、Cloud ID も必要になります。(<a href="https://www.elastic.co/search-labs/tutorials/install-elasticsearch/elastic-cloud#finding-your-cloud-id">こちらで</a>確認できます)。</p><h4>Blazorプロジェクトの作成</h4><p>まず Blazor をインストールし、<a href="https://dotnet.microsoft.com/en-us/learn/aspnet/blazor-tutorial/install">公式の手順</a>に従ってサンプル プロジェクトを作成します。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb5c20b896fe1e98e/6a17f65f6317301566585bff/f5720867d960c91fd1b3c4ae9be174e062e6b2fc-975x830.png" alt="Blazor チュートリアル - Blazor プロジェクトの作成" /><p>プロジェクトを作成すると、フォルダー構造とファイルは次のようになります。</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>テンプレートアプリケーションには<a href="https://blog.getbootstrap.com/2021/08/04/bootstrap-5-1-0/">Bootstrap v5.1.0</a>が含まれていますスタイリング用。</p><p><a href="https://www.elastic.co/guide/en/elasticsearch/client/net-api/8.0/installation.html">Elasticsearch .NET</a>クライアントをインストールしてプロジェクトのセットアップを完了します。</p>dotnet add package Elastic.Clients.Elasticsearch<p>この手順を完了すると、ページは次のようになります。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3b90e394ded7ff56/6a17f660faa913d49f93ca10/ef9d2186c2cf49e78a307c4aa69c51632e84578d-940x529.png" alt="Blazor の hello world" /><h3>フォルダ構造</h3><p>ここで、フォルダーを次のように整理します。</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>ファイルの説明:</p><ul><li><p>Components/Pages/Search.razor: 検索バー、結果、フィルターを含むメイン ページ。</p></li><li><p>コンポーネント/ページ/Search.razor.css:ページ スタイル。</p></li><li><p>Components/Elasticsearch/SearchBar.razor: 検索バー コンポーネント。</p></li><li><p>Components/Elasticsearch/Results.razor: 結果コンポーネント。</p></li><li><p>Components/Elasticsearch/Facet.razor: フィルター コンポーネント。</p></li><li><p>Components/Svg/GlassIcon.razor: 検索アイコン。</p></li><li><p>Components/_Imports.razor: これにより、すべてのコンポーネントがインポートされます。</p></li><li><p>Models/Book.cs: ブックフィールドのスキーマを保存します。</p></li><li><p>Models/Response.cs: 検索結果、ファセット、ヒット数の合計を含む応答スキーマを保存します。</p></li><li><p>Services/ElasticsearchService.cs: Elasticsearch サービス。Elasticsearch への接続とクエリを処理します。</p></li></ul><h4>初期設定</h4><p>まずはクリーンアップから始めましょう。</p><p>ファイルを削除します:</p><ul><li><p>コンポーネント/ページ/Counter.razor</p></li><li><p>コンポーネント/ページ/Weather.razor</p></li><li><p>コンポーネント/ページ/ホーム.razor</p></li><li><p>コンポーネント/レイアウト/NavMenu.razor</p></li><li><p>コンポーネント/レイアウト/NavMenu.razor.css</p></li></ul><p><code>/Components/_Imports.razor</code>ファイルを確認してください。次のインポートが必要です。</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>プロジェクトにElasticを統合する</h4><p>次に、Elasticsearch コンポーネントをインポートします。</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>アプリケーションのスペースを増やすために、 <code>/Components/Layout/MainLayout.razor</code>ファイルからデフォルトのサイドバーを削除します。</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="ナビゲーションバーをレイアウトから削除しました" /><p>次に、<a href="https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-8.0&amp;tabs=linux#secret-manager">ユーザー シークレット</a>の Elasticsearch 資格情報を入力します。</p>dotnet user-secrets init
dotnet user-secrets set ElasticsearchCloudId "your Cloud ID"
dotnet user-secrets set ElasticsearchApiKey "your API Key"<p>このアプローチを使用すると、.Net 8 は機密データをプロジェクト フォルダー外の別の場所に保存し、 <code>IConfiguration</code>インターフェースを使用してアクセスできるようになります。これらの変数は、同じユーザー シークレットを使用するすべての .Net プロジェクトで使用できます。</p><p>次に、 <code>Program.cs</code>ファイルを変更してシークレットを読み取り、Elasticsearch クライアントをマウントします。</p><p>まず、必要なライブラリをインポートします。</p>using BlazorApp.Services;
using Elastic.Clients.Elasticsearch;
using Elastic.Transport;<ul><li><p>BlazorApp.Services: Elasticsearch サービスが含まれます。</p></li><li><p>Elastic.Clients.Elasticsearch: Elasticsearch クライアント .Net 8 ライブラリをインポートします。</p></li><li><p>Elastic.Transport: Elasticsearch トランスポート ライブラリをインポートします。これにより、ApiKey クラスを使用してリクエストを認証できるようになります。</p></li></ul><p>次に、 <code>var app = builder.Build()</code>行の前に次のコードを挿入します。</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>このコードは、ユーザー シークレットから Elasticsearch 資格情報を読み取り、Elasticsearch クライアント インスタンスを作成します。</p><p>ElasticSearch クライアントの初期化後、次の行を追加して Elasticsearch サービスを登録します。</p>builder.Services.AddScoped&lt;ElasticsearchService&gt;();<p>次のステップでは、 <code>/Services/ElasticsearchService.cs</code>ファイルに検索ロジックを構築します。</p><p>まず、必要なライブラリとモデルをインポートします。</p>using BlazorApp.Models;
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.QueryDsl;<p>次に、クラス<code>ElasticsearchService</code> 、コンストラクター、および変数を追加します。</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>検索の設定</h4><p>それでは、検索ロジックを構築してみましょう。</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> ユーザーが選択したファセットを使用して、検索クエリのフィルターを構築します。</p></li><li><p><code>BuildHybridQuery</code> 全文検索とセマンティック検索を組み合わせた<a href="https://www.elastic.co/search-labs/tutorials/search-tutorial/vector-search/hybrid-search">ハイブリッド検索</a>クエリを構築します。</p></li></ul><p>次に、検索メソッドを追加します。</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>: ハイブリッド クエリを使用して検索を実行し、ファセットを構築するための集計が含まれた結果を返します。</p></li><li><p><code>FormatFacets</code>: 集計応答を辞書にフォーマットします。</p></li><li><p><code>ConvertFacetDictionary</code>: ファセット辞書をより読みやすい形式に変換します。</p></li></ul><p>次のステップでは、検索ページの結果として出力される Elasticsearch クエリの<code>hits</code>で返されるデータを表すモデルを作成します。</p><p>まず、ファイル<code>/Models/Book.cs</code>を作成し、次の内容を追加します。</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>次に、 <code>/Models/Response.cs</code>ファイルに Elastic レスポンスを設定し、次のコードを追加します。</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>基本的なUIの設定</h4><p>次に、SearchBar コンポーネントを追加します。ファイル<code>/Components/Elasticsearch/SearchBar.razor</code>に次の内容を追加します。</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>このコンポーネントには、検索バーと検索を実行するためのボタンが含まれています。</p><p>Blazor は、同じファイル内で C# コードを使用して HTML を動的に生成できるようにすることで、優れた柔軟性を提供します。</p><p>その後、ファイル<code>/Components/Elasticsearch/Results.razor</code>で、検索結果を表示する結果コンポーネントを構築します。</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>最後に、検索結果をフィルタリングするためのファセットを作成する必要があります。</p><p><em>注: ファセットとは、製品タイプ、価格帯、ブランドなど、特定の属性やカテゴリに基づいて検索結果を絞り込むことができるフィルターです。これらのフィルターは通常、クリック可能なオプション（多くの場合チェックボックス）として表示され、ユーザーが検索を絞り込み、関連性の高い結果を見つけやすくします。Elasticsearchでは、ファセットは</em><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html"><em>集計を</em></a>使用して作成されます<em>。</em></p><p>ファイル<code>/Components/Elasticsearch/Facet.razor</code>に次のコードを配置してファセットを設定します。</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>このコンポーネントは、 <code>author</code> 、 <code>categories</code> 、および<code>status</code>フィールドの<code>terms</code>集計を読み取り、Elasticsearch に送り返すフィルターのリストを生成します。</p><p>さて、すべてをまとめてみましょう。</p><p><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>私たちのページは機能しています!</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc750e55fff43de4b/6a17f6636864a4ab08b68931/5f0f25cb029a34df7f577a9f307278226ed07c3f-816x473.png" alt="Blazorページの例" /><p>ご覧のとおり、ページは機能しますが、スタイルが欠けています。見た目をもっと整理してレスポンシブにするために、CSS を追加してみましょう。</p><p>レイアウト スタイルの置き換えを始めましょう。<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><code>Components/Pages/Search.razor.css</code>ファイルで検索ページのスタイルを追加します。</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>ページの見た目が良くなり始めました:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3ecc49f404f18591/6a17f6654b055d959b432384/3dadd7ade2dd9070300a4e0514a4da2ae9cc9fb9-817x473.png" alt="検索ページにスタイルを追加した後の Blazor ページ" /><p>最後に仕上げましょう:</p><p>次のファイルを作成します。</p><ul><li><p>コンポーネント/Elasticsearch/Facet.razor.css</p></li><li><p>コンポーネント/Elasticsearch/Results.razor.css</p></li></ul><p><code>Facet.razor.css</code>のスタイルを追加します:</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><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>最終結果:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt78bbd8e6f9e23988/6a17f6664b055d2ed8432388/3e68ec4a38775fdfbeaa1a990e6a1e11dfb081d8-816x472.png" alt="Blazor アプリページの構築の最終結果" /><p>アプリケーションを実行するには、次のコマンドを使用できます。</p><p><code>dotnet watch</code></p><p>やったね！検索バーを使用して Elasticsearch インデックス内の書籍を検索し、著者、カテゴリ、ステータス別に結果をフィルタリングできるようになりました。</p><h3>全文検索とセマンティック検索の実行</h3><p>デフォルトでは、アプリは<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html"> 全文検索</a> と <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-semantic-query.html">セマンティック検索の</a><a href="https://www.elastic.co/search-labs/tutorials/search-tutorial/vector-search/hybrid-search"> 両方を使用して ハイブリッド検索を</a> 実行します。フルテキスト検索用とセマンティック検索用の 2 つの別個のメソッドを作成し、いずれかのメソッドを選択してユーザーの入力に基づいてクエリを構築することで、検索ロジックを変更できます。</p><p><code>/Services/ElasticsearchService.cs</code>ファイルの<code>ElasticsearchService</code>クラスに次のメソッドを追加します。</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>どちらのメソッドも<code>BuildHybridQuery</code>メソッドと同様に動作しますが、全文検索またはセマンティック検索のみを実行します。</p><p>選択した検索方法を使用するように<code>SearchBooksAsync</code>メソッドを変更できます。</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>完全な申請書は<a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/esre-with-blazor">ここから</a>ご覧いただけます</p><h2>まとめ</h2><p>Blazor は、C# を使用して Web アプリケーションを構築できる効果的なフレームワークです。Elasticsearch は、検索アプリケーションを構築できる強力な検索エンジンです。両方を組み合わせることで、ESRE のパワーを活用して、<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-search.html">セマンティック検索エクスペリエンス</a>を短期間で実現し、堅牢な検索アプリケーションを簡単に構築できます。</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[Vector Database]]></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>