<?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/pt/search-labs/author/gustavo-llermaly</link>
    </image>
    <link>https://www.elastic.co/pt/search-labs/author/gustavo-llermaly</link>
    <atom:link href="https://www.elastic.co/pt/search-labs/rss/author/gustavo-llermaly.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[pt]]></language>
    <lastBuildDate>Mon, 21 Sep 2026 13:39:51 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Monitorando as visualizações do dashboard do Kibana com o Elastic Workflows]]></title>
    <description><![CDATA[Aprenda a usar o Elastic Workflows para coletar métricas de visualização do dashboard do Kibana a cada 30 minutos e indexá-las no Elasticsearch, para que você possa criar análises e visualizações personalizadas com base em seus próprios dados.]]></description>
    <content:encoded><![CDATA[<p>O <a href="https://www.elastic.co/kibana">Kibana</a> rastreia quantas vezes cada dashboard é visualizado, mas esses dados não são expostos nativamente em nenhum dashboard integrado. Neste artigo, vamos usar o <strong>Elastic Workflows</strong> para coletar automaticamente esses dados a cada 30 minutos e indexá-los no Elasticsearch, para que possamos criar nossa própria analítica sobre eles.</p><p>O <a href="https://www.elastic.co/docs/explore-analyze/workflows">Elastic Workflows</a> é um mecanismo de automação integrado dentro do Kibana que permite definir processos de várias etapas usando uma simples configuração YAML. Cada fluxo de trabalho pode ser acionado em um cronograma ou evento ou como uma ferramenta no <a href="https://www.elastic.co/docs/explore-analyze/ai-features/elastic-agent-builder">Elastic Agent Builder</a>, e cada etapa pode chamar APIs do Kibana, consultar o Elasticsearch ou transformar dados.</p><p>Vamos usar as contagens de visualização de dashboards como um exemplo concreto, mas o mesmo padrão se aplica a qualquer métrica exposta pela API de objetos salvos do Kibana.</p><h2>Pré-requisitos</h2><ul><li><p><a href="https://www.elastic.co/cloud">Elastic Cloud</a> ou cluster <a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed">autogerenciado </a>executando a versão 9.3</p></li><li><p><a href="https://www.elastic.co/docs/explore-analyze/workflows/get-started#workflows-prerequisites">Fluxos de trabalho ativados</a> (Configurações avançadas)</p></li></ul><h2>Passo 1: explorar os dados brutos no <a href="https://www.elastic.co/docs/explore-analyze/query-filter/tools/console">Dev Tools</a></h2><p>Antes de construir qualquer coisa, vamos entender quais dados temos. O Kibana armazena a maior parte de sua configuração e metadados como <a href="https://www.elastic.co/docs/api/doc/kibana/group/endpoint-saved-objects">objetos salvos</a> em um índice interno dedicado. Uma das coisas que o Kibana monitora dessa forma são as contagens de visualizações do dashboard, usando um tipo especial de objeto salvo chamado contadores de uso. Você pode consultá-los diretamente pelas Ferramentas de Desenvolvimento:</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>A resposta tem aparência semelhante a esta:</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>O campo <code>counterName</code> é o ID do dashboard, e <code>count</code> é a contagem cumulativa de visualizações para aquele dashboard naquele dia específico. Kibana cria um objeto contador por dashboard por dia; você pode ver o sufixo de data no ID do objeto (... visualizado:servidor:20260310). A contagem cresce ao longo do dia à medida que os usuários abrem o dashboard.</p><p>Em vez de replicar esse modelo de documento diário em nosso índice, criaremos um documento por execução de fluxo de trabalho. Cada documento registra quantas visualizações aquele dashboard acumulou no dia no momento da captura.</p><h2>Passo 2: criar o índice de destino</h2><p>Precisamos de um índice para armazenar os snapshots da vista do nosso dashboard. O comando a seguir cria com mapeamentos explícitos para que possamos agregar e visualizar depois. Execute isso nas ferramentas de desenvolvimento:</p>PUT dashboard-views
{
  "mappings": {
    "properties": {
      "captured_at": {
        "type": "date"
      },
      "dashboard_id": {
        "type": "keyword"
      },
      "dashboard_name": {
        "type": "keyword"
      },
      "view_count": {
        "type": "integer"
      }
    }
  }
}<p>O uso de mapeamentos <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/keyword"><code>keyword</code></a> para IDs e nomes permite <a href="https://www.elastic.co/docs/explore-analyze/query-filter/aggregations">agregações</a>. Usar <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/number"><code>integer</code></a> para <code>view_count</code> é um padrão seguro, já que o Kibana reinicia o contador diariamente e atingir o limite de 32 bits (mais de 2 bilhões de visualizações em um único dia) não é uma preocupação realista. Ainda permite operações 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> e <a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-metrics-min-aggregation"><code>min</code></a>, entre outras.</p><h2>Passo 3: Crie o fluxo de trabalho</h2><p>Acesse <strong>Stack Management &gt; Fluxo de trabalho &gt; Novo Fluxo de Trabalho</strong> e cole a seguinte configuração YAML do fluxo de trabalho:</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>Na próxima seção, vamos analisar o fluxo de trabalho passo a passo.</p><h3>Como funciona o fluxo de trabalho</h3><h4>Gatilhos</h4><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7672aa533b4bc9ed/6a17dc5b420229d07c29f4d4/5670991d65c64ee833924225c2d375a1be868b13-325x162.png" alt=" Gatilhos agendados" /><p>O fluxo de trabalho é executado com um gatilho programado a cada 30 minutos. Isso nos fornece dados de séries temporais sem sobrecarregar a API.</p><h4>buscar_visualizações_do_painel</h4><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltab2a16f1f11304ea/6a17dc5d25daab26f608a117/66eaec147c3d01c524c67cf1c7f663ac56a3259d-812x215.png" alt=" Buscar o dashboard" /><p>Usa <code>kibana.request</code> para chamar a API de objetos salvos do Kibana. Não é necessário configurar autenticação: o motor de fluxo de trabalho anexa automaticamente os cabeçalhos corretos com base no contexto de execução.</p><h4>index_each_dashboard (foreach)</h4><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte6b2611b0216555e/6a17dc5f445de95b584cffe5/aad45e8aed8dc81ded6260cd6199ff78dcffe3b4-1892x290.png" alt="Indexar cada dashboard" /><p>Itera sobre o array <a href="https://www.elastic.co/docs/api/doc/kibana/group/endpoint-saved-objects"><code>saved_objects</code></a> retornado pela etapa anterior. O item atual em cada iteração está disponível como <code>foreach.item</code>. Dentro do loop, executamos duas etapas aninhadas 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=": Buscar nome do dashboard" /><p>Resolve o título do dashboard legível por humanos chamando <code>GET /api/saved_objects/dashboard/{id}</code>. Adicionamos <code>on-failure: continue: true</code> para que, se um dashboard for excluído mas ainda tiver contadores de visualização, o loop continue em vez de falhar toda a execução.</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=" Solicitação do Elasticsearch" /><p>Indexa cada documento usando <code>POST /dashboard-views/_doc</code> (sem um ID explícito), o que permite que o Elasticsearch gere IDs automaticamente. Isso cria um novo documento a cada execução, construindo um histórico de contagens de visualizações ao longo do tempo, em vez de sobrescrever o snapshot anterior.</p><p>Duas coisas que valem a pena notar:</p><ul><li><p>O campo <code>captured_at</code> usa o filtro de data para formatar o carimbo de data/hora como <a href="https://www.iso.org/iso-8601-date-and-time-format.html">ISO 8601</a>. Sem isso, o valor aparece como uma string de data em JavaScript, como <code>Tue Mar 10 2026 05:03:47 GMT+0000</code>, que o Elasticsearch não mapeia como data.</p></li><li><p>O <code>view_count</code> usa a sintaxe <code>${{ }}</code> com <code>| plus: 0</code> para preservar o tipo numérico. Usar <code>{{ }}</code> o renderizaria como uma string, o que impediria operações matemáticas no dashboard.</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3b94bb6c22c0253e/6a17dc6425daab37a508a11b/6d48c8784d5df6192e8b5175e69dbab5098194bc-919x774.png" alt="" /><p><em>A UI permite que você depure cada uma das etapas do fluxo de trabalho.</em></p><h2>Etapa 4: Crie o dashboard de estatísticas</h2><p>Depois que o fluxo de trabalho for executado algumas vezes e os dados forem coletados, crie um novo dashboard no Kibana usando a Data view dashboard-views.</p><p>Alguns painéis para começar:</p><ul><li><p><strong>Principais dashboards por visualizações:</strong> use um <a href="https://www.elastic.co/docs/explore-analyze/visualize/charts/bar-charts"><strong>gráfico de barras</strong></a> com <code>dashboard_name</code> no eixo X e <code>last_value(view_count)</code> no eixo Y. Isso mostra a contagem diária atual de visualizações por dashboard.</p></li><li><p><strong>Visualizações ao longo do tempo:</strong> use um <a href="https://www.elastic.co/docs/explore-analyze/visualize/charts/line-charts"><strong>gráfico de linhas</strong></a> com <code>captured_at</code> no eixo X e <code>last_value(view_count)</code> no eixo Y, dividido por <code>dashboard_name</code>. Como cada execução adiciona um novo documento, use o último valor para obter a contagem de picos por buckets, em vez de somar duplicados.</p></li><li><p><strong>Snapshot atual:</strong> use uma <a href="https://www.elastic.co/docs/explore-analyze/visualize/charts/tables"><strong>tabela de dados</strong></a> com os <code>captured_at</code> mais recentes para mostrar as contagens de visualizações mais recentes em todos os dashboards.</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt18d0390e0526b215/6a17dc65a292991da7d02b79/e245b95f67daf76a2aaf4cb9df2c75ef4cfef582-1462x747.png" alt="" /><p>Como cada fluxo de trabalho cria um novo documento, você pode filtrar por faixa de tempo para analisar a atividade em períodos específicos, comparar semana a semana ou criar alertas quando um dashboard cair abaixo de um limite de visualização.</p><h2><strong>Conclusão</strong></h2><p>O Elastic Workflows é uma boa opção para esse tipo de coleta periódica de dados porque tanto a fonte (Kibana API) quanto o destino (Elasticsearch) são nativos, o que significa zero gerenciamento de credenciais. O motor de fluxo de trabalho lida automaticamente com autenticação para <code>kibana.request</code> e <code>elasticsearch.request</code> etapas, então a única coisa que você escreve é a lógica.</p><h2><strong>Recursos</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/">API do 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[Experiência do Desenvolvedor]]></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 como código]]></title>
    <description><![CDATA[Aprenda a usar o GitHub Actions para gerenciar as configurações do Elastic Open Crawler, de forma que, sempre que enviarmos alterações para o repositório, elas sejam aplicadas automaticamente à instância implantada do crawler.]]></description>
    <content:encoded><![CDATA[<p>Com <a href="https://github.com/elastic/crawler">o Elastic Open Web Crawler</a> e sua arquitetura orientada por linha de comando, ter configurações de crawler versionadas e um pipeline de CI/CD com testes locais agora é bastante simples de se obter.</p><p>Tradicionalmente, o gerenciamento de rastreadores era um processo manual e propenso a erros. Isso envolvia editar configurações diretamente na interface do usuário e ter dificuldades com a clonagem de configurações de rastreamento, reversão, controle de versão e muito mais. Tratar as configurações do rastreador como código resolve isso, proporcionando os mesmos benefícios que esperamos no desenvolvimento de software: repetibilidade, rastreabilidade e automação.</p><p>Esse fluxo de trabalho facilita a integração do Open Web Crawler ao seu pipeline de CI/CD para reversões, backups e migrações — tarefas que eram muito mais complicadas com versões anteriores do Elastic Crawler, como o Elastic Web Crawler ou o App Search Crawler.</p><p>Neste artigo, vamos aprender como:</p><ul><li><p>Gerencie nossas configurações de rastreamento usando o GitHub.</p></li><li><p>Ter um ambiente local para testar pipelines antes da implantação.</p></li><li><p>Criar um ambiente de produção para executar o rastreador web com novas configurações sempre que enviarmos alterações para nossa branch principal.</p></li></ul><p>Você pode encontrar o repositório do projeto <a href="https://github.com/llermaly/elastic-open-crawler-as-code"><em><strong>aqui</strong></em></a><em><strong>. </strong></em><em>No momento em que escrevo, estou usando o Elasticsearch 9.1.3 e o Open Web Crawler 0.4.2.</em></p><h2>Pré-requisitos</h2><ul><li><p>Docker Desktop</p></li><li><p>instância do Elasticsearch</p></li><li><p>Máquina virtual com acesso SSH (por exemplo, AWS EC2) e Docker instalado.</p></li></ul><h2>Etapas</h2><ol><li><p>Estrutura de pastas</p></li><li><p>Configuração do rastreador</p></li><li><p>Arquivo Docker-compose (ambiente local)</p></li><li><p>Ações do GitHub</p></li><li><p>Testando localmente</p></li><li><p>Implantação em produção</p></li><li><p>Realizar alterações e redistribuir</p></li></ol><h2>Estrutura de pastas</h2><p>Para este projeto, teremos a seguinte estrutura de arquivos:</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>Configuração do rastreador</h2><p>Em <code>crawler-config.yml,</code> colocaremos o seguinte:</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>Este script irá extrair dados de <a href="https://web-scraping.dev/products">https://web-scraping.dev/products</a>, um site fictício para produtos. Iremos rastrear apenas as três primeiras páginas de produtos. A configuração <code>max_crawl_depth</code> impedirá que o rastreador descubra mais páginas do que as definidas como <code>seed_urls</code> , não abrindo os links dentro delas.</p><p>Elasticsearch <code>host</code> e <code>api_key</code> serão preenchidos dinamicamente dependendo do ambiente em que estamos executando o script.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7d37b966aafbd3c1/6a17ef9842022946e929f6b9/f9831034e1c4ccb554d37bdd188f2824338355a0-890x624.png" alt="Página do produto &quot;Caixa de Balas de Chocolate&quot; do domínio web-scraping.dev, um site simulado para testes de web scraping. A página exibe o título do produto, a imagem, a descrição e os elementos HTML para o preço e os botões de compra." /><h2>Arquivo Docker-compose (ambiente local)</h2><p>Para o ambiente local <code>docker-compose.yml,</code> implantaremos o rastreador e um único cluster Elasticsearch + Kibana, para que possamos visualizar facilmente os resultados do rastreamento <em><strong>antes</strong></em> da implantação em produção.</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>Observe como o rastreador aguarda até que o Elasticsearch esteja pronto para ser executado.</p><h2>Ações do GitHub</h2><p>Agora precisamos criar uma ação do GitHub que copie as novas configurações e execute o rastreador em nossa máquina virtual a cada push para o repositório principal. Isso garante que sempre tenhamos a configuração mais recente implantada, sem precisar entrar manualmente na máquina virtual para atualizar arquivos e executar o rastreador. Vamos usar o AWS EC2 como provedor de máquinas virtuais.</p><p>O primeiro passo é adicionar o host (<code>VM_HOST</code>), o usuário da máquina (<code>VM_USER</code>), a chave SSH RSA (<code>VM_KEY</code>), o host do Elasticsearch (<code>ES_HOST</code>) e a chave da API do Elasticsearch (<code>ES_API_KEY</code>) aos segredos da ação do GitHub:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb5a0fcfff9b7f997/6a17ef9a6df731d7d40a0fdf/e1075bc54151b4b94eac2a6bd2682e9997e6c709-1106x707.png" alt="Uma página de configuração de &quot;ações, segredos e variáveis&quot;, que exibe segredos do repositório, como VM_HOST, VM_KEY e VM_USER, em uma interface baseada na web." /><p>Dessa forma, a ação poderá acessar nosso servidor para copiar os novos arquivos e executar a indexação.</p><p>Agora, vamos criar nosso arquivo <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>Essa ação executará os seguintes passos sempre que enviarmos alterações para o arquivo de configuração do rastreador:</p><ol><li><p>Preencha o arquivo de configuração YAML com o host e a chave da API do Elasticsearch.</p></li><li><p>Copie a pasta de configuração para a nossa máquina virtual.</p></li><li><p>Conecte-se à nossa VM via SSH.</p></li><li><p>Execute o rastreamento com a configuração que acabamos de copiar do repositório.</p></li></ol><h2>Testando localmente</h2><p>Para testar nosso rastreador localmente, criamos um script bash que popula o host do Elasticsearch com a versão local do Docker e inicia uma busca. Você pode executar <code>./local.sh</code> para executá-lo.</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>Vamos verificar as Ferramentas de Desenvolvedor do Kibana para confirmar se o campo<code> web-crawler-index</code> foi preenchido corretamente:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt989660368fe14db8/6a17ef9b9da390c79ce46562/18551635e8265866e389a9632c4e4540958e4468-990x723.png" alt="Código do Kibana DevTools para confirmar se o índice do rastreador web está configurado corretamente." /><h2>Implantação em produção</h2><p>Agora estamos prontos para enviar a alteração para a branch principal, o que implantará o crawler em sua máquina virtual e começará a enviar logs para sua instância do Elasticsearch Serverless.</p>git add .
git commit -m "First commit"
git push<p>Isso acionará a ação do GitHub, que executará o script de implantação na máquina virtual e iniciará a indexação.</p><p>Você pode confirmar se a ação foi executada acessando o repositório do GitHub e visitando a aba “Ações”:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt986f1a4e4f3288d2/6a17ef9c7f6f1584fdc09c10/67ba3a7164d7a8049fe5661264820826cb18ed64-667x325.png" alt="As ações são implantadas na aba EC2 de um repositório GitHub." /><h2>Realizar alterações e redistribuir</h2><p>Algo que você pode ter notado é que o <code>price</code> de cada produto faz parte do corpo do documento. O ideal seria armazenar o preço em um campo separado para que pudéssemos aplicar filtros a ele.</p><p>Vamos adicionar essa alteração ao arquivo <code>crawler.yml</code> para usar <a href="https://github.com/elastic/crawler/blob/main/docs/features/EXTRACTION_RULES.md">regras de extração</a> para extrair o preço da classe CSS <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>Também vemos que o preço inclui um sinal de dólar (<code>$</code>), que devemos remover se quisermos executar consultas de intervalo. Podemos usar um pipeline de ingestão para isso. Observe que estamos fazendo referência a ele em nosso novo arquivo de configuração do rastreador acima:</p>PUT _ingest/pipeline/pricing-pipeline
{
  "processors": [
    {
      "script": {
        "source": """
                ctx['price'] = ctx['price'].replace("$","")
            """
      }
    }
  ]
}<p>Podemos executar esse comando em nosso cluster Elasticsearch de produção. Para o desenvolvimento, como é efêmero, podemos fazer com que a criação do pipeline faça parte do arquivo <code>docker-compose.yml</code> adicionando o seguinte serviço. Observe que também adicionamos um <code>depends_on</code> ao serviço de rastreamento para que ele seja iniciado após a criação bem-sucedida do pipeline.</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>Agora vamos executar <code>`./local.sh`</code> para ver a alteração localmente:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0f390aedf67cb4fe/6a17ef9eaf47b62bd2cde05b/dc1801599344a9f69f072b07ff828c4ba3815d7b-738x473.png" alt="Executando `./local.sh` para ver a alteração de preço localmente." /><p>Ótimo! Agora vamos impulsionar a mudança:</p>git add crawler-config.yml
git commit -m "added price CSS selector"
git push<p>Para confirmar se tudo funciona corretamente, você pode verificar seu Kibana de produção, que deverá refletir as alterações e mostrar o preço como um novo campo sem o símbolo de dólar.</p><h2>Conclusão</h2><p>O Elastic Open Web Crawler permite que você gerencie seu crawler como código, o que significa que você pode automatizar todo o pipeline — do desenvolvimento à implantação — e adicionar ambientes locais efêmeros e testes com os dados rastreados programaticamente, para citar alguns exemplos.</p><p>Você está convidado a clonar o repositório oficial e começar a indexar seus próprios dados usando este fluxo de trabalho. Você também pode ler <a href="https://www.elastic.co/search-labs/blog/semantic-search-open-crawler">este artigo</a> para aprender como executar uma pesquisa semântica em índices produzidos pelo 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[Dados de indexação]]></category>
    <category><![CDATA[Operações]]></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[Criando um app de buscas com o Blazor e o Elasticsearch]]></title>
    <description><![CDATA[Aprenda como criar um aplicativo de busca usando Blazor e Elasticsearch, e como usar o cliente Elasticsearch .NET para busca híbrida.]]></description>
    <content:encoded><![CDATA[<p>Neste artigo, você aprenderá como aproveitar suas habilidades em C# para criar um aplicativo de busca usando Blazor e Elasticsearch. Vamos usar o cliente <a href="https://www.elastic.co/guide/en/elasticsearch/client/net-api/current/introduction.html">Elasticsearch .NET</a> para executar consultas de pesquisa de <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/full-text-queries.html">texto completo</a>, <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-search.html">semântica</a> e <a href="https://www.elastic.co/search-labs/tutorials/search-tutorial/vector-search/hybrid-search">híbrida</a> .</p><p><strong>NOTA:</strong> Se você estiver familiarizado com a versão antiga do cliente C# do Elasticsearch, <a href="https://www.elastic.co/guide/en/elasticsearch/client/net-api/7.17/nest.html">o NEST</a>, leia esta <a href="https://www.elastic.co/search-labs/blog/net-client-evolution">postagem do blog</a> sobre a descontinuação do cliente NEST e seus novos recursos. <em>O NEST era a geração anterior do cliente .NET, que foi substituído pelo </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="Criar aplicativo Blazor: esre com diagrama Blazor" /><ul><li><p><a href="https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor#what-is-blazor?">O que é Blazor?</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor#what-is-esre?">O que é ESRE?</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor#configuring-elser">Configurando o ELSER</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/search-app-with-esre-blazor#indexing-data">Dados de indexação</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">Aplicação de construção</a></p></li></ul><h2>O que é 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> é um framework web de código aberto baseado em HTML, CSS e C#, criado pela Microsoft para permitir que desenvolvedores criem aplicações web que rodam tanto no cliente quanto no servidor. O Blazor também permite criar componentes reutilizáveis para desenvolver aplicações mais rapidamente; ele possibilita que os desenvolvedores criem a visualização HTML e as ações em C# no mesmo arquivo, o que ajuda a manter um código legível e limpo. Além disso, com o Blazor Hybrid, você pode criar aplicativos móveis nativos acessando os recursos nativos da plataforma por meio de código .NET.</p><p>Algumas das características que fazem do Blazor uma ótima estrutura para se trabalhar:</p><ul><li><p>Opções de renderização do lado do servidor e do lado do cliente</p></li><li><p>Componentes de interface do usuário reutilizáveis</p></li><li><p>Atualizações em tempo real com SignalR</p></li><li><p>Gerenciamento de estado integrado</p></li><li><p>Sistema de roteamento integrado</p></li><li><p>Tipagem forte e verificações em tempo de compilação</p></li></ul><h3>Por que Blazor?</h3><p>O Blazor oferece diversas vantagens em relação a outros frameworks e bibliotecas: permite que os desenvolvedores usem C# tanto para o código do cliente quanto para o código do servidor, fornecendo tipagem forte e verificação em tempo de compilação, o que aumenta a confiabilidade. Ele se integra perfeitamente ao ecossistema .NET, permitindo a reutilização de bibliotecas e ferramentas .NET, e oferece suporte robusto para depuração.</p><h2>O que é ESRE?</h2><p><a href="https://www.elastic.co/elasticsearch/elasticsearch-relevance-engine">O Elasticsearch Relevance Engine ™ (ESRE)</a> é um conjunto de <a href="https://www.elastic.co/guide/en/esre/current/learn.html">ferramentas para criar aplicações de busca</a> utilizando aprendizado de máquina e inteligência artificial sobre o poderoso mecanismo de busca Elasticsearch.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf313ba986de01b93/6a17d851505ac306fcad8975/4c0f2645ed1c27fe3ef61a1a9126adadfd8d5368-721x421.png" alt="esre" /><p>Para saber mais sobre ESRE, você pode ler nossa postagem esclarecedora no blog, que está disponível <a href="https://www.elastic.co/search-labs/blog/introducing-elasticsearch-relevance-engine-esre">aqui.</a></p><h2>Configurando o ELSER</h2><p>Para aproveitar os recursos <a href="https://www.elastic.co/elasticsearch/elasticsearch-relevance-engine">do ESRE</a> da Elastic, usaremos <a href="https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-elser.html">o ELSER</a> como nosso provedor de modelos.</p><p><em>Para usar os modelos ELSER do Elasticsearch, você precisa de uma licença Platinum ou Enterprise e de um nó dedicado para Machine Learning (ML) com no mínimo 4 GB de espaço. Saiba mais</em> <a href="https://www.elastic.co/guide/en/machine-learning/8.15/ml-nlp-elser.html#elser-req"><em>aqui.</em></a></p><p>Comece criando o ponto de extremidade de inferência:</p>PUT _inference/sparse_embedding/my-elser-model
{
  "service": "elser",
  "service_settings": {
    "num_allocations": 1,
    "num_threads": 1
  }
}<p>Se esta for a sua primeira vez usando o ELSER, você poderá encontrar um erro 502 Bad Gateway enquanto o modelo é carregado em segundo plano. Você pode verificar o status do modelo em <code>Machine Learning &gt; Trained Models</code> no Kibana. Após a implantação, você poderá prosseguir para a próxima etapa.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltee295a3516338302/6a17f65b414c640deb94531d/a7ff94b72d337cde892165d744b9f42fba702a87-1440x649.png" alt="Verificando modelos treinados" /><h2>Dados de indexação</h2><p>Você pode baixar o conjunto de dados <a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/esre-with-blazor/books.zip">aqui</a> e, em seguida, importar os dados usando o Kibana. Para isso, acesse a página inicial e clique em "Carregar dados". Em seguida, carregue o arquivo e clique em <code>Import</code>. Por fim, acesse a aba <code>Advanced</code> e cole os seguintes mapeamentos:</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="Importar dados" /><p>Vamos criar um índice capaz de executar consultas semânticas e de texto completo. O tipo de campo <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-text.html">semantic_text</a> cuidará do agrupamento e da incorporação dos dados. <em>Observe que estamos indexando</em> <em><code>longDescription</code></em> <em>como</em> <em><code>semantic_text</code></em><em>, você pode usar copy_to se quiser indexar um campo como</em> <em><code>semantic_text</code></em> <em>e text`</em>.</p><h2>Construindo o aplicativo com Blazor e Elasticsearch</h2><h3>Chave de API</h3><p>A primeira coisa que precisamos fazer é criar uma chave de API para autenticar nossas solicitações ao Elasticsearch. A chave da API deve ser somente leitura e permitir apenas consultas ao índice <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>Você verá algo parecido com isto:
</p>{
  "id": "XXXXXXXXXXXXXXXXXXXXXXXX",
  "name": "books-blazor-key",
  "api_key": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  "encoded": "XXXXXXXXXXXXXXXXXXXXXXXX=="
}<p>Salve o valor do campo de resposta <code>encoded</code> , pois ele será necessário posteriormente. Se você estiver usando o <a href="https://www.elastic.co/cloud/">Elastic Cloud</a>, também precisará do seu ID da nuvem. (Você pode encontrá-lo <a href="https://www.elastic.co/search-labs/tutorials/install-elasticsearch/elastic-cloud#finding-your-cloud-id">aqui</a>).</p><h4>Criando o projeto Blazor</h4><p>Comece instalando o Blazor e criando um projeto de exemplo seguindo as <a href="https://dotnet.microsoft.com/en-us/learn/aspnet/blazor-tutorial/install">instruções oficiais</a>.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb5c20b896fe1e98e/6a17f65f6317301566585bff/f5720867d960c91fd1b3c4ae9be174e062e6b2fc-975x830.png" alt="Tutorial de Blazor - Criando um projeto Blazor" /><p>Após a criação do projeto, a estrutura de pastas e arquivos deve ser semelhante a esta:</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>O aplicativo modelo inclui <a href="https://blog.getbootstrap.com/2021/08/04/bootstrap-5-1-0/">o Bootstrap v5.1.0.</a> para estilização.</p><p>Finalize a configuração do projeto instalando o 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>Após concluir esta etapa, sua página deverá ter esta aparência:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3b90e394ded7ff56/6a17f660faa913d49f93ca10/ef9d2186c2cf49e78a307c4aa69c51632e84578d-940x529.png" alt="Blazor olá mundo" /><h3>Estrutura de pastas</h3><p>Agora, vamos organizar nossas pastas da seguinte forma:</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>Explicação dos arquivos:</p><ul><li><p>Components/Pages/Search.razor: página principal contendo a barra de pesquisa, os resultados e os filtros.</p></li><li><p>Components/Pages/Search.razor.css: estilos de página.</p></li><li><p>Components/Elasticsearch/SearchBar.razor: componente da barra de pesquisa.</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: ícone de pesquisa.</p></li><li><p>Components/_Imports.razor: este arquivo importará todos os componentes.</p></li><li><p>Models/Book.cs: este arquivo armazenará o esquema do campo do livro.</p></li><li><p>Models/Response.cs: este arquivo armazenará o esquema de resposta, incluindo os resultados da pesquisa, as facetas e o total de resultados.</p></li><li><p>Services/ElasticsearchService.cs: Serviço Elasticsearch. Ele será responsável pela conexão e pelas consultas ao Elasticsearch.</p></li></ul><h4>Configuração inicial</h4><p>Vamos começar com uma limpeza.</p><p>Apague os arquivos:</p><ul><li><p>Components/Pages/Counter.razor</p></li><li><p>Componentes/Páginas/Weather.razor</p></li><li><p>Componentes/Páginas/Home.razor</p></li><li><p>Components/Layout/NavMenu.razor</p></li><li><p>Components/Layout/NavMenu.razor.css</p></li></ul><p>Verifique o arquivo <code>/Components/_Imports.razor</code> . Você deve ter as seguintes importações:</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>Integrando o Elastic ao projeto</h4><p>Agora, vamos importar os componentes do 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 remover a barra lateral padrão para termos mais espaço para nossa aplicação, removendo-a do arquivo <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 navegação removida do layout" /><p>Agora vamos inserir as credenciais do Elasticsearch para os <a href="https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-8.0&amp;tabs=linux#secret-manager">segredos do usuário</a>:</p>dotnet user-secrets init
dotnet user-secrets set ElasticsearchCloudId "your Cloud ID"
dotnet user-secrets set ElasticsearchApiKey "your API Key"<p>Usando esta abordagem, o .Net 8 armazena dados sensíveis em um local separado, fora da pasta do projeto e os torna acessíveis usando a interface <code>IConfiguration</code> . Essas variáveis estarão disponíveis para qualquer projeto .NET que utilize os mesmos segredos de usuário.</p><p>Em seguida, vamos modificar o arquivo <code>Program.cs</code> para ler os segredos e montar o cliente Elasticsearch:</p><p>Primeiro, importe as bibliotecas necessárias:</p>using BlazorApp.Services;
using Elastic.Clients.Elasticsearch;
using Elastic.Transport;<ul><li><p>BlazorApp.Services: contém o serviço Elasticsearch.</p></li><li><p>Elastic.Clients.Elasticsearch: importa a biblioteca cliente Elasticsearch para .NET 8.</p></li><li><p>Elastic.Transport: importa a biblioteca de transporte do Elasticsearch, que nos permite usar a classe ApiKey para autenticar nossas solicitações.</p></li></ul><p>Em segundo lugar, insira o seguinte código antes da linha <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 lerá as credenciais do Elasticsearch a partir dos segredos do usuário e criará uma instância de cliente Elasticsearch.</p><p>Após a inicialização do cliente Elasticsearch, adicione a seguinte linha para registrar o serviço Elasticsearch:</p>builder.Services.AddScoped&lt;ElasticsearchService&gt;();<p>O próximo passo será construir a lógica de busca no arquivo <code>/Services/ElasticsearchService.cs</code> :</p><p>Primeiro, importe as bibliotecas e os modelos necessários:</p>using BlazorApp.Models;
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.QueryDsl;<p>Em segundo lugar, adicione a classe <code>ElasticsearchService</code>, o construtor e as variáveis:</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>Configurando a pesquisa</h4><p>Agora, vamos construir nossa lógica de busca:</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> irá construir os filtros para a consulta de pesquisa usando as facetas selecionadas pelo usuário.</p></li><li><p><code>BuildHybridQuery</code> irá construir uma consulta <a href="https://www.elastic.co/search-labs/tutorials/search-tutorial/vector-search/hybrid-search">de pesquisa híbrida</a> que combina pesquisa de texto completo e pesquisa semântica.</p></li></ul><p>Em seguida, adicione o método de pesquisa:</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>A função realizará a busca usando a consulta híbrida e retornará os resultados, incluindo as agregações para a construção das facetas.</p></li><li><p><code>FormatFacets</code>: irá formatar a resposta de agregações em um dicionário.</p></li><li><p><code>ConvertFacetDictionary</code>: irá converter o dicionário de facetas em um formato mais legível.</p></li></ul><p>O próximo passo é criar os modelos que representarão os dados retornados no <code>hits</code> da consulta Elasticsearch que serão impressos como resultados em nossa página de pesquisa.</p><p>Começamos criando o arquivo <code>/Models/Book.cs</code> e adicionando o seguinte:</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>Em seguida, configure a resposta do Elastic no arquivo <code>/Models/Response.cs</code> e adicione o seguinte:</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>Configurando uma interface de usuário básica</h4><p>Em seguida, adicione o componente SearchBar. No arquivo <code>/Components/Elasticsearch/SearchBar.razor</code> , adicione o seguinte:</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 contém uma barra de pesquisa e um botão para realizar a pesquisa.</p><p>O Blazor oferece grande flexibilidade ao permitir a geração dinâmica de HTML usando código C# dentro do mesmo arquivo.</p><p>Em seguida, no arquivo <code>/Components/Elasticsearch/Results.razor</code> construiremos o componente de resultados que exibirá os resultados da pesquisa:</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>Por fim, precisaremos criar facetas para filtrar os resultados da pesquisa.</p><p><em>Nota: Os facets são filtros que permitem aos usuários refinar os resultados da pesquisa com base em atributos ou categorias específicos, como tipo de produto, faixa de preço ou marca. Esses filtros geralmente são apresentados como opções clicáveis, frequentemente na forma de caixas de seleção, para ajudar os usuários a refinar sua pesquisa e encontrar resultados relevantes com mais facilidade. No contexto do Elasticsearch, os facets são criados usando</em> <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html"><em>agregações</em></a><em>.</em></p><p>Configuramos as facetas inserindo o seguinte código no arquivo <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 lê de uma agregação <code>terms</code> nos campos <code>author</code>, <code>categories</code> e <code>status</code> e, em seguida, produz uma lista de filtros para enviar de volta ao Elasticsearch.</p><p>Agora, vamos juntar tudo.</p><p>No arquivo <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>Nossa página está funcionando!</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc750e55fff43de4b/6a17f6636864a4ab08b68931/5f0f25cb029a34df7f577a9f307278226ed07c3f-816x473.png" alt="Exemplo de página Blazor" /><p>Como você pode ver, a página é funcional, mas carece de estilo. Vamos adicionar um pouco de CSS para deixar a aparência mais organizada e responsiva.</p><p>Vamos começar a substituir os estilos de layout. No arquivo <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>Adicione os estilos para a página de pesquisa no arquivo <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>Nossa página está começando a ficar melhor:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3ecc49f404f18591/6a17f6654b055d959b432384/3dadd7ade2dd9070300a4e0514a4da2ae9cc9fb9-817x473.png" alt="Página Blazor após a adição de estilos para a página de pesquisa." /><p>Vamos dar os toques finais:</p><p>Crie os seguintes arquivos:</p><ul><li><p>Components/Elasticsearch/Facet.razor.css</p></li><li><p>Components/Elasticsearch/Results.razor.css</p></li></ul><p>E adicione os 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 da criação da página do aplicativo Blazor" /><p>Para executar o aplicativo, você pode usar o seguinte comando:</p><p><code>dotnet watch</code></p><p>Você conseguiu! Agora você pode pesquisar livros em seu índice Elasticsearch usando a barra de pesquisa e filtrar os resultados por autor, categoria e status.</p><h3>Realização de buscas semânticas e de texto completo</h3><p>Por padrão, nosso aplicativo realizará uma <a href="https://www.elastic.co/search-labs/tutorials/search-tutorial/vector-search/hybrid-search">pesquisa híbrida</a> usando tanto <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html">pesquisa de texto completo</a> quanto <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-semantic-query.html">pesquisa semântica</a>. Você pode alterar a lógica de pesquisa criando dois métodos separados, um para pesquisa de texto completo e outro para pesquisa semântica, e então selecionando um dos métodos para construir a consulta com base na entrada do usuário.</p><p>Adicione os seguintes métodos à classe <code>ElasticsearchService</code> no arquivo <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 os métodos funcionam de forma semelhante ao método <code>BuildHybridQuery</code> , mas realizam apenas pesquisa de texto completo ou pesquisa semântica.</p><p>Você pode modificar o método <code>SearchBooksAsync</code> para usar o método de pesquisa selecionado:</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>Você pode encontrar o formulário de inscrição completo <a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/esre-with-blazor">aqui.</a></p><h2>Conclusão</h2><p>Blazor é uma estrutura eficaz que permite criar aplicações web usando C#. O Elasticsearch é um mecanismo de busca poderoso que permite criar aplicativos de busca. Combinando ambos, você pode facilmente criar aplicativos de busca robustos, aproveitando o poder do ESRE para criar uma <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-search.html">experiência de busca semântica</a> em pouco tempo.</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[Banco de dados vetorial]]></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>