<?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/es/search-labs/author/gustavo-llermaly</link>
    </image>
    <link>https://www.elastic.co/es/search-labs/author/gustavo-llermaly</link>
    <atom:link href="https://www.elastic.co/es/search-labs/rss/author/gustavo-llermaly.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[es]]></language>
    <lastBuildDate>Mon, 28 Sep 2026 06:00:51 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Monitoreo de las vistas del dashboard de Kibana con flujos de trabajo de Elastic]]></title>
    <description><![CDATA[Conoce cómo usar flujos de trabajo de Elastic para recopilar métricas de vista del dashboard de Kibana cada 30 minutos e indexarlas en Elasticsearch, para que puedas crear análisis y vistas personalizadas sobre tus propios datos.]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/kibana">Kibana</a> registra cuántas veces se visualiza cada dashboard, pero esos datos no se muestran de forma nativa en ningún dashboard integrado. En este artículo, usaremos los <strong>flujos de trabajo de Elastic</strong> para recopilar automáticamente esos datos cada 30 minutos e indexarlos en Elasticsearch, para poder crear nuestras propias analíticas sobre ellos.</p><p><a href="https://www.elastic.co/docs/explore-analyze/workflows">Los flujos de trabajo de Elastic</a> son un motor de automatización integrado dentro de Kibana que te permite definir procesos multipaso usando una configuración sencilla de YAML. Cada flujo de trabajo puede activarse según una programación o un evento, o como una herramienta en <a href="https://www.elastic.co/docs/explore-analyze/ai-features/elastic-agent-builder">Elastic Agent Builder</a>, y cada paso puede llamar a las API de Kibana, consultar Elasticsearch o transformar datos.</p><p>Usaremos los recuentos de vistas del dashboard como ejemplo concreto, pero el mismo patrón aplica a cualquier métrica expuesta a través de la API de objetos guardados de Kibana.</p><h2>Requisitos previos</h2><ul><li><p><a href="https://www.elastic.co/cloud">Elastic Cloud</a> o un clúster <a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed">autogestionado </a>ejecutando la versión 9.3</p></li><li><p><a href="https://www.elastic.co/docs/explore-analyze/workflows/get-started#workflows-prerequisites">Flujos de trabajo activados</a> (Configuración avanzada)</p></li></ul><h2>Paso 1: explora los datos sin procesar en <a href="https://www.elastic.co/docs/explore-analyze/query-filter/tools/console">Herramientas de desarrollo</a></h2><p>Antes de crear cualquier cosa, entendamos qué datos tenemos. Kibana almacena la mayor parte de su configuración y metadatos como <a href="https://www.elastic.co/docs/api/doc/kibana/group/endpoint-saved-objects">objetos guardados</a> en un índice interno dedicado. Una de las cosas que Kibana rastrea de esta manera es el número de vistas del dashboard, mediante un tipo especial de objeto guardado llamado contadores de uso. Puedes consultarlas directamente desde las Herramientas de desarrollo:</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>La respuesta tiene el siguiente aspecto:</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>El campo <code>counterName</code> es el ID del dashboard y <code>count</code> es el recuento acumulado de vistas para ese dashboard en ese día específico. Kibana crea un objeto de contador por dashboard al día; puedes ver el sufijo de fecha en el ID del objeto (...viewed:server:20260310). El conteo aumenta a lo largo del día a medida que los usuarios abren el dashboard.</p><p>En lugar de replicar este modelo de documento diario en nuestro índice, crearemos un documento por cada ejecución del flujo de trabajo. Cada documento registra cuántas vistas había acumulado ese dashboard durante el día en el momento de la captura.</p><h2>Paso 2: Crear el índice de destino</h2><p>Necesitamos un índice para almacenar las snapshots de la vista del dashboard. El siguiente comando lo crea con mapeos explícitos para que podamos agregar y visualizar más tarde. Ejecuta esto en herramientas de desarrollo:</p>PUT dashboard-views
{
  "mappings": {
    "properties": {
      "captured_at": {
        "type": "date"
      },
      "dashboard_id": {
        "type": "keyword"
      },
      "dashboard_name": {
        "type": "keyword"
      },
      "view_count": {
        "type": "integer"
      }
    }
  }
}<p>Usar<a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/keyword"><code>keyword</code></a> mapeos para ID y nombres permite <a href="https://www.elastic.co/docs/explore-analyze/query-filter/aggregations">agregaciones</a>. Usar <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/number"><code>integer</code></a> para <code>view_count</code> es un valor predeterminado seguro, ya que Kibana restablece el contador diariamente, por lo que alcanzar el límite de 32 bits (más de 2 mil millones de vistas en un solo día) no es una preocupación realista. Todavía admite operaciones numéricas, como <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> y <a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-metrics-min-aggregation"><code>min</code></a>, entre otras.</p><h2>Paso 3: Crear el flujo de trabajo</h2><p>Ve a <strong>Stack Management &gt; Flujos de trabajo &gt; Nuevo flujo de trabajo</strong>, y pega la siguiente configuración YAML del flujo de trabajo:</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>En la próxima sección, hagamos un desglose del flujo de trabajo paso a paso.</p><h3>Cómo funciona el flujo de trabajo</h3><h4>Desencadenantes</h4><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7672aa533b4bc9ed/6a17dc5b420229d07c29f4d4/5670991d65c64ee833924225c2d375a1be868b13-325x162.png" alt=" Desencadenantes programados" /><p>El flujo de trabajo se ejecuta mediante un desencadenante programado cada 30 minutos. Esto nos proporciona datos temporales sin saturar la API.</p><h4>fetch_dashboard_views</h4><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltab2a16f1f11304ea/6a17dc5d25daab26f608a117/66eaec147c3d01c524c67cf1c7f663ac56a3259d-812x215.png" alt=" Obtener dashboard" /><p>Usa<code>kibana.request</code> para llamar a la API de objetos guardados de Kibana. No hace falta configurar la autenticación: el motor de flujos de trabajo añade automáticamente los encabezados correctos según el contexto de ejecución.</p><h4>index_each_dashboard (foreach)</h4><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte6b2611b0216555e/6a17dc5f445de95b584cffe5/aad45e8aed8dc81ded6260cd6199ff78dcffe3b4-1892x290.png" alt="Indexa cada dashboard" /><p>Itera sobre la matriz<a href="https://www.elastic.co/docs/api/doc/kibana/group/endpoint-saved-objects"><code>saved_objects</code></a> devuelta por el paso anterior. El elemento actual en cada iteración está disponible como <code>foreach.item</code>. Dentro del bucle, ejecutamos dos pasos anidados para cada dashboard.</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=": obtener nombre del dashboard" /><p>Resuelve el título legible para los humanos del dashboard al llamar a <code>GET /api/saved_objects/dashboard/{id}</code>. Agregamos <code>on-failure: continue: true</code> para que, si un dashboard se eliminó pero aún tiene contadores de vistas, el bucle continúe en lugar de fallar toda la ejecución.</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=" Solicitud de Elasticsearch" /><p>Indexa cada documento usando <code>POST /dashboard-views/_doc</code> (sin un ID explícito), lo que permite que Elasticsearch genere automáticamente los ID. Esto crea un nuevo documento en cada ejecución, lo que crea un historial del número de vistas a lo largo del tiempo en lugar de sobreescribir el snapshot anterior.</p><p>Dos cosas que vale la pena destacar:</p><ul><li><p>El campo <code>captured_at</code> usa el filtro de fecha para formatear la marca de tiempo como <a href="https://www.iso.org/iso-8601-date-and-time-format.html">ISO 8601</a>. Sin ella, el valor sale como un texto de fechas en JavaScript, como <code>Tue Mar 10 2026 05:03:47 GMT+0000</code>, que Elasticsearch no asigna como fecha.</p></li><li><p>El <code>view_count</code> usa la sintaxis <code>${{ }}</code> con <code>| plus: 0</code> para preservar el tipo numérico. Usar <code>{{ }}</code> lo mostraría como un texto, lo que impediría realizar operaciones matemáticas en el dashboard.</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3b94bb6c22c0253e/6a17dc6425daab37a508a11b/6d48c8784d5df6192e8b5175e69dbab5098194bc-919x774.png" alt="" /><p><em>La UI te permite depurar fácilmente cada uno de los pasos del flujo de trabajo.</em></p><h2>Paso 4: Crea el dashboard de estadísticas</h2><p>Una vez que el flujo de trabajo se haya ejecutado varias veces y se hayan recopilado los datos, crea un nuevo dashboard en Kibana usando la Data view de vistas del dashboard.</p><p>Algunos paneles para empezar:</p><ul><li><p><strong>Los dashboards más vistos:</strong> Usa un <a href="https://www.elastic.co/docs/explore-analyze/visualize/charts/bar-charts"><strong>Gráfico de barras</strong></a> con <code>dashboard_name</code> en el eje X y <code>last_value(view_count)</code> en el eje Y. Aquí se muestra el número actual de vistas diarias por dashboard.</p></li><li><p><strong>Vistas a lo largo del tiempo:</strong> usa un <a href="https://www.elastic.co/docs/explore-analyze/visualize/charts/line-charts"><strong>gráfico de líneas</strong></a> con <code>captured_at</code> en el eje X y <code>last_value(view_count)</code> en el eje Y, desglosado por <code>dashboard_name</code>. Dado que cada ejecución agrega un nuevo documento, usa el último valor para obtener el recuento máximo por cubetas de tiempo en lugar de sumar duplicados.</p></li><li><p><strong>Snapshot actual:</strong> usa una <a href="https://www.elastic.co/docs/explore-analyze/visualize/charts/tables"><strong>tabla de datos</strong></a> con el <code>captured_at</code> más reciente para mostrar los recuentos de vistas más recientes en todos los dashboards.</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt18d0390e0526b215/6a17dc65a292991da7d02b79/e245b95f67daf76a2aaf4cb9df2c75ef4cfef582-1462x747.png" alt="" /><p>Dado que cada flujo de trabajo crea un nuevo documento, puedes filtrar por intervalo de tiempo para analizar la actividad en períodos específicos, comparar semana a semana o configurar alertas cuando un dashboard caiga por debajo de un umbral de visitas.</p><h2><strong>Conclusión</strong></h2><p>Elastic Flujos de trabajo es una buena opción para este tipo de recopilación periódica de datos porque tanto el origen (API de Kibana) como el destino (Elasticsearch) son nativos, lo que significa cero gestión de credenciales. El motor de flujo de trabajo maneja la autenticación automáticamente para los pasos <code>kibana.request</code> y <code>elasticsearch.request</code>, por lo que lo único que escribes es la lógica.</p><h2><strong>Recursos</strong></h2><ul><li><p><a href="https://www.elastic.co/docs/explore-analyze/workflows">Flujos de trabajo de Elastic</a></p></li><li><p><a href="https://www.elastic.co/docs/api/doc/kibana/">API de Kibana</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[Experiencia del desarrollador]]></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[Rastreador Elástico de Sitio web Abierto como código]]></title>
    <description><![CDATA[Aprende a usar GitHub Actions para gestionar configuraciones de Elastic Open Crawler, de modo que cada vez que enviemos cambios al repositorio, los cambios se apliquen automáticamente a la instancia desplegada del rastreador.]]></description>
    <content:encoded><![CDATA[<p>Con <a href="https://github.com/elastic/crawler">Elastic Open Sitio web Crawler</a> y su arquitectura basada en CLI, tener configuraciones de rastreador versionado y una pipeline CI/CD con pruebas locales ahora es bastante sencillo de lograr.</p><p>Tradicionalmente, gestionar los rastreadores era un proceso manual y propenso a errores. Implicaba editar configuraciones directamente en la interfaz y luchar con clonar configuraciones de rastreo, retrocesos, versionear y más. Tratar las configuraciones de rastreadores como código resuelve esto al proporcionar los mismos beneficios que esperamos en el desarrollo de software: repetibilidad, trazabilidad y automatización.</p><p>Este flujo de trabajo facilita la incorporación del Open Sitio web Crawler a tu pipeline CI/CD para rollbacks, copias de seguridad y migraciones, tareas que eran mucho más complicadas con los Elastic Crawlers anteriores, como el Elastic Sitio web Crawler o el App Search Crawler.</p><p>En este artículo, vamos a aprender cómo:</p><ul><li><p>Gestiona nuestras configuraciones de rastreo usando GitHub</p></li><li><p>Tener una configuración local para probar pipelines antes de desplegar</p></li><li><p>Crea una configuración de producción para ejecutar el rastreador sitio web con nuevos ajustes cada vez que enviemos cambios a nuestra rama principal</p></li></ul><p>Puedes encontrar el repositorio de <a href="https://github.com/llermaly/elastic-open-crawler-as-code"><em><strong>proyectos aquí</strong></em></a><em><strong>. </strong></em><em>Según escribo, estoy usando Elasticsearch 9.1.3 y Open Sitio web Crawler 0.4.2.</em></p><h2>Prerrequisitos</h2><ul><li><p>Escritorio Docker</p></li><li><p>Instancia de Elasticsearch</p></li><li><p>Máquina virtual con acceso SSH (por ejemplo, AWS EC2) y Docker instalados</p></li></ul><h2>Pasos</h2><ol><li><p>Estructura de carpetas</p></li><li><p>Configuración del orugador</p></li><li><p>Docker-compose (entorno local)</p></li><li><p>Acciones en Github</p></li><li><p>Pruebas locales</p></li><li><p>Desplegando a la producción</p></li><li><p>Realización de cambios y re-despliegue</p></li></ol><h2>Estructura de carpetas</h2><p>Para este proyecto, tendremos la siguiente estructura de archivos:</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>Configuración del orugador</h2><p>Bajo <code>crawler-config.yml,</code> pondremos lo siguiente:</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>Esto se rastreará desde <a href="https://web-scraping.dev/products">https://sitio web-scraping.dev/products</a>, un sitio simulado de productos. Solo rastrearemos las tres primeras páginas del producto. La configuración <code>max_crawl_depth</code> evitará que el rastreador descubra más páginas de las definidas como <code>seed_urls</code> al no abrir los enlaces que contienen.</p><p>Elasticsearch <code>host</code> y <code>api_key</code> se llenarán dinámicamente dependiendo del entorno en el que ejecutemos el script.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7d37b966aafbd3c1/6a17ef9842022946e929f6b9/f9831034e1c4ccb554d37bdd188f2824338355a0-890x624.png" alt="La página del producto de &quot;Box of Chocolate Candy&quot; del dominio sitio web-scraping.dev, un sitio simulado para probar sitio web scraping. La página muestra el título del producto, la imagen, la descripción y los elementos HTML para los botones de precio y compra." /><h2>Docker-compose (entorno local)</h2><p>Para la <code>docker-compose.yml,</code> local desplegaremos el rastreador y un único clúster Elasticsearch + Kibana, para poder visualizar fácilmente los resultados del <em><strong>rastreo antes</strong></em> de desplegarlos en producción.</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>Fíjate en cómo el rastreador espera hasta que Elasticsearch esté listo para ejecutar.</p><h2>Acciones en Github</h2><p>Ahora necesitamos crear una acción en GitHub que copie la nueva configuración y ejecute el rastreador en nuestra máquina virtual en cada envío a main. Esto garantiza que siempre tengamos la última configuración desplegada, sin tener que entrar manualmente en la máquina virtual para actualizar archivos y ejecutar el rastreador. Vamos a usar AWS EC2 como proveedor de máquinas virtuales.</p><p>El primer paso es agregar el host (<code>VM_HOST</code>), el usuario de la máquina (<code>VM_USER</code>), la clave SSH RSA (<code>VM_KEY</code>), el host de Elasticsearch (<code>ES_HOST</code>) y la clave API de Elasticsearch (<code>ES_API_KEY</code>) a los secretos de acción de GitHub:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb5a0fcfff9b7f997/6a17ef9a6df731d7d40a0fdf/e1075bc54151b4b94eac2a6bd2682e9997e6c709-1106x707.png" alt="Una página de configuración de &quot;acciones, secretos y variables&quot;, que muestra secretos del repositorio como VM_HOST, VM_KEY y VM_USER dentro de una interfaz sitio web." /><p>De este modo, la acción podrá acceder a nuestro servidor para copiar los archivos nuevos y ejecutar el rastreo.</p><p>Ahora, creemos nuestro archivo <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>Esta acción ejecutará los siguientes pasos cada vez que empujemos cambios en el archivo de configuración del rastreador:</p><ol><li><p>Llenar el host y la clave API de Elasticsearch en la configuración de yml</p></li><li><p>Copia la carpeta config a nuestra máquina virtual</p></li><li><p>Conéctate vía SSH a nuestra máquina virtual</p></li><li><p>Ejecuta el rastreo con la configuración que acabamos de copiar del repositorio</p></li></ol><h2>Pruebas locales</h2><p>Para probar nuestro rastreador localmente, creamos un script bash que llena el host de Elasticsearch con el local de Docker y comienza un rastreo. Puedes ejecutar <code>./local.sh</code> para ejecutarlo.</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>Veamos Kibana DevTools para confirmar que el<code> web-crawler-index</code> se rellenó correctamente:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt989660368fe14db8/6a17ef9b9da390c79ce46562/18551635e8265866e389a9632c4e4540958e4468-990x723.png" alt="Kibana DevTools para confirmar que el índice del rastreador sitio web está configurado correctamente." /><h2>Desplegando a la producción</h2><p>Ahora estamos listos para enviar a la rama principal, que desplegará el rastreador en tu máquina virtual y comenzará a enviar registros a tu instancia Serverless Elasticsearch.</p>git add .
git commit -m "First commit"
git push<p>Esto activará la Acción de GitHub, que ejecutará el script de despliegue dentro de la máquina virtual y comenzará a rastrear.</p><p>Puedes confirmar que la acción se ejecutó yendo al repositorio de GitHub y visitando la pestaña "Acciones":</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt986f1a4e4f3288d2/6a17ef9c7f6f1584fdc09c10/67ba3a7164d7a8049fe5661264820826cb18ed64-667x325.png" alt="Las acciones se despliegan en la pestaña EC2 de un repositorio de GitHub." /><h2>Realización de cambios y re-despliegue</h2><p>Algo que quizá notaste es que el <code>price</code> de cada producto forma parte del cuerpo del documento. Lo ideal sería almacenar el precio en un campo aparte para poder aplicar filtros sobre él.</p><p>Vamos a agregar este cambio al archivo <code>crawler.yml</code> para usar <a href="https://github.com/elastic/crawler/blob/main/docs/features/EXTRACTION_RULES.md">reglas de extracción</a> que extraigan el precio de la clase CSS de <code>product-price</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>También vemos que el precio incluye un signo de dólar (<code>$</code>), que debemos eliminar si queremos hacer consultas por rango. Podemos usar una canalización de ingesta para eso. Ten en cuenta que lo estamos haciendo referencia en nuestro nuevo archivo de configuración del rastreador arriba:</p>PUT _ingest/pipeline/pricing-pipeline
{
  "processors": [
    {
      "script": {
        "source": """
                ctx['price'] = ctx['price'].replace("$","")
            """
      }
    }
  ]
}<p>Podemos ejecutar ese comando en nuestro clúster de Elasticsearch en producción. Para el desarrollo, al ser efímero, podemos hacer que la creación de pipeline forme parte del archivo <code>docker-compose.yml</code> agregando el siguiente servicio. Ten en cuenta que también agregamos un <code>depends_on</code> al servicio de rastreo para que empiece después de que la tubería se creó con éxito.</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>Ahora vamos a ejecutar <code>`./local.sh`</code> para ver el cambio localmente:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0f390aedf67cb4fe/6a17ef9eaf47b62bd2cde05b/dc1801599344a9f69f072b07ff828c4ba3815d7b-738x473.png" alt="Estoy usando './local.sh' para ver el cambio de precio localmente." /><p>¡Bien! Ahora impulsemos el cambio:</p>git add crawler-config.yml
git commit -m "added price CSS selector"
git push<p>Para confirmar que todo funciona, puedes comprobar tu Kibana de producción, que debería reflejar los cambios y mostrar el precio como un nuevo campo sin el signo del dólar.</p><h2>Conclusión</h2><p>El Elastic Open Sitio web Crawler te permite gestionar tu rastreador como código, lo que significa que puedes automatizar toda la pipeline —desde el desarrollo hasta el despliegue— y agregar entornos locales efímeros y pruebas programáticas contra los datos rastreados, por nombrar algunos ejemplos.</p><p>Se te invita a clonar el repositorio oficial y empezar a indexar tus propios datos usando este flujo de trabajo. También puedes leer <a href="https://www.elastic.co/search-labs/blog/semantic-search-open-crawler">este artículo</a> para aprender a realizar búsqueda semántica en índices producidos por el rastreador.</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[Datos de índice]]></category>
    <category><![CDATA[Operaciones]]></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[Crear una aplicación de búsqueda con Blazor y Elasticsearch]]></title>
    <description><![CDATA[Aprende a construir una aplicación de búsqueda usando Blazor y Elasticsearch, y cómo emplear el cliente Elasticsearch .NET para búsqueda híbrida.]]></description>
    <content:encoded><![CDATA[<p>En este artículo, aprenderás cómo aprovechar tus habilidades en C# para crear una aplicación de búsqueda usando Blazor y Elasticsearch. Vamos a usar el cliente <a href="https://www.elastic.co/guide/en/elasticsearch/client/net-api/current/introduction.html">Elasticsearch .NET</a> para ejecutar consultas <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/full-text-queries.html">de texto completo</a>, <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-search.html">semánticas</a> y <a href="https://www.elastic.co/search-labs/tutorials/search-tutorial/vector-search/hybrid-search">búsquedas híbridas</a> .</p><p><strong>NOTA</strong> Si conoces la versión anterior <a href="https://www.elastic.co/guide/en/elasticsearch/client/net-api/7.17/nest.html">del cliente</a> Nest de Elasticsearch en C#, lee esta <a href="https://www.elastic.co/search-labs/blog/net-client-evolution">entrada del blog</a> sobre la deprecación del cliente NEST y las nuevas funciones. <em>NEST fue la generación anterior del cliente .NET que fue reemplazado por el </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="Build app Blazor: esre con diagrama Blazor" /><ul><li><p><a href="https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor#what-is-blazor?">¿Qué es Blazor?</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor#what-is-esre?">¿Qué es ESRE?</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor#configuring-elser">Configuración de ELSER</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor#indexing-data">Datos de indexación</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">Aplicación en edificios</a></p></li></ul><h2>¿Qué es Blazor?</h2><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt91278ae422883e28/6a17f6592f4a5c3213fa8a95/622741915d016b68bf94f742332d10d736d60052-707x461.png" alt="Servidor Blazor" /><p><a href="https://dotnet.microsoft.com/en-us/apps/aspnet/web-apps/blazor">Blazor</a> es un framework sitio web de código abierto basado en HTML, CSS y C# creado por Microsoft para permitir a los desarrolladores crear aplicaciones sitio web que se ejecuten en el cliente o en el servidor. Blazor también permite crear componentes reutilizables para construir aplicaciones más rápido; permite a los desarrolladores construir la vista HTML y las acciones en C# dentro del mismo archivo, lo que ayuda a mantener un código legible y limpio. Además, con Blazor Hybrid puedes crear aplicaciones móviles nativas accediendo a las capacidades nativas de la plataforma mediante código .NET.</p><p>Algunas de las características que hacen de Blazor un excelente marco para trabajar:</p><ul><li><p>Opciones de renderizado tanto en el lado servidor como en el cliente</p></li><li><p>Componentes reutilizables de la interfaz</p></li><li><p>Actualizaciones en tiempo real con SignalR</p></li><li><p>Gestión del estado integrada</p></li><li><p>Sistema de enrutamiento integrado</p></li><li><p>Comprobaciones fuertes de tipado y tiempo de compilación</p></li></ul><h3>¿Por qué Blazor?</h3><p>Blazor ofrece varios beneficios sobre otros frameworks y librerías: permite a los desarrolladores usar C# tanto para el código cliente como para el servidor, proporcionando una fuerte verificación de tipado y compilación que mejora la fiabilidad. Se integra perfectamente con el ecosistema .NET, permitiendo la reutilización de librerías y herramientas .NET, y ofrece un soporte robusto para depuración.</p><h2>¿Qué es ESRE?</h2><p><a href="https://www.elastic.co/elasticsearch/elasticsearch-relevance-engine">El Elasticsearch Relevance Engine™ (ESRE)</a> es un conjunto de <a href="https://www.elastic.co/guide/en/esre/current/learn.html">herramientas para construir aplicaciones de búsqueda</a> empleando aprendizaje automático e inteligencia artificial sobre el poderoso motor de búsqueda Elasticsearch.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf313ba986de01b93/6a17d851505ac306fcad8975/4c0f2645ed1c27fe3ef61a1a9126adadfd8d5368-721x421.png" alt="Esre" /><p>Para saber más sobre ESRE, puedes leer nuestra interesante entrada de blog <a href="https://www.elastic.co/search-labs/blog/introducing-elasticsearch-relevance-engine-esre">aquí</a></p><h2>Configuración de ELSER</h2><p>Para aprovechar las capacidades <a href="https://www.elastic.co/elasticsearch/elasticsearch-relevance-engine">ESRE</a> de Elastic, vamos a emplear <a href="https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-elser.html">ELSER</a> como nuestro proveedor de modelos.</p><p><em>Nota: para usar los modelos ELSER de Elasticsearch, debes tener una licencia Platinum o Enterprise, y disponer de un nodo mínimo dedicado de Machine Lerning (ML) de 4GB de tamaño. Lee más</em> <a href="https://www.elastic.co/guide/en/machine-learning/8.15/ml-nlp-elser.html#elser-req"><em>sobre esto aquí.</em></a></p><p>Empieza creando el punto final de inferencia:</p>PUT _inference/sparse_embedding/my-elser-model
{
  "service": "elser",
  "service_settings": {
    "num_allocations": 1,
    "num_threads": 1
  }
}<p>Si es la primera vez que usas ELSER, puede que te encuentres con un error 502 de Mal Gateway mientras el modelo se carga en segundo plano. Puedes consultar el estado del modelo en <code>Machine Learning &gt; Trained Models</code> en Kibana. Una vez desplegado, puedes pasar al siguiente paso.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltee295a3516338302/6a17f65b414c640deb94531d/a7ff94b72d337cde892165d744b9f42fba702a87-1440x649.png" alt="Modelos capacitados comprobando" /><h2>Datos de indexación</h2><p>Puedes descargar el conjunto <a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/esre-with-blazor/books.zip">de datos aquí</a> y luego importar los datos usando Kibana. Para ello, ve a la página principal y haz clic en "Subir datos". Luego, sube el archivo y haz clic <code>Import</code>. Por último, ve a la pestaña <code>Advanced</code> y pega los siguientes mapeos:</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="Datos importados" /><p>Vamos a crear un índice capaz de ejecutar consultas semánticas y de texto completo. El tipo <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-text.html">de campo semantic_text</a> se encargará del fragmento de datos y la incrustación. <em>Ten en cuenta que estamos indexando</em> <em><code>longDescription</code></em> <em>como</em> <em><code>semantic_text</code></em><em>, puedes usar copy_to si quieres indexar un campo tanto como</em> <em><code>semantic_text</code></em> <em>como texto'</em>.</p><h2>Construcción de la app con Blazor &amp; Elasticsearch</h2><h3>Clave API</h3><p>Lo primero que tenemos que hacer es crear una clave API para autenticar nuestras solicitudes a Elasticsearch. La clave API debe ser de solo lectura y solo permitir consultar el índice de <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>Verás algo así:
</p>{
  "id": "XXXXXXXXXXXXXXXXXXXXXXXX",
  "name": "books-blazor-key",
  "api_key": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  "encoded": "XXXXXXXXXXXXXXXXXXXXXXXX=="
}<p>Almacena el valor del campo de respuesta <code>encoded</code> cuando sea necesario más adelante. Si usas <a href="https://www.elastic.co/cloud/">Elastic Cloud</a>, también necesitarás tu ID de la nube. (Puedes <a href="https://www.elastic.co/search-labs/tutorials/install-elasticsearch/elastic-cloud#finding-your-cloud-id">encontrarlo aquí</a>).</p><h4>Creación del proyecto Blazor</h4><p>Empieza instalando Blazor y creando un proyecto de muestra siguiendo las <a href="https://dotnet.microsoft.com/en-us/learn/aspnet/blazor-tutorial/install">instrucciones oficiales</a>.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb5c20b896fe1e98e/6a17f65f6317301566585bff/f5720867d960c91fd1b3c4ae9be174e062e6b2fc-975x830.png" alt="Tutorial de Blazor - Creando el proyecto Blazor" /><p>Una vez que tengas el proyecto creado, la estructura de carpetas y los archivos deberían ver así:</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>La aplicación plantilla incluye <a href="https://blog.getbootstrap.com/2021/08/04/bootstrap-5-1-0/">Bootstrap v5.1.0</a> para peinar.</p><p>Termina la configuración del proyecto instalando el cliente <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>Una vez que completes este paso, tu página debería ver así:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3b90e394ded7ff56/6a17f660faa913d49f93ca10/ef9d2186c2cf49e78a307c4aa69c51632e84578d-940x529.png" alt="Blazor, hola mundo" /><h3>Estructura de carpetas</h3><p>Ahora, vamos a organizar nuestras carpetas de la siguiente manera:</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>Archivos explicados:</p><ul><li><p>Components/Pages/Search.razor: página principal que contiene la barra de búsqueda, los resultados y los filtros.</p></li><li><p>Componentes/Páginas/Search.razor.css: estilos de página.</p></li><li><p>Components/Elasticsearch/SearchBar.razor: componente de la barra de búsqueda.</p></li><li><p>Components/Elasticsearch/Results.razor: componente de resultados.</p></li><li><p>Components/Elasticsearch/Facet.razor: componente de filtros.</p></li><li><p>Components/Svg/GlassIcon.razor: icono de búsqueda.</p></li><li><p>Components/_Imports.razor: esto importará todos los componentes.</p></li><li><p>Modelos/Book.cs: esto almacenará el esquema del campo libro.</p></li><li><p>Modelos/Response.cs: esto almacenará el esquema de respuesta, incluyendo los resultados de búsqueda, facetas y resultados totales.</p></li><li><p>Servicios/ElasticsearchService.cs: Servicio Elasticsearch. Se encargará de la conexión y las consultas a Elasticsearch.</p></li></ul><h4>Configuración inicial</h4><p>Empecemos con una limpieza.</p><p>Elimina los archivos:</p><ul><li><p>Componentes/Páginas/Counter.razor</p></li><li><p>Componentes/Páginas/Weather.razor</p></li><li><p>Componentes/Páginas/Home.razor</p></li><li><p>Componentes/Layout/NavMenu.razor</p></li><li><p>Componentes/Diseño/NavMenu.razor.css</p></li></ul><p>Revisa el archivo <code>/Components/_Imports.razor</code> . Deberías tener las siguientes importaciones:</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>Integración de Elastic en el proyecto</h4><p>Ahora, importemos los componentes de 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>Vamos a eliminar la barra lateral predeterminada para tener más espacio para nuestra aplicación eliminándola del archivo <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="Barra de navegación eliminada del diseño" /><p>Ahora introduzcamos las credenciales de Elasticsearch para los <a href="https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-8.0&amp;tabs=linux#secret-manager">secretos de usuario</a>:</p>dotnet user-secrets init
dotnet user-secrets set ElasticsearchCloudId "your Cloud ID"
dotnet user-secrets set ElasticsearchApiKey "your API Key"<p>Con este enfoque, .Net 8 almacena datos sensibles en una ubicación separada, fuera de la carpeta del proyecto, y los hace accesibles mediante la interfaz <code>IConfiguration</code> . Estas variables estarán disponibles para cualquier proyecto .Net que emplee los mismos secretos de usuario.</p><p>Luego, modifiquemos el archivo <code>Program.cs</code> para leer los secretos y montar el cliente Elasticsearch:</p><p>Primero, importa las bibliotecas necesarias:</p>using BlazorApp.Services;
using Elastic.Clients.Elasticsearch;
using Elastic.Transport;<ul><li><p>BlazorApp.Services: contiene el servicio Elasticsearch.</p></li><li><p>Elastic.Clients.Elasticsearch: importa la biblioteca cliente .Net 8 de Elasticsearch.</p></li><li><p>Elastic.Transport: importa la biblioteca de transporte Elasticsearch, que nos permite usar la clase ApiKey para autenticar nuestras solicitudes.</p></li></ul><p>Segundo, inserta el siguiente código antes de la línea <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>Este código leerá las credenciales de Elasticsearch de los secretos de usuario y creará una instancia cliente de Elasticsearch.</p><p>Tras la inicialización del cliente ElasticSearch, agrega la siguiente línea para registrar el servicio Elasticsearch:</p>builder.Services.AddScoped&lt;ElasticsearchService&gt;();<p>El siguiente paso será construir la lógica de búsqueda en el archivo <code>/Services/ElasticsearchService.cs</code> :</p><p>Primero, importa las bibliotecas y modelos necesarios:</p>using BlazorApp.Models;
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.QueryDsl;<p>Segundo, agregar la clase <code>ElasticsearchService</code>, constructor y variables:</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>Configuración de búsqueda</h4><p>Ahora, vamos a construir nuestra lógica de búsqueda:</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> construirá los filtros para la consulta de búsqueda usando las facetas seleccionadas por el usuario.</p></li><li><p><code>BuildHybridQuery</code> construirá una consulta <a href="https://www.elastic.co/search-labs/tutorials/search-tutorial/vector-search/hybrid-search">de búsqueda híbrida</a> que combine texto completo y búsqueda semántica.</p></li></ul><p>A continuación, agrega el método de búsqueda:</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>: realizará la búsqueda usando la consulta híbrida y devolverá los resultados incluidas agregaciones para construir las facetas.</p></li><li><p><code>FormatFacets</code>: formateará la respuesta de agregación en un diccionario.</p></li><li><p><code>ConvertFacetDictionary</code>: convertirá el diccionario de facetas en un formato más legible.</p></li></ul><p>El siguiente paso es crear los modelos que representarán los datos devueltos en el <code>hits</code> de la consulta de Elasticsearch que se imprimirán como resultados en nuestra página de búsqueda.</p><p>Comenzamos creando el <code>/Models/Book.cs</code> de archivo y agregando lo siguiente:</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>Luego, configurar la respuesta Elastic en el archivo <code>/Models/Response.cs</code> y agregar lo siguiente:</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>Configuración de una interfaz básica</h4><p>A continuación, agrega el componente Barra de búsqueda. En el archivo <code>/Components/Elasticsearch/SearchBar.razor</code> y agrega lo siguiente:</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>Este componente contiene una barra de búsqueda y un botón para realizar la búsqueda.</p><p>Blazor ofrece una gran flexibilidad al permitir generar HTML dinámicamente usando código C# dentro del mismo archivo.</p><p>Después, en el archivo <code>/Components/Elasticsearch/Results.razor</code> construiremos el componente de resultados que mostrará los resultados de búsqueda:</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>Finalmente, necesitaremos crear facetas para filtrar los resultados de búsqueda.</p><p><em>Nota: Los facetos son filtros que permiten a los usuarios reducir los resultados de búsqueda en función de atributos o categorías específicas, como el tipo de producto, el rango de precio o la marca. Estos filtros suelen presentar como opciones clicables, a menudo en forma de casillas para marcar, para ayudar a los usuarios a refinar su búsqueda y encontrar resultados relevantes con mayor facilidad. En el contexto de Elasticsearch, las facetas se crean mediante</em> <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html"><em>agregaciones</em></a><em>.</em></p><p>Configuramos facetas poniendo el siguiente código en el archivo <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>Este componente lee de una agregación <code>terms</code> en los campos <code>author</code>, <code>categories</code>y <code>status</code> , y luego produce una lista de filtros para enviar de vuelta a Elasticsearch.</p><p>Ahora, vamos a juntar todo.</p><p>En <code>/Components/Pages/Search.razor</code> expediente:</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>¡Nuestra página funciona!</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc750e55fff43de4b/6a17f6636864a4ab08b68931/5f0f25cb029a34df7f577a9f307278226ed07c3f-816x473.png" alt="Ejemplo de página en Blazor" /><p>Como puedes ver, la página es funcional pero carece de estilos. Vamos a agregar algo de CSS para que parezca más organizado y receptivo.</p><p>Empecemos a reemplazar los estilos de distribución. En el archivo <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>Agrega los estilos de la página de búsqueda en el archivo <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>Nuestra página empieza a ver mejor:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3ecc49f404f18591/6a17f6654b055d959b432384/3dadd7ade2dd9070300a4e0514a4da2ae9cc9fb9-817x473.png" alt="Página Blazor luego de agregar estilos para la página de búsqueda" /><p>Vamos a darle los toques finales:</p><p>Crea los siguientes archivos:</p><ul><li><p>Componentes/Elasticsearch/Facet.razor.css</p></li><li><p>Components/Elasticsearch/Results.razor.css</p></li></ul><p>Y agrega los estilos para <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>Para <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>Resultado final:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt78bbd8e6f9e23988/6a17f6664b055d2ed8432388/3e68ec4a38775fdfbeaa1a990e6a1e11dfb081d8-816x472.png" alt="Resultado final de la creación de la página de la app Blazor" /><p>Para ejecutar la aplicación puedes usar el siguiente comando:</p><p><code>dotnet watch</code></p><p>¡Lo conseguiste! Ahora puedes buscar libros en tu índice de Elasticsearch usando la barra de búsqueda y filtrar los resultados por autor, categoría y estado.</p><h3>Realización de búsqueda en texto completo y semántica</h3><p>Por defecto, nuestra app realiza una <a href="https://www.elastic.co/search-labs/tutorials/search-tutorial/vector-search/hybrid-search">búsqueda híbrida</a> usando tanto <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html">texto completo</a> <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-semantic-query.html">como búsqueda semántica</a>. Puedes cambiar la lógica de búsqueda creando dos métodos separados, uno para texto completo y otro para búsqueda semántica, y luego seleccionar uno para construir la consulta basar en la entrada del usuario.</p><p>Agrega los siguientes métodos a la clase <code>ElasticsearchService</code> en el archivo <code>/Services/ElasticsearchService.cs</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>Ambos métodos funcionan de forma similar al método <code>BuildHybridQuery</code> , pero solo realizan búsqueda en texto completo o semántica.</p><p>Puedes modificar el método <code>SearchBooksAsync</code> para usar el método de búsqueda seleccionado:</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>Puedes encontrar la solicitud completa <a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/esre-with-blazor">aquí</a></p><h2>Conclusión</h2><p>Blazor es un framework eficaz que te permite crear aplicaciones sitio web usando C#. Elasticsearch es un poderoso motor de búsqueda que te permite crear aplicaciones de búsqueda. Combinando ambos, puedes construir fácilmente aplicaciones de búsqueda robustas, aprovechando el poder de ESRE para crear una <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-search.html">experiencia de búsqueda semántica</a> en poco tiempo.</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[Base de datos vectorial]]></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>