<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[Jeffrey Rengifo - Elasticsearch Labs]]></title>
    <description><![CDATA[Articles and tutorials from the Search team at Elastic]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[Jeffrey Rengifo - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/pt/search-labs/author/jeffrey-rengifo</link>
    </image>
    <link>https://www.elastic.co/pt/search-labs/author/jeffrey-rengifo</link>
    <atom:link href="https://www.elastic.co/pt/search-labs/rss/author/jeffrey-rengifo.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[pt]]></language>
    <lastBuildDate>Mon, 28 Sep 2026 04:16:15 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Como medir e melhorar o recall das buscas no Elasticsearch: de 0,43 a 0,75 com a busca híbrida]]></title>
    <description><![CDATA[Aprenda a medir e melhorar o recall das buscas no Elasticsearch combinando a busca léxica BM25 com embeddings vetoriais do Jina AI, usando a API rank_eval para validar a melhoria com números reais.]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/docs/solutions/search/full-text">A busca lexical</a> usando o <a href="https://www.elastic.co/blog/practical-bm25-part-1-how-shards-affect-relevance-scoring-in-elasticsearch">algoritmo de classificação BM25</a> é barata, rápida e muito eficaz para uma ampla gama de consultas. Mas ela tem um ponto cego: consultas que não compartilham tokens com seus documentos. Neste artigo, você vai medir exatamente onde a BM25 falha. Usaremos a <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval">API de avaliação de classificação</a> do Elasticsearch (<code>rank_eval</code>) e preencheremos essa lacuna adicionando <a href="https://www.elastic.co/search-labs/es/blog/jina-embeddings-v3-elastic-inference-service">Jina AI embeddings</a> via <a href="https://www.elastic.co/docs/explore-analyze/elastic-inference/eis">Elastic Inference Service</a> (EIS). Você verá a pontuação de recall variar de <code>0.43</code> para <code>0.75</code> e saberá o porquê.</p><h2>O que é recall?</h2><p><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval#k-recall">O recall</a> mede em uma escala de <code>0</code> a <code>1</code> quantos dos documentos que seus usuários realmente querem aparecem em algum lugar dos seus resultados de busca. Se uma consulta aparecer em três produtos e sua busca devolver apenas dois deles entre os 10 primeiros, <code>recall@10 = 0.67</code> nessa consulta. É uma métrica baseada em conjuntos: ela não se importa com a posição dos documentos relevantes dentro desses <em>k</em> resultados. Um documento relevante na posição 10 conta o mesmo que um na posição 1. Ter um recall alto significa que você não está perdendo resultados relevantes.</p><p>
</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5ffd147b13705680/6a170a6fe8fbce11a539fc22/b13af2a5d0ca055535d8bfe3dfe4b3d1093ee6da-1457x796.png" alt="Diagrama de Venn ilustrando como o Recall@10 é calculado, mostrando a sobreposição entre todos os documentos relevantes e os 10 principais resultados recuperados pelo BM25, resultando em uma pontuação de Recall@10 igual a 0,40." /><p>O diagrama mostra dois conjuntos: todos os documentos relevantes (à esquerda) e o que o BM25 realmente recuperou (top 10, à direita). Apenas a interseção conta para a recuperação, <code>prod_1</code> e <code>prod_2</code> foram encontrados, enquanto <code>prod_3</code>, <code>prod_4</code> e <code>prod_6</code> foram completamente perdidos. Resultado: <code>Recall@10 = 2/5 = </code><strong><code>0.40</code></strong>.</p><h2>Pré-requisitos</h2><p>Vamos direto ao ponto para entender melhor como o recall funciona. Esta demonstração usa Python. Você pode acompanhar no notebook complementar (<a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/relevance-tuning-improving-recall-adding-vectors/notebook.ipynb">notebook.ipynb</a>), onde cada bloco de código é uma célula pronta para ser executada.</p><p>O código fornecido utiliza o seguinte:</p><ul><li><p>Elasticsearch 9.3+</p></li><li><p>Python 3.10+</p></li></ul>pip install elasticsearch pandas plotly python-dotenv<ul><li><p>Um arquivo <code>.env</code> com suas credenciais do Elasticsearch</p></li></ul>ELASTICSEARCH_URL=https://your-cluster-url
ELASTICSEARCH_API_KEY=your-api-key<h2>O conjunto de dados</h2><p>Usaremos um catálogo de produtos com 1.000 produtos, abrangendo categorias como calçados, eletrônicos, ferramentas e outros.</p><p>Cada documento tem quatro campos:</p><p>Campo</p><p>Tipo</p><p>`title`</p><p>texto</p><p>`description`</p><p>texto</p><p>`marca`</p><p>palavra-chave</p><p>`category`</p><p>palavra-chave</p><p>O conjunto de dados é carregado a partir de <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/relevance-tuning-improving-recall-adding-vectors/dataset.csv"><code>dataset.csv</code></a>.</p><h2>O poder e os limites da busca lexical</h2><p>BM25 é o algoritmo padrão de ranqueamento no Elasticsearch e na maioria dos mecanismos de busca. Ele classifica os documentos de acordo com a frequência com que seus termos de consulta aparecem neles, ajustados ao tamanho do documento e à frequência desses termos em todo o índice. Você tem <a href="https://www.elastic.co/docs/reference/text-analysis/analyzer-reference">analisadores</a> na parte superior: normalização de letras minúsculas, stemming e retirada de stopwords. Uma busca por "tênis de corrida" retornará resultados como "Tênis de corrida" e provavelmente também "correr".</p><p>Isso funciona bem para uma grande classe de consultas:</p><ul><li><p>"tênis de corrida" faz a correspondência imediata dos produtos com esses tokens exatos no título.</p></li><li><p>"alto-falante Bluetooth" destaca produtos de áudio portáteis porque os tokens aparecem literalmente.</p></li></ul><p>Os resultados são determinísticos e explicáveis: um documento tem classificação alta porque os termos de consulta aparecem nele. Depurar a relevância é simples.</p><h3>Onde ocorre a falha</h3><p>Agora, vamos testar essas consultas no mesmo catálogo:</p><ul><li><p><strong>"rotina de cuidados com a pele":</strong> a palavra "rotina" não aparece em nenhum título de produto. O BM25 pode corresponder parcialmente com "cuidados com a pele", mas séruns faciais, óleos corporais e hidratantes são descritos usando termos como "vitamina C", "retinol" ou "iluminador", nenhum dos quais se sobrepõe à consulta. Produtos que formam uma rotina completa de cuidados com a pele ficam espalhados pelo índice, sem nenhum token compartilhado para ancorá-los.</p></li></ul>ID: B06XX6DS3P, Score: 9.0552, Title: Replenix Retinol Smooth + Tighten Body Lotion - Collagen-Boosting, Regenerating Anti-Aging Body Cream, Reduces Appearance of Stretch Marks, 6.7 oz.

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

  ID: B01CY764KQ, Score: 5.0057, Title: Nike Up Or Down Men Deodorant - Pack of 2 | Long-Lasting Fragrance, Body Spray Combo for Men | Deodorant for Active Living | Nike Men's Deo Set | Ultimate Odor Protection | Grooming Essentials | Signature Nike Scent | High-Performance Men's Deodorant<ul><li><p><strong>"acessórios de viagem para pets":</strong> é um agrupamento de casos de uso, não uma categoria de produto. Um sling para cães, uma cadeirinha para pets e uma caixa de viagem são todos relevantes, mas as descrições falam sobre portabilidade, segurança e conforto, em vez de "acessórios de viagem". O BM25 corresponde amplamente a palavra "pet", mas não tem sinal para distinguir produtos específicos para viagens do restante do catálogo de pets.</p></li></ul>ID: B0BVV7BKTW, Score: 7.4371, Title: Large Foldable Travel Duffel Bag with Shoes Compartment

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

ID: B08R8FRW53, Score: 6.6335, Title: CUBY Dog and Cat Sling Carrier
ID: B08QMCQYGM, Score: 6.5259, Title: YTFGGY Whiteboard Pinstripe Tape 6 Rolls 1/8"
ID: B0CP3LQSWM, Score: 6.2994, Title: Portable Dog Water Bottle 32 Oz<p>Esse é um <strong>problema de recall</strong>. Os documentos relevantes existem no seu índice. O BM25 simplesmente não os encontra porque as palavras do usuário e as do documento não coincidem o suficiente.</p><p>Adicionar sinônimos ajuda em casos conhecidos. Mas não dá para enumerar todas as formas como o usuário pode expressar uma intenção. É aí que entram os vetores.</p><h2>Por que medir o recall</h2><p>Antes de corrigir um problema, você precisa quantificá-lo.</p><p><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval#k-recall"><strong>Recall@k</strong></a> mede quantos documentos que seus usuários realmente desejam aparecem nos resultados de busca. Formalmente:</p>Recall@k = (relevant documents found in top k) / (total relevant documents)<p><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval#k-precision"><strong>Precision@k</strong></a> mede os k principais resultados e quantos são realmente relevantes:</p>Precision@k = (relevant documents in top k) / k<p>Alta precisão mostra que os resultados que você retorna são bons. No comércio eletrônico, perder um produto relevante (baixo recall) geralmente é pior do que mostrar um resultado um pouco imperfeito (menor precisão), porque o produto oculto é venda perdida.</p><p>A API <code>rank_eval</code> do Elasticsearch permite medir os dois de forma sistemática. Você fornece uma lista de consultas, cada uma com um conjunto de documentos avaliados, e o Elasticsearch calcula as métricas para você em todas elas.</p><h2>Configuração da avaliação</h2><p>A API <code>rank_eval</code> precisa de um <strong>conjunto de dados de avaliações</strong>: um mapeamento das consultas para os documentos relevantes para cada um, junto com uma nota de relevância (0 = não relevante, 1 = relevante, 2 = altamente relevante).</p><p>No bloco de notas, esta é a <a href="https://www.elastic.co/docs/solutions/search/ranking/learning-to-rank-ltr#learning-to-rank-judgement-list">lista de julgamentos</a>:</p>judgments = [
    # Query 1: "running shoes" BM25 handles well (tokens appear in product titles) 
    {"query_id": "q1", "doc_id": "B09NQJFRW6", "grade": 2, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B08JMD4LMM", "grade": 2, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B08VRJ6F2Q", "grade": 2, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B07S8NRRWR", "grade": 2, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B01HD620I8", "grade": 2, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B07DX86321", "grade": 2, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B0968YVLQ8", "grade": 1, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B093QJ39ZS", "grade": 1, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B096FGSC39", "grade": 1, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B01GVQWVV2", "grade": 1, "query": "running shoes"},

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

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

    # Query 4: "pet travel accessories" use-case grouping, products are carriers/crates/seats
    {"query_id": "q4", "doc_id": "B08R8FRW53", "grade": 2, "query": "pet travel accessories"},
    {"query_id": "q4", "doc_id": "B01MYUYX33", "grade": 2, "query": "pet travel accessories"},
    {"query_id": "q4", "doc_id": "B003C5RKE4", "grade": 2, "query": "pet travel accessories"},
    {"query_id": "q4", "doc_id": "B09GF8GBF6", "grade": 1, "query": "pet travel accessories"},
    {"query_id": "q4", "doc_id": "B0CP3LQSWM", "grade": 1, "query": "pet travel accessories"},
]<p>A combinação é intencional: <code>q1</code> é uma consulta que o BM25 lida bem (tokens exatos nos títulos dos produtos), enquanto <code>q2</code>, <code>q3</code> e <code>q4</code> são consultas orientadas à intenção, nas quais a intenção do usuário é expressa como um conceito, não como palavras-chave específicas de produto.</p><h2>Medindo o recall de base do BM25</h2><p>Primeiro, configure o cliente do Elasticsearch e indexe os dados de texto bruto:</p>import os
import json
import pandas as pd
import plotly.graph_objects as go
from elasticsearch import Elasticsearch, helpers
from dotenv import load_dotenv

load_dotenv()

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

INDEX_NAME = "ecommerce-products"<p>Agora, crie a solicitação <code>rank_eval</code> para o BM25. Cada solicitação na lista combina uma consulta com as classificações correspondentes:</p>judgments_df = pd.DataFrame(judgments)

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

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

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

bm25_result = es.rank_eval(index=INDEX_NAME, body=bm25_eval)
print("BM25 Recall@10:", bm25_result.body["metric_score"])<p>Resultado:</p>BM25 Recall@10: 0.43<p><code>0.43</code> significa que, em todas as quatro consultas, o BM25 encontra apenas 43% dos documentos que deveria. A deficiência se concentra nas consultas baseadas na intenção: "rotina de cuidados com a pele" não inclui séruns faciais e óleos corporais, pois "rotina" nunca aparece nos títulos dos produtos. Já "acessórios de viagem para pets" retorna produtos para pets fora do tópico, enquanto não inclui transportadoras e caixas de transporte descritas em termos de portabilidade e segurança, em vez de "acessórios de viagem".</p><p>Esta é a nossa linha de base. Agora, temos um número a superar.</p><h2>Adicionando busca vetorial com embeddings do Jina</h2><p><a href="https://www.elastic.co/docs/solutions/search/vector"><code>Vector search</code></a> codifica documentos e consultas como vetores de alta dimensão, tipo de vetor composto por centenas ou milhares de valores numéricos, cada um codificando um recurso específico dos dados que representa. Documentos com significado semelhante acabam próximos uns dos outros no espaço vetorial, mesmo que não compartilhem palavras. "Equipamento de ginástica" e "conjunto de halteres" ficam próximos porque os conceitos estão relacionados. Escolhi o Elasticsearch como meu banco de dados vetorial porque ele faz busca híbrida, oferecendo compreensão semântica e precisão de palavras-chave prontas para uso.</p><p><a href="https://www.elastic.co/docs/explore-analyze/elastic-inference/eis">EIS</a> inclui suporte pronto para uso de modelos via <a href="https://www.elastic.co/docs/api/doc/elasticsearch/group/endpoint-inference">API de inferência</a>.</p><h3>Passo 1: usando embeddings Jina v5 como endpoint de inferência</h3>INFERENCE_ENDPOINT_ID = ".jina-embeddings-v5-text-small"<p>Se seu cluster tem recursos de GPU (disponíveis no Elastic Cloud e Elasticsearch 9.3+), as incorporações são geradas na GPU, o que é bem mais rápido do que a inferência da CPU e elimina o tradeoff de desempenho que historicamente encarecia os vetores em larga escala.</p><p>Por que as incorporações Jina especificamente? <a href="https://www.elastic.co/search-labs/blog/jina-embeddings-v5-text">jina-embeddings-v5-text</a> é um modelo multilíngue (mais de 119 idiomas) com uma janela de contexto de 32 mil tokens e suporte para <a href="https://arxiv.org/abs/2106.09685">adaptadores de Adaptação de Baixa Ordem (LoRA)</a> específicos para cada tarefa. Ele funciona bem para descrições curtas de produtos, prontamente utilizável. Saiba mais sobre o modelo <code>jina-embeddings-v5-text</code> <a href="https://huggingface.co/jinaai/jina-embeddings-v5-text-small">aqui</a>.</p><h3>Passo 2: criar o índice com um campo semântico</h3>index_mappings = {
    "mappings": {
        "properties": {
            "title": {"type": "text", "copy_to": "semantic_field"},
            "description": {"type": "text", "copy_to": "semantic_field"},
            "brand": {"type": "keyword"},
            "category": {"type": "keyword"},
            "semantic_field": {
                "type": "semantic_text",
                "inference_id": INFERENCE_ENDPOINT_ID,
            },
        }
    }
}

if not es.indices.exists(index=INDEX_NAME):
    es.indices.create(index=INDEX_NAME, body=index_mappings)
    print(f"Created index: {INDEX_NAME}")<p>O tipo de campo <a href="https://www.elastic.co/docs/solutions/search/semantic-search/semantic-search-semantic-text"><code>semantic_text</code></a> é essencial aqui. É uma abstração de nível mais alto sobre <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector"><code>dense_vector</code></a>: você aponta para um endpoint de inferência, e o Elasticsearch cuida de gerar automaticamente os embeddings.</p><p>A propriedade <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/copy-to"><code>copy_to</code></a> em <code>title</code> e <code>description</code> significa que o conteúdo de ambos os campos flui para <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-text"><code>semantic_field</code></a> para incorporação, de modo que um único vetor captura a representação completa do produto.</p><h3>Passo 3: indexar os produtos</h3>def bulk_index(products, index_name):
    actions = []
    for product in products:
        doc_id = product.get("_id")
        source = {k: v for k, v in product.items() if k != "_id"}
        action = {"_index": index_name, "_source": source}
        if doc_id:
            action["_id"] = doc_id
        actions.append(action)

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

bulk_index(products, INDEX_NAME)<p>No momento do índice, o Elasticsearch chama o endpoint de inferência para cada documento e armazena a incorporação resultante em <code>semantic_field</code>. Sem código extra do seu lado.</p><h2>Busca híbrida: combinando BM25 e vetores com RRF</h2><p>Adicionar vetores melhora a recuperação, mas usar vetores sozinhos pode prejudicar a precisão em consultas de correspondência exata; "tênis de corrida" ainda deve priorizar correspondências exatas. A busca híbrida mantém o componente léxico especificamente para preservar essa precisão.</p><p>A busca híbrida com <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion">Fusão de Classificação Recíproca</a> (RRF) mantém o melhor dos dois mundos:</p><ul><li><p>O BM25 lida com consultas exatas e quase exatas com alta precisão.</p></li><li><p>A busca semântica processa consultas baseadas em intenção e multilíngues com alta precisão.</p></li><li><p>O RRF combina as duas listas classificadas em uma única classificação.</p></li></ul><p>A fórmula RRF atribui a cada documento uma pontuação baseada em sua classificação em cada lista de resultados:</p>score = sum(1 / (rank_constant + rank))<p>Um documento bem classificado em ambas as listas recebe uma pontuação combinada maior. O <code>rank_constant</code> controla quanto peso documentos de menor classificação recebem.</p>hybrid_requests = []

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

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

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

hybrid_result = es.rank_eval(index=INDEX_NAME, body=hybrid_eval)
print("Hybrid Recall@10:", hybrid_result.body["metric_score"])<p>Resultado:</p>Hybrid Recall@10: 0.75<p>O Hybrid melhora substancialmente em relação ao BM25 (<code>0.43</code>) e preserva a precisão para consultas de correspondência exata como "tênis de corrida".</p><h2>Resultados: Antes e depois</h2><p>Eis a comparação completa entre as três abordagens:</p>methods = {
    "BM25 (Lexical)": bm25_requests,
    "Hybrid (BM25 + Vectors)": hybrid_requests,
}

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

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

comparison_df = pd.DataFrame(comparison_data)
print(comparison_df.to_string(index=False))<p>Resultado:</p><p>Método</p><p>Recall@10</p><p>BM25 (Léxico)</p><p>0,43</p><p>Híbrido (BM25 + Vetores)</p><p>0,75</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5a1d72b57056fe64/6a170a71c1e8a56c58f882ab/e49f6c10516b0a48a0ad75962c6590ee07311407-700x500.png" alt="Gráfico de barras comparando Recall@10 entre a busca lexical BM25 e a busca híbrida combinando BM25 com vetores, mostrando que a busca híbrida alcança uma recordação significativamente maior." /><p>Analisando por consulta:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt871347f754c866d0/6a170a73839dfa40abdcfeb4/40e36dcb7b34cbf4649c512bcb60cef60f1778a6-700x500.png" alt="Gráfico de barras agrupadas comparando o Recall@10 entre a busca lexical e a busca híbrida do BM25 em quatro consultas de produtos, mostrando que a busca híbrida supera consistentemente a busca lexical em cada consulta." /><h2>Conclusão</h2><p>Ao longo deste artigo, vimos que a busca léxica do BM25 é confiável quando os usuários digitam consultas exatas, mas perde a capacidade de recuperação quando buscam por intenção em vez de palavras-chave. Usando <code>rank_eval</code>, estabelecemos uma linha base reprodutível para medir essa lacuna com números reais. A partir daí, adicionamos um campo <code>semantic_text</code> alimentado por embeddings Jina e rodamos a avaliação novamente. O resultado: a buscar híbrida melhorou a capacidade de recuperação de <code>0.43</code> para <code>0.75</code> enquanto preservava a precisão nas consultas de correspondência exata, embora a margem real dependa da sua mistura de consultas.</p><p>O padrão se estende além deste exemplo: colete julgamentos das consultas reais de seus usuários, execute <code>rank_eval</code> como linha de base, adicione <code>semantic_text</code> e meça novamente. Você saberá exatamente o que melhorou e em quanto.</p><h2>Próximas etapas</h2><ul><li><p>Aprofunde-se no recall e na busca vetorial: <a href="https://www.elastic.co/search-labs/blog/recall-vector-search-quantization">quantização de busca vetorial e recall</a>, de Jeff Vestal</p></li><li><p>Adicione o reranking para melhorar ainda mais a precisão nos resultados principais</p></li><li><p>Consulte a <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/rrf.html">documentação de busca híbrida do Elasticsearch</a></p></li><li><p>Leia mais sobre a <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-rank-eval.html"><code>rank_eval</code></a><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-rank-eval.html"> API</a></p></li></ul>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-relevance-tuning-improve-recall</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-relevance-tuning-improve-recall</guid>
    <category><![CDATA[Busca híbrida]]></category>
    <category><![CDATA[Banco de dados vetorial]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt37c9d2971b5a2db3/6a170a75cf4f254223b2d149/492c9b5432a2b9e40cebb3b60f0df019a8c7bf6d-1280x720.png" length="0" type="image/png"/>
    <pubDate>Mon, 04 May 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Criando um servidor MCP do Elasticsearch com TypeScript]]></title>
    <description><![CDATA[Saiba como criar servidor MCP do Elasticsearch com TypeScript e Claude Desktop.]]></description>
    <content:encoded><![CDATA[<p>Ao trabalhar com grandes bases de conhecimento no Elasticsearch, encontrar informações é apenas metade da batalha. Engenheiros precisam sintetizar resultados de múltiplos documentos, gerar resumos e rastrear respostas até as fontes. Para isso, o Protocolo de Contexto do Modelo (MCP) oferece uma maneira padronizada de conectar o Elasticsearch a aplicativos baseados em grandes modelos de linguagem (LLM). Embora a Elastic ofereça soluções oficiais, como o Elastic Agent Builder (que inclui um <a href="https://www.elastic.co/docs/solutions/search/agent-builder/mcp-server">endpoint MCP</a> entre os recursos), a criação de um servidor MCP personalizado oferece controle total sobre a lógica de busca, a formatação dos resultados e como o conteúdo recuperado é passado para um LLM para síntese, resumos e citações.</p><p>Neste artigo, exploraremos os benefícios de criar um servidor MCP do Elasticsearch personalizado e mostraremos como criar um servidor em TypeScript que conecte o Elasticsearch a aplicativos com LLM.</p><h2>Por que criar um servidor MCP do Elasticsearch personalizado?</h2><p>A Elastic oferece algumas alternativas para <a href="https://www.elastic.co/docs/solutions/search/mcp">servidores MCP</a>:</p><ul><li><p><a href="https://www.elastic.co/docs/solutions/search/agent-builder/mcp-server">Servidor MCP do Elastic Agent Builder para Elasticsearch 9.2+</a></p></li><li><p><a href="https://github.com/elastic/mcp-server-elasticsearch?tab=readme-ov-file#elasticsearch-mcp-server">Servidor MCP do Elasticsearch para versões mais antigas (Python)</a></p></li></ul><p>Se você precisar de mais controle sobre como o servidor MCP interage com o Elasticsearch, a criação do seu próprio servidor personalizado oferece a flexibilidade de adaptá-lo exatamente às suas necessidades. Por exemplo, o endpoint MCP do Agent Builder é limitado a consultas em Elasticsearch Query Language (ES|QL), enquanto um servidor personalizado permite usar o Query DSL completo. Você também tem controle sobre como os resultados são formatados antes de serem passados para o LLM e pode integrar etapas adicionais de processamento, como o resumo com tecnologia OpenAI que implementaremos neste tutorial.</p><p>Ao final deste artigo, você terá um servidor MCP no TypeScript que busca informações armazenadas em um índice do Elasticsearch, resume essas informações e fornece citações. Usaremos o Elasticsearch para recuperação, o modelo <code>gpt-4o-mini</code> da OpenAI para resumir e gerar citações, e o Claude Desktop como cliente MCP e UI para receber consultas dos usuários e dar respostas. O resultado é um assistente de conhecimento interno que ajuda os engenheiros a entender e sintetizar as práticas recomendadas nos documentos técnicos de sua organização.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltad9133cb083ad352/6a170c19b0367d411e72bd5b/ec5771a874cf9740d4cac6888622cbe8cd6aede7-1999x1133.png" alt="Criando um servidor MCP Elastic com TypeScript e Claude Desktop." /><h2>Pré-requisitos:</h2><ul><li><p>Node.js 20 +</p></li><li><p>Elasticsearch</p></li><li><p>Chave de API da OpenAI</p></li><li><p>Claude Desktop</p></li></ul><h3>O que é MCP?</h3><p><a href="https://www.elastic.co/what-is/mcp">O MCP</a> é um padrão aberto, criado pela <a href="https://www.anthropic.com/news/model-context-protocol">Anthropic</a>, que oferece conexões seguras e bidirecionais entre LLMs e sistemas externos, como o Elasticsearch. Você pode ler mais sobre a situação atual do MCP <a href="https://www.elastic.co/search-labs/blog/mcp-current-state">neste artigo</a>.</p><p>O cenário de MCP está <a href="https://www.elastic.co/search-labs/blog/mcp-current-state#mcp-project-updates:-transport,-elicitation,-and-structured-tooling">em constante evolução</a>, com servidores disponíveis para uma ampla gama de casos de uso. Além disso, é fácil criar seu próprio servidor MCP personalizado, como mostraremos neste artigo.</p><h3>Clientes do MCP</h3><p>Há uma longa <a href="https://modelcontextprotocol.io/clients">lista de clientes MCP disponíveis</a>, cada um com as próprias características e limitações. Por simplicidade e popularidade, usaremos o <a href="https://claude.ai/download">Claude Desktop</a> como nosso cliente MCP. Ele servirá como interface de chat na qual os usuários poderão fazer perguntas em linguagem natural e que invocará automaticamente as ferramentas expostas pelo nosso servidor MCP para buscar documentos e gerar resumos.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt06fd7a02042094e1/6a170c1b14b2700024e3c651/66eb0b11473347b6cf2d85718251eeac38d6249d-1999x1491.png" alt="Página do Claude 4.5 Sonnet, com a observação, &quot;hora do café e do Claude? Como posso ajudar você hoje?&quot;" /><h2>Criando um servidor MCP do Elasticsearch</h2><p>Usando o <a href="https://github.com/modelcontextprotocol/typescript-sdk">TypeScript SDK</a>, podemos criar um servidor que entende como consultar nossos dados do Elasticsearch com base em uma entrada de consulta do usuário.</p><p>Aqui estão os passos deste artigo para integrar o servidor MCP do Elasticsearch com o cliente Claude Desktop:</p><ol><li><p><a href="https://www.elastic.co/search-labs/blog/elastic-mcp-server-typescript-claude#configure-mcp-server-for-elasticsearch">Configure o servidor MCP para o Elasticsearch.</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/elastic-mcp-server-typescript-claude#load-the-mcp-server-into-claude-desktop">Carregue o servidor MCP no Claude Desktop.</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/elastic-mcp-server-typescript-claude#test-it-out">Faça o teste.</a></p></li></ol><h3>Configure o servidor MCP para o Elasticsearch</h3><p>Para começar, vamos iniciar uma aplicação de nó:</p>npm init -y<p>Isso criará um arquivo <code>package.json</code> e, com ele, poderemos começar a instalar as dependências necessárias para essa aplicação.</p>npm install @elastic/elasticsearch @modelcontextprotocol/sdk openai zod &amp;&amp; npm install --save-dev ts-node @types/node typescript<ul><li><p><strong>@elastic/elasticsearch</strong> nos dará acesso à biblioteca Elasticsearch Node.js.</p></li><li><p><strong>@modelcontextprotocol/sdk</strong> fornece as ferramentas de núcleo para criar e gerenciar um servidor MCP, registrar ferramentas e lidar com a comunicação com clientes MCP.</p></li><li><p><strong>openai</strong> permite a interação com modelos OpenAI para gerar resumos ou respostas em linguagem natural.</p></li><li><p><a href="https://zod.dev/"><strong>zod</strong></a>ajuda a definir e validar esquemas estruturados para dados de entrada e saída em cada ferramenta.</p></li></ul><p><code>ts-node</code>, <code>@types/node</code>, e <code>typescript</code> serão usados durante o desenvolvimento para digitar o código e compilar os scripts.</p><h4>Configurar o conjunto de dados</h4><p>Para fornecer os dados que o Claude Desktop pode consultar usando nosso servidor MCP, usaremos um <a href="https://github.com/Delacrobix/typescript-elasticsearch-mcp/blob/main/dataset.json">conjunto de dados fictício de base de conhecimento interna</a>. Veja como será um documento desse conjunto de dados:</p>{
    "id": 5,
    "title": "Logging Standards for Microservices",
    "content": "Consistent logging across microservices helps with debugging and tracing. Use structured JSON logs and include request IDs and timestamps. Avoid logging sensitive information. Centralize logs in Elasticsearch or a similar system. Configure log rotation to prevent storage issues and ensure logs are searchable for at least 30 days.",
    "tags": ["logging", "microservices", "standards"]
}<p>Para ingerir os dados, preparamos um script que cria um índice no Elasticsearch e carrega o conjunto de dados nele. Você pode encontrá-lo <a href="https://github.com/Delacrobix/typescript-elasticsearch-mcp/blob/main/setup.ts">aqui</a>.</p><h4>Servidor MCP</h4><p>Crie um arquivo chamado <a href="https://github.com/Delacrobix/typescript-elasticsearch-mcp/blob/main/index.ts"><code>index.ts</code></a> e adicione o seguinte código para importar as dependências e lidar com as variáveis de ambiente:</p>// index.ts
import { z } from "zod";
import { Client } from "@elastic/elasticsearch";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import OpenAI from "openai";

const ELASTICSEARCH_ENDPOINT =
  process.env.ELASTICSEARCH_ENDPOINT ?? "http://localhost:9200";
const ELASTICSEARCH_API_KEY = process.env.ELASTICSEARCH_API_KEY ?? "";
const OPENAI_API_KEY = process.env.OPENAI_API_KEY ?? "";
const INDEX = "documents";<p>Além disso, vamos inicializar os clientes para lidar com as chamadas do Elasticsearch e do OpenAI:</p>const openai = new OpenAI({
  apiKey: OPENAI_API_KEY,
});

const _client = new Client({
  node: ELASTICSEARCH_ENDPOINT,
  auth: {
    apiKey: ELASTICSEARCH_API_KEY,
  },
});<p>Para tornar nossa implementação mais robusta e garantir entrada e saída estruturadas, definiremos esquemas usando <a href="https://zod.dev/"><code>zod</code></a>. Isso nos permite validar dados em tempo de execução, detectar erros com antecedência e facilitar o processamento de forma programática das respostas da ferramenta:</p>const DocumentSchema = z.object({
  id: z.number(),
  title: z.string(),
  content: z.string(),
  tags: z.array(z.string()),
});

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

type Document = z.infer&lt;typeof DocumentSchema&gt;;
type SearchResult = z.infer&lt;typeof SearchResultSchema&gt;;<p>Saiba mais sobre saídas estruturadas <a href="https://www.elastic.co/search-labs/blog/structured-outputs-elasticsearch-guide">aqui</a>.</p><p>Agora vamos inicializar o servidor MCP:</p>const server = new McpServer({
  name: "Elasticsearch RAG MCP",
  description:
    "A RAG server using Elasticsearch. Provides tools for document search, result summarization, and source citation.",
  version: "1.0.0",
});<h4>Definição das ferramentas MCP</h4><p>Com tudo configurado, podemos começar a escrever as ferramentas que serão expostas pelo nosso servidor MCP. Esse servidor expõe duas ferramentas:</p><ul><li><p><strong><code>search_docs</code></strong><strong>: </strong>Busca por documentos no Elasticsearch usando busca de texto completo.</p></li><li><p><strong><code>summarize_and_cite</code></strong><strong>:</strong> Resume e sintetiza informações de documentos previamente recuperados para responder a uma pergunta do usuário. Essa ferramenta também adiciona citações que referenciam os documentos fonte.</p></li></ul><p>Juntas, essas ferramentas formam um fluxo de trabalho simples de "recuperar e resumir", em que uma ferramenta busca documentos relevantes e a outra usa esses documentos para gerar uma resposta resumida e citada.</p><h4>Formato de resposta da ferramenta</h4><p>Cada ferramenta pode aceitar parâmetros de entrada arbitrários, mas deve responder com a seguinte estrutura:</p><ul><li><p><strong>Conteúdo:</strong> esta é a resposta da ferramenta em um formato não estruturado. Este campo geralmente é usado para retornar texto, imagens, áudio, links ou embeddings. Para esta aplicação, ele será usado para retornar texto formatado com as informações geradas pelas ferramentas.</p></li><li><p><strong>structuredContent: </strong>Esse é um retorno opcional usado para fornecer os resultados de cada ferramenta em um formato estruturado. É útil para fins programáticos. Embora não seja usado neste servidor MCP, pode ser útil caso você queira desenvolver outras ferramentas ou processar os resultados programaticamente.</p></li></ul><p>Com essa estrutura em mente, vamos nos aprofundar em cada ferramenta em detalhes.</p><h4>Ferramenta de Busca de Documentos</h4><p>Esta ferramenta realiza uma <a href="https://www.elastic.co/docs/solutions/search/full-text">busca de texto completo</a> no índice do Elasticsearch para recuperar os documentos mais relevantes com base na consulta do usuário. Ele destaca correspondências-chave e oferece uma visão geral rápida com pontuações de relevância.</p>server.registerTool(
  "search_docs",
  {
    title: "Search Documents",
    description:
      "Search for documents in Elasticsearch using full-text search. Returns the most relevant documents with their content, title, tags, and relevance score.",
    inputSchema: {
      query: z
        .string()
        .describe("The search query terms to find relevant documents"),
      max_results: z
        .number()
        .optional()
        .default(5)
        .describe("Maximum number of results to return"),
    },
    outputSchema: {
      results: z.array(SearchResultSchema),
      total: z.number(),
    },
  },
  async ({ query, max_results }) =&gt; {
    if (!query) {
      return {
        content: [
          {
            type: "text",
            text: "Query parameter is required",
          },
        ],
        isError: true,
      };
    }

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

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

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

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

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

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

      return {
        content: [
          {
            type: "text",
            text: `Error searching documents: ${error.message}`,
          },
        ],
        isError: true,
      };
    }
  }
);<p><em>Configuramos </em><a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-fuzzy-query"><em><code>fuzziness</code></em></a><em><code>: “AUTO”</code></em><em> para ter uma tolerância variável de erros de digitação com base no comprimento do token que está sendo analisado. Também definimos </em><em><code>title^2</code></em><em> para aumentar a pontuação dos documentos onde a correspondência ocorre no campo de título.</em></p><h4>Ferramenta summarize_and_cite</h4><p>Esta ferramenta gera um resumo baseado em documentos recuperados na busca anterior. Usa o modelo <code>gpt-4o-mini</code> da OpenAI para sintetizar as informações mais relevantes e responder à pergunta do usuário, fornecendo respostas derivadas diretamente dos resultados da busca. Além do resumo, também retorna metadados de citação para os documentos de origem usados.</p>server.registerTool(
  "summarize_and_cite",
  {
    title: "Summarize and Cite",
    description:
      "Summarize the provided search results to answer a question and return citation metadata for the sources used.",
    inputSchema: {
      results: z
        .array(SearchResultSchema)
        .describe("Array of search results from search_docs"),
      question: z.string().describe("The question to answer"),
      max_length: z
        .number()
        .optional()
        .default(500)
        .describe("Maximum length of the summary in characters"),
      max_docs: z
        .number()
        .optional()
        .default(5)
        .describe("Maximum number of documents to include in the context"),
    },
    outputSchema: {
      summary: z.string(),
      sources_used: z.number(),
      citations: z.array(
        z.object({
          id: z.number(),
          title: z.string(),
          tags: z.array(z.string()),
          relevance_score: z.number(),
        })
      ),
    },
  },
  async ({ results, question, max_length, max_docs }) =&gt; {
    if (!results || results.length === 0 || !question) {
      return {
        content: [
          {
            type: "text",
            text: "Both results and question parameters are required, and results must not be empty",
          },
        ],
        isError: true,
      };
    }

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

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

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

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

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

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

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

      return {
        content: [
          {
            type: "text",
            text: combinedText,
          },
        ],
        structuredContent: {
          summary: summaryText,
          sources_used: citations.length,
          citations: citations,
        },
      };
    } catch (error: any) {
      return {
        content: [
          {
            type: "text",
            text: `Error generating summary and citations: ${error.message}`,
          },
        ],
        isError: true,
      };
    }
  }
);<p>Por fim, precisamos iniciar o servidor usando <a href="https://github.com/modelcontextprotocol/typescript-sdk?tab=readme-ov-file#stdio">stdio</a>. Isso significa que o cliente MCP se comunicará com nosso servidor lendo e escrevendo nos fluxos padrão de entrada e saída. Stdio é a opção de transporte mais simples e funciona bem para servidores MCP locais lançados como subprocessos pelo cliente. Adicione o seguinte código ao final do arquivo:</p>const transport = new StdioServerTransport();
server.connect(transport);<p>Agora compile o projeto usando o seguinte comando:</p>npx tsc index.ts --target ES2022 --module node16 --moduleResolution node16 --outDir ./dist --strict --esModuleInterop<p>Isso criará uma pasta <code>dist</code> e, dentro dela, um arquivo <code>index.js</code>.</p><h3>Carregue o servidor MCP no Claude Desktop</h3><p>Siga <a href="https://modelcontextprotocol.io/docs/develop/connect-local-servers">este guia</a> para configurar o servidor MCP com o Claude Desktop. No arquivo de configuração Claude, precisamos definir os seguintes valores:</p>{
  "mcpServers": {
    "elasticsearch-rag-mcp": {
      "command": "node",
      "args": [   "/Users/user-name/app-dir/dist/index.js"
      ],
      "env": {
        "ELASTICSEARCH_ENDPOINT": "your-endpoint-here",
        "ELASTICSEARCH_API_KEY": "your-api-key-here",
        "OPENAI_API_KEY": "your-openai-key-here"
      }
    }
  }
}<p>O valor <code>args</code> deve apontar para o arquivo compilado na pasta <code>dist</code>. Você também precisa definir as variáveis de ambiente no arquivo de configuração com exatamente os mesmos nomes definidos no código.</p><h3>Faça o teste</h3><p>Antes de executar cada ferramenta, clique em <strong>Busca e Ferramentas</strong> para garantir que as ferramentas estejam ativadas. Aqui você também pode ativar ou desativar cada uma delas:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt395a7337021f9820/6a170c1c67045bb74d45c228/172981c2a54adabc70d5819013c3007670935605-1999x1002.png" alt="Página do Claude 4.5 Sonnet, com a observação, &quot;Boa tarde, Jeff. Como posso ajudar você hoje?&quot;" /><p>Por fim, vamos testar o servidor MCP no chat do Claude Desktop e começar a fazer perguntas:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf4ac458dc0206271/6a170c1e66c4f91328f8c072/03654c0f8c53c714f801fba8b25747071179209b-1999x1353.png" alt="Solicitação de pesquisa do usuário no chat do Claude Desktop para documentos sobre métodos de autenticação e controle de acesso por função (RBAC), junto com as respostas do Claude." /><p>Para a pergunta “<strong>Buscar documentos sobre métodos de autenticação e controle de acesso por função</strong>”, a ferramenta <code>search_docs</code> é executada e retorna os seguintes resultados:</p>Most Relevant Documents:
Access Control and Role Management (highest relevance) - This document covers role-based access control (RBAC) principles, including ensuring users only have necessary permissions, regular auditing of user roles, revoking inactive accounts, and implementing just-in-time access for sensitive operations.
User Authentication with OAuth 2.0 - This document explains OAuth 2.0 authentication, which enables secure delegated access without credential sharing. It covers configuring identity providers, token management with limited scope and lifetime, and secure storage of refresh tokens.
Container Security Guidelines - While primarily about container security, this document touches on access control aspects like running containers as non-root users and avoiding embedded credentials.
Incident Response Playbook - This mentions role assignment during incidents (incident commander, communications lead, etc.), which relates to access control in emergency scenarios.
Logging Standards for Microservices - This document includes guidance on avoiding logging sensitive information, which is relevant to authentication security.<p>A resposta é: "Ótimo! Encontrei 5 documentos relevantes sobre métodos de autenticação e controle de acesso por função. Eis o que foi encontrado:"</p><p>A chamada de ferramenta retorna os documentos fonte como parte da carga útil de resposta, que são posteriormente usados para gerar citações.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbaf48a9468770ce2/6a170c21509168bffee1bb14/25ff4c7e9563d99752f95540dafdc7fd211a66e3-800x530.gif" alt="Página do Claude 4.5 Sonnet, com respostas em rolagem que incluem os cinco documentos relevantes sobre métodos de autenticação e controle de acesso por função." /><p>Também é possível encadear várias ferramentas em uma única interação. Neste caso, o Claude Desktop analisa a pergunta do usuário e determina que precisa primeiro chamar <code>search_docs</code> para recuperar documentos relevantes e depois passar esses resultados para <code>summarize_and_cite</code> para gerar a resposta final, tudo isso sem exigir prompts separados do usuário:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta46ff45274e64192/6a170c230c4857a91501aac1/e6a8a46acb4236e77058f18bcd2f0737b5882c05-1999x1101.png" alt="Chat no Claude Desktop, com a observação &quot;Jeff está de volta&quot;, além de uma nova pergunta do usuário: &quot;Quais são as principais recomendações para melhorar a autenticação e o controle de acesso em nossos sistemas?&quot; Inclua referências.&quot;" /><p>Neste caso, para a consulta “<strong>Quais são as principais recomendações para melhorar a autenticação e o controle de acesso em nossos sistemas? Inclua referências.</strong>”, obtivemos os seguintes resultados:</p>Based on the documentation, here are the main recommendations to improve authentication and access control across your systems:

Key Recommendations

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

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

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

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

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

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

Access Control and Role Management (Tags: security, access-control)
User Authentication with OAuth 2.0 (Tags: authentication, oauth)
These recommendations work together to create a defense-in-depth approach, where multiple security layers protect your systems from unauthorized access.<p>Como na etapa anterior, podemos ver a resposta de cada ferramenta para esta pergunta:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8f633c518e708a99/6a170c25ab7f082991db9ed6/cb606d356b2f7d5e4878a5eff71bc881869ac0ee-800x585.gif" alt="Página de chat do Claude Desktop, com texto rolando que inclui a resposta de cada ferramenta para a pergunta: &quot;Quais são as principais recomendações para melhorar a autenticação e o controle de acesso em nossos sistemas? Inclua referências.&quot;" /><p><em>Nota: Se aparecer um submenu perguntando se você aprova o uso de cada ferramenta, selecione </em><em><strong>Permitir sempre</strong></em><em> ou </em><em><strong>Permitir uma vez</strong></em><em>.</em></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6627ee0bff1862df/6a170c266f7f040f6f91488c/aea942ba9b0037526ea215bec65690f1a5c3099c-1522x250.png" alt="Opções &quot;Sempre permitir&quot; e &quot;Permitir uma vez&quot; do Claude Desktop para o usuário escolher." /><h2>Conclusão</h2><p>Os servidores MCP representam um passo significativo rumo à padronização das ferramentas LLM para aplicações locais e remotas. Embora a compatibilidade total ainda esteja em andamento, estamos avançando nessa direção.</p><p>Neste artigo, aprendemos como criar um servidor MCP personalizado em TypeScript que conecta o Elasticsearch a aplicações baseadas em LLM. Nosso servidor expõe duas ferramentas: <code>search_docs</code> para recuperar documentos relevantes usando Query DSL; e <code>summarize_and_cite</code> para gerar resumos com citações via modelos OpenAI e Claude Desktop como UI.</p><p>O futuro da compatibilidade entre diferentes provedores de clientes e servidores parece promissor. As próximas etapas incluem adicionar mais funcionalidades e flexibilidade ao seu agente. Existe um <a href="https://www.elastic.co/search-labs/blog/llm-functions-elasticsearch-intelligent-query">artigo</a> prático sobre como parametrizar suas consultas usando modelos de pesquisa para ter precisão e flexibilidade.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elastic-mcp-server-typescript-claude</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elastic-mcp-server-typescript-claude</guid>
    <category><![CDATA[IA agêntica]]></category>
    <category><![CDATA[Integrações]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5600198cb47666a5/6a170c28509168ce3ae1bb18/0bb24c05fff391f42070c2883182ea6fe9cb9680-1280x720.png" length="0" type="image/png"/>
    <pubDate>Fri, 27 Mar 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Usando a API de Inferência Elasticsearch junto com modelos de Hugging Face]]></title>
    <description><![CDATA[Aprenda a conectar o Elasticsearch a modelos do Hugging Face usando endpoints de inferência e a construir um sistema multilíngue de recomendação de blogs com busca semântica e conclusões de chat.]]></description>
    <content:encoded><![CDATA[<p>Em atualizações recentes, o Elasticsearch introduziu uma integração nativa para conectar a modelos hospedados no <a href="https://endpoints.huggingface.co/">Hugging Face Inference Service</a>. Neste post, vamos explorar como configurar essa integração e realizar inferência por meio de chamadas simples de API usando um grande modelo de linguagem (LLM). Vamos usar <a href="https://huggingface.co/HuggingFaceTB/SmolLM3-3B">SmolLM3-3B</a>, um modelo leve de uso geral com bom equilíbrio entre uso de recursos e qualidade da resposta.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9094997548bd70f8/6a170d6a839dfa0ad6dcff54/7ddadf1976421a860a7d62087239adb9150d808b-1999x1388.png" alt="Gráfico de dispersão mostrando vários modelos de linguagem pequenos plotados pelo tamanho do modelo (em bilhões de parâmetros) no eixo x e a taxa de vitória (em porcentagem) no eixo y. O SmolLM3‑3B aparece no topo da tendência de eficiência, com uma taxa de vitória maior do que outros modelos de tamanho semelhante." /><h2>Pré-requisitos</h2><ul><li><p><strong>Elasticsearch 9.3 ou Elastic Cloud Serverless: </strong>você pode criar uma implantação na nuvem seguindo <a href="https://www.elastic.co/search-labs/tutorials/install-elasticsearch/elastic-cloud">essas instruções</a>, ou você pode usar o <a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed/local-development-installation-quickstart#local-dev-quick-start"><code>start-local</code></a> quickstart.</p></li><li><p><strong>Python 3.12: </strong>baixe o Python <a href="https://www.python.org/">aqui</a>.</p></li><li><p><strong>Hugging Face </strong><a href="https://huggingface.co/docs/hub/en/security-tokens">Token de acesso</a>.</p></li></ul><h2>Chat completions usando um endpoint de inferência do Hugging Face</h2><p>Primeiro, vamos construir um exemplo prático que conecta o Elasticsearch a um <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-put">endpoint de inferência</a> Hugging Face para gerar recomendações baseadas em IA a partir de uma coleção de artigos de blog. Para a base de conhecimento do app, usaremos um conjunto de dados de artigos de blog da empresa, que contém informações valiosas, mas frequentemente difíceis de navegar.</p><p>Com este endpoint, a <a href="https://www.elastic.co/docs/solutions/search/semantic-search">busca semântica</a> recupera os artigos mais relevantes para uma consulta específica, e um Hugging Face LLM gera recomendações curtas e contextuais com base nesses resultados.</p><p>Vamos dar uma olhada em uma visão geral do fluxo de informações que vamos criar:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf217b7b7db4e1e6c/6a170d6ca929cf8022ae0a3b/1dfbc2323438feaaa42e13ab242dd1f7166f74aa-1200x676.png" alt="Diagrama de fluxo mostrando um índice Elasticsearch que fornece resultados de busca semântica para um endpoint de inferência, que retorna recomendações de artigos." /><p>Neste artigo, testaremos a <strong>capacidade do SmolLM3-3B </strong>decombinar seu tamanho compacto com fortes capacidades de raciocínio multíngue e chamada de ferramentas. Com base em uma consulta de busca, enviaremos todo o conteúdo correspondente (em inglês e espanhol) para o LLM para gerar uma lista de artigos recomendados com uma descrição personalizada com base na consulta de busca e nos resultados.</p><p>Veja como poderia ser a UI de um site de artigos com um sistema de geração de recomendações por IA.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt20e69b9a06fecd65/6a170d6e839dfa6f97dcff58/8d3b86b212f28ff279f2da67a33e6134039f0e4e-1999x949.png" alt="UI de um site de artigos com um sistema de geração de recomendações por IA, listando três exemplos, com texto em inglês e títulos em inglês ou espanhol." /><p>Você pode encontrar a implementação completa desta aplicação no <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/elasticsearch-inference-api-and-hugging-face/notebook.ipynb">notebook</a> vinculado.</p><h3>Configuração de endpoints de inferência do Elasticsearch</h3><p>Para usar o <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-put-hugging-face">endpoint de inferência do Hugging Face</a> no Elasticsearch, precisamos de dois elementos importantes: uma chave de API do Hugging Face e uma URL de endpoint do Hugging Face em execução. Ela deverá ficar assim:</p>PUT _inference/chat_completions/hugging-face-smollm3-3b
{
    "service": "hugging_face",
    "service_settings": {
        "api_key": "hugging-face-access-token", 
        "url": "url-endpoint" 
    }
}<p>O endpoint de inferência Hugging Face no Elasticsearch permite diferentes tipos de tarefas: <code>text_embedding</code>, <code>completion</code>, <code>chat_completion</code>, e <code>rerank</code>. Neste post do blog, usamos <code>chat_completion</code> porque precisamos que o modelo gere recomendações conversacionais baseadas nos resultados de busca e em um prompt do sistema. Esse endpoint nos permite realizar preenchimentos de chat diretamente do Elasticsearch de forma simples usando a API do Elasticsearch:</p>POST _inference/chat_completion/hugging-face-smollm3-3b/_stream
{
  "messages": [
      { "role": "user", "content": "&lt;user prompt&gt;" }
  ]
}<p>Isso servirá como o núcleo da aplicação, recebendo o prompt e os resultados de busca que passarão pelo modelo. Com a teoria explicada, vamos começar a implementar a aplicação.</p><h4>Configurando o endpoint de inferência no Hugging Face</h4><p>Para implantar o modelo Hugging Face, vamos usar <a href="https://huggingface.co/inference-endpoints/dedicated">implantações Hugging Face One-Click</a>, um serviço fácil e rápido para implantar endpoints de modelos. Lembre-se de que este é um serviço pago, e seu uso pode incorrer em custos adicionais. Esta etapa criará a instância do modelo que será usada para gerar as recomendações dos artigos.</p><p>Você pode escolher um modelo do catálogo de um clique.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta7bdfa43d6766324/6a170d6fb339d59e5476a039/b816e9fba1fe172687bf58f5143fb1f838c1077f-549x331.png" alt="Exibição da interface de um catálogo de modelos filtrado para &quot;smoll3&quot;, mostrando um modelo chamado &quot;smollm3‑3b&quot; com geração de texto, vLLM, GPU 1× Nvidia L4 e um preço listado de US$ 0,80, além de uma nota sugerindo que você amplie a busca para todos os modelos de Hugging Face." /><p>Vamos escolher o modelo <strong>SmolLM3-3B</strong> :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdb0a2e6ffd7deb20/6a170d710c48574b7401aafc/610d3aba0429f3666c2df3616d513eb6a4397c0c-502x478.png" alt="Interface para criar um endpoint para o modelo SmolLM3‑3B, mostrando o nome do modelo, uma nota &quot;verified by Hugging Face&quot;, um campo de nome de endpoint, um custo de US$ 0,80 por hora por réplica em execução, uma opção cURL e um botão &quot;Create Endpoint&quot;." /><p>A partir daqui, pegue o URL do endpoint do Hugging Face:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt25714021711ed6ff/6a170d72c1e8a54853f88336/025094ddb2cfbd1f0f216a5ec4e119b0f4fa2c42-646x328.png" alt="Visualização do **dashboard** de um endpoint de inferência Hugging Face chamado &quot;smollm3‑3b‑pnz&quot;, mostrando um status verde de execução, uma réplica ativa, zero solicitações na última hora, guias de navegação e o URL do endpoint exibido." /><p>Como mencionado na <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-put-hugging-face">documentação de endpoints de inferência Hugging Face do Elasticsearch</a>, a geração de texto requer um modelo compatível com a API OpenAI. Por esse motivo, precisamos anexar o subcaminho <code>/v1/chat/completions</code> à URL do endpoint Hugging Face. O resultado final ficará assim:</p>https://j2g31h0futopfkli.us-east-1.aws.endpoints.huggingface.cloud/v1/chat/completions<p>Com isso pronto, podemos começar a programar em um notebook Python.</p><h4>Gerando a Chave API do Hugging Face</h4><p>Crie uma <a href="https://huggingface.co/join">conta Hugging Face</a> e obtenha um token de API seguindo <a href="https://huggingface.co/docs/hub/en/security-tokens#user-access-tokens">estas instruções</a>. Você pode escolher entre três tipos de token: <em>detalhado</em> (recomendado para produção, pois fornece acesso apenas a recursos específicos); <em>de leitura</em> (para acesso somente leitura); ou <em>de gravação</em> (para acesso de leitura e gravação). Para este tutorial, um token de leitura é suficiente, já que só precisamos chamar o endpoint de inferência. Guarde esta chave para o próximo passo.</p><h4>Configurando o endpoint de inferência do Elasticsearch</h4><p>Primeiro, vamos declarar um cliente Elasticsearch Python:</p>os.environ["ELASTICSEARCH_API_KEY"] = "your-elasticsearch-api-key"
os.environ["ELASTICSEARCH_URL"] = "https://xxxx.us-central1.gcp.cloud.es.io:443"

es_client = Elasticsearch(
    os.environ["ELASTICSEARCH_URL"], api_key=os.environ["ELASTICSEARCH_API_KEY"]
)<p>Em seguida, vamos criar um endpoint de inferência no Elasticsearch que use o modelo Hugging Face. Esse endpoint nos permitirá gerar respostas com base nos posts do blog e no prompt passado para o modelo.</p>INFERENCE_ENDPOINT_ID = "smollm3-3b-pnz"

os.environ["HUGGING_FACE_INFERENCE_ENDPOINT_URL"] = (
 "https://j2g31h0futopfkli.us-east-1.aws.endpoints.huggingface.cloud/v1/chat/completions"
)
os.environ["HUGGING_FACE_API_KEY"] = "hf_xxxxx"

resp = es_client.inference.put(
        task_type="chat_completion",
        inference_id=INFERENCE_ENDPOINT_ID,
        body={
            "service": "hugging_face",
            "service_settings": {
                "api_key": os.environ["HUGGING_FACE_API_KEY"],
                "url": os.environ["HUGGING_FACE_INFERENCE_ENDPOINT_URL"],
            },
        },
    )<h3>Conjunto de dados</h3><p>O conjunto de dados contém os <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/elasticsearch-inference-api-and-hugging-face/dataset.json">posts do blog</a> que serão consultados, representando um conjunto de conteúdo multilíngue usado em todo o fluxo de trabalho:</p>// Articles dataset document example: 
{
    "id": "6",
    "title": "Complete guide to the new API: Endpoints and examples",
    "author": "Tomas Hernandez",
    "date": "2025-11-06",
    "category": "tutorial",
    "content": "This guide describes in detail all endpoints of the new API v2. It includes code examples in Python, JavaScript, and cURL for each endpoint. We cover authentication, resource creation, queries, updates, and deletion. We also explain error handling, rate limiting, and best practices. Complete documentation is available on our developer portal."
  }<h4>Mapeamento do Elasticsearch</h4><p>Com o conjunto de dados definido, precisamos criar um esquema de dados que se ajuste adequadamente à estrutura do post do blog. Os seguintes <a href="https://www.elastic.co/docs/manage-data/data-store/mapping">mapeamentos de índice</a> serão usados para armazenar os dados no Elasticsearch:</p>INDEX_NAME = "blog-posts"

mapping = {
    "mappings": {
        "properties": {
            "id": {"type": "keyword"},
            "title": {
                "type": "object",
                "properties": {
                    "original": {
                        "type": "text",
                        "copy_to": "semantic_field",
                        "fields": {"keyword": {"type": "keyword"}},
                    },
                    "translated_title": {
                        "type": "text",
                        "fields": {"keyword": {"type": "keyword"}},
                    },
                },
            },
            "author": {"type": "keyword", "copy_to": "semantic_field"},
            "category": {"type": "keyword", "copy_to": "semantic_field"},
            "content": {"type": "text", "copy_to": "semantic_field"},
            "date": {"type": "date"},
            "semantic_field": {"type": "semantic_text"},
        }
    }
}


es_client.indices.create(index=INDEX_NAME, body=mapping)<p>Aqui, podemos ver com mais clareza como os dados são estruturados. Usaremos busca semântica para recuperar resultados baseados em linguagem natural, junto com a propriedade <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/copy-to"><code>copy_to</code></a> para copiar o conteúdo do campo para o campo <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-text"><code>semantic_text</code></a>. Além disso, o campo <code>title</code> contém dois subcampos: o subcampo <code>original</code> armazena o título em inglês ou espanhol, dependendo do idioma original do artigo; e o subcampo <code>translated_title</code> está presente apenas para artigos em espanhol e contém a tradução para o inglês do título original.</p><h3>Ingestão de dados</h3><p>O seguinte trecho de código ingere o conjunto de dados de postagens do blog no Elasticsearch usando a <a href="https://www.elastic.co/docs/reference/elasticsearch/clients/javascript/bulk_examples">bulk API</a>:</p>def build_data(json_file, index_name):
    with open(json_file, "r") as f:
        data = json.load(f)

    for doc in data:
        action = {"_index": index_name, "_source": doc}
        yield action


try:
    success, failed = helpers.bulk(
        es_client,
        build_data("dataset.json", INDEX_NAME),
    )
    print(f"{success} documents indexed successfully")

    if failed:
        print(f"Errors: {failed}")
except Exception as e:
    print(f"Error: {str(e)}")<p>Agora que os artigos já estão no Elasticsearch, precisamos criar uma função capaz de buscar no campo <code>semantic_text</code>:</p>def perform_semantic_search(query_text, index_name=INDEX_NAME, size=5):
    try:
        query = {
            "query": {
                "match": {
                    "semantic_field": {
                        "query": query_text,
                    }
                }
            },
            "size": size,
        }

        response = es_client.search(index=index_name, body=query)
        hits = response["hits"]["hits"]

        return hits
    except Exception as e:
        print(f"Semantic search error: {str(e)}")
        return []<p>Precisamos também de uma função que chame o endpoint de inferência. Neste caso, chamaremos o endpoint usando <strong><code>chat_completion</code></strong>tipo de tarefa para obter respostas de streaming:</p>def stream_chat_completion(messages: list, inference_id: str = INFERENCE_ENDPOINT_ID):
    url = f"{ELASTICSEARCH_URL}/_inference/chat_completion/{inference_id}/_stream"
    payload = {"messages": messages}
    headers = {
        "Authorization": f"ApiKey {ELASTICSEARCH_API_KEY}",
        "Content-Type": "application/json",
    }

    try:
        response = requests.post(url, json=payload, headers=headers, stream=True)
        response.raise_for_status()

        for line in response.iter_lines(decode_unicode=True):
            if line:
                line = line.strip()

                if line.startswith("event:"):
                    continue

                if line.startswith("data: "):
                    data_content = line[6:]

                    if not data_content.strip() or data_content.strip() == "[DONE]":
                        continue

                    try:
                        chunk_data = json.loads(data_content)

                        if "choices" in chunk_data and len(chunk_data["choices"]) &gt; 0:
                            choice = chunk_data["choices"][0]
                            if "delta" in choice and "content" in choice["delta"]:
                                content = choice["delta"]["content"]
                                if content:
                                    yield content

                    except json.JSONDecodeError as json_err:
                        print(f"\nJSON decode error: {json_err}")
                        print(f"Problematic data: {data_content}")
                        continue

    except requests.exceptions.RequestException as e:
        yield f"Error: {str(e)}"<p>Agora podemos escrever uma função que chama a função de busca semântica, junto com o endpoint de inferência <code>chat_completions</code> e o endpoint de recomendações, para gerar os dados que serão alocados nos cartões:</p>def recommend_articles(search_query, index_name=INDEX_NAME, max_articles=5):
    print(f"\n{'='*80}")
    print(f"🔍 Search Query: {search_query}")
    print(f"{'='*80}\n")

    articles = perform_semantic_search(search_query, index_name, size=max_articles)

    if not articles:
        print("❌ No relevant articles found.")
        return None, None

    print(f"✅ Found {len(articles)} relevant articles\n")

    # Build context with found articles
    context = "Available blog articles:\n\n"
    for i, article in enumerate(articles, 1):
        source = article.get("_source", article)
        context += f"Article {i}:\n"
        context += f"- Title: {source.get('title', 'N/A')}\n"
        context += f"- Author: {source.get('author', 'N/A')}\n"
        context += f"- Category: {source.get('category', 'N/A')}\n"
        context += f"- Date: {source.get('date', 'N/A')}\n"
        context += f"- Content: {source.get('content', 'N/A')}\n\n"

    system_prompt = """You are an expert content curator that recommends blog articles.

    Write recommendations in a conversational style starting with phrases like:
    - "If you're interested in [topic], this article..."
    - "This post complements your search with..."
    - "For those looking into [topic], this article provides..."


    FORMAT REQUIREMENTS:
    - Return ONLY a JSON array
    - Each element must have EXACTLY these three fields: "article_number", "title", "recommendation"
    - If the original title is in spanish, use the "translated_title" subfield in the "title" field

    Keep each recommendation concise (2-3 sentences max) and focused on VALUE to the reader.

    EXAMPLE OF CORRECT FORMAT:
    [
        {"article_number": 1, "title": "Article title in english", "recommendation": "If you are interested in [topic], this article provides..."},
        {"article_number": 2, "title": "Article title in english", "recommendation": " for those looking into [topic], this article provides..."}
    ]

    Return ONLY the JSON array following this exact structure."""

    user_prompt = f"""Search query: "{search_query}"

    Generate recommendations for the following articles: {context}
    """

    messages = [
        {"role": "system", "content": "/no_think"},
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": user_prompt},
    ]

    # LLM generation
    print(f"{'='*80}")
    print("🤖 Generating personalized recommendations...\n")

    full_response = ""

    for chunk in stream_chat_completion(messages):
        print(chunk, end="", flush=True)
        full_response += chunk

    return context, articles, full_response<p>Finalmente, precisamos extrair as informações e formatá-las para serem impressas:</p>def display_recommendation_cards(articles, recommendations_text):
    print("\n" + "=" * 100)
    print("📇 RECOMMENDED ARTICLES".center(100))
    print("=" * 100 + "\n")

    # Parse JSON recommendations - clean tags and extract JSON
    recommendations_list = []
    try:

        # Clean up &lt;think&gt; tags
        cleaned_text = re.sub(
            r"&lt;think&gt;.*?&lt;/think&gt;", "", recommendations_text, flags=re.DOTALL
        )
        # Remove markdown code blocks ( ... ``` or ``` ... ```)
        cleaned_text = re.sub(r"```(?:json)?", "", cleaned_text)
        cleaned_text = cleaned_text.strip()

        parsed = json.loads(cleaned_text)

        # Extract recommendations from list format
        for item in parsed:
            article_number = item.get("article_number")
            title = item.get("title", "")
            rec_text = item.get("recommendation", "")

            if article_number and rec_text:
                recommendations_list.append(
                    {
                        "article_number": article_number,
                        "title": title,
                        "recommendation": rec_text,
                    }
                )
    except json.JSONDecodeError as e:
        print(f"⚠️  Could not parse recommendations as JSON: {e}")
        return

    for i, article in enumerate(articles, 1):
        source = article.get("_source", article)

        # Card border
        print("┌" + "─" * 98 + "┐")

        # Find recommendation and title for this article number
        recommendation = None
        title = None
        for rec in recommendations_list:
            if rec.get("article_number") == i:
                recommendation = rec.get("recommendation")
                title = rec.get("title")
                break

        # Print title
        title_lines = textwrap.wrap(f"📌 {title}", width=94)
        for line in title_lines:
            print(f"│  {line}".ljust(99) + "│")

        # Card border
        print("├" + "─" * 98 + "┤")

        # Print recommendation
        if recommendation:
            recommendation_lines = textwrap.wrap(recommendation, width=94)
            for line in recommendation_lines:
                print(f"│  {line}".ljust(99) + "│")

        # Card bottom
        print("└" + "─" * 98 + "┘")<p>Vamos testar isso fazendo uma pergunta sobre as postagens do blog de segurança:</p>search_query = "Security and vulnerabilities"

context, articles, recommendations = recommend_articles(search_query)

print("\nElasticsearch context:\n", context)

# Display visual cards
display_recommendation_cards(articles, recommendations)<p>Aqui podemos ver os cartões no console gerados pelo fluxo de trabalho:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4aa221a08a51aeb3/6a170d7460084be1413c45d6/730d35212594bb3db30447c3ea7e2a92857287b7-1999x1515.png" alt="Seção intitulada &quot;Artigos Recomendados&quot; apresenta cinco resumos de artigos em caixa, incluindo tópicos sobre vulnerabilidade no sistema de autenticação, riscos de migração, desempenho da REST API v2 e melhorias na autenticação, mudanças no sistema de notificações e um guia completo para a nova API." /><p>Você pode ver os resultados completos, incluindo todos os acertos e a resposta do LLM, <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/elasticsearch-inference-api-and-hugging-face/results.md">neste arquivo</a>.</p><p>Estamos pedindo artigos relacionados a: "Segurança e vulnerabilidades." Esta pergunta é usada como consulta de busca nos documentos armazenados no Elasticsearch. Os resultados recuperados são então passados para o modelo, que gera recomendações com base em seu conteúdo. Como podemos ver, o modelo fez um ótimo trabalho criando textos curtos envolventes que podem motivar o leitor a clicar.</p><h2>Conclusão</h2><p>Este exemplo mostra como Elasticsearch e Hugging Face podem ser combinados para criar um sistema centralizado rápido e eficiente para aplicações de IA. Essa abordagem reduz o esforço manual e oferece flexibilidade, graças ao extenso catálogo de modelos da Hugging Face. O uso do SmolLM3-3B, em particular, demonstra como modelos compactos e multilíngues ainda podem fornecer raciocínio significativo e geração de conteúdo quando combinados com busca semântica. Juntas, essas ferramentas oferecem uma base escalável e eficaz para construir análises inteligentes de conteúdo e aplicações multilíngues.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/hugging-face-elasticsearch-inference-api</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/hugging-face-elasticsearch-inference-api</guid>
    <category><![CDATA[IA agêntica]]></category>
    <category><![CDATA[Integrações]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5f961af4cb26ec97/6a170d767d8d6790c770e790/1417d6ff033712206c9bd4bcc22074ee3437ce96-1999x1125.png" length="0" type="image/png"/>
    <pubDate>Mon, 23 Mar 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Crie um fluxo de trabalho de busca por IA financeira usando LangGraph.js e Elasticsearch]]></title>
    <description><![CDATA[Aprenda a usar o LangGraph.js com o Elasticsearch para criar um fluxo de trabalho de busca financeira com IA que converte consultas em linguagem natural em filtros dinâmicos e condicionais para análise de investimentos e do mercado.]]></description>
    <content:encoded><![CDATA[<p>A criação de aplicativos de busca com IA geralmente envolve a coordenação de múltiplas tarefas, recuperação e extração de dados em um fluxo de trabalho integrado. O LangGraph simplifica esse processo ao permitir que os desenvolvedores orquestrem agentes de IA usando uma estrutura baseada em nós. Neste artigo, vamos construir uma solução financeira usando <a href="https://langchain-ai.github.io/langgraphjs/">LangGraph.js</a></p><h2>O que é LangGraph</h2><p><a href="https://langchain-ai.github.io/langgraphjs/">LangGraph</a> é um framework para construir agentes de IA e orquestrá-los em um fluxo de trabalho para criar aplicações assistidas por IA. O LangGraph possui uma arquitetura de nós onde podemos declarar funções que representam tarefas e atribuí-las como nós do fluxo de trabalho. O resultado de múltiplos nós interagindo será um gráfico. O LangGraph faz parte do ecossistema mais amplo <a href="https://js.langchain.com/docs/introduction/">LangChain</a>, que oferece ferramentas para construir sistemas de IA modulares e componíveis.</p><p>Para entender melhor por que o LangGraph é útil, vamos resolver uma situação problemática usando-o.</p><h2>Visão geral da solução</h2><p>Em uma empresa de capital de risco, os investidores têm acesso a um grande banco de dados com muitas opções de filtragem, mas quando se deseja combinar critérios, o processo se torna difícil e lento. Isso pode fazer com que algumas startups relevantes não sejam encontradas para investimento. Isso resulta em gastar muitas horas tentando identificar os melhores candidatos, ou até mesmo em perder oportunidades.</p><p>Com o LangGraph e o Elasticsearch, podemos realizar buscar filtradas utilizando linguagem natural, eliminando a necessidade de os usuários construírem manualmente solicitações complexas com dezenas de filtros. Para torná-lo mais flexível, o fluxo de trabalho decide automaticamente com base na entrada do usuário entre dois tipos de consultas:</p><ul><li><p><strong>Consultas focadas em investimento</strong>: essas consultas visam aspectos financeiros e de financiamento de startups, como <a href="https://www.investopedia.com/articles/personal-finance/102015/series-b-c-funding-what-it-all-means-and-how-it-works.asp">rodadas de financiamento</a>, avaliação ou <a href="https://www.investopedia.com/terms/r/revenue.asp">receita</a>. <em>Exemplo:</em> "Encontre startups com financiamento Série A ou Série B entre US$ 8 milhões e US$ 25 milhões e receita mensal acima de US$ 500 mil."</p></li><li><p><strong>Consultas focadas no mercado</strong>: essas consultas concentram-se em <a href="https://en.wikipedia.org/wiki/Vertical_market">verticais da indústria</a>, <a href="https://en.wikipedia.org/wiki/Target_market">mercados geográficos</a> ou <a href="https://www.investopedia.com/terms/b/businessmodel.asp">modelos de negócios</a>, ajudando a identificar oportunidades em setores ou regiões específicos. <em>Exemplo:</em> “Encontre startups de fintech e saúde em São Francisco, Nova York ou Boston.”</p></li></ul><p>Para manter a robustez das consultas, faremos com que o LLM crie <a href="https://www.elastic.co/docs/solutions/search/search-templates">modelos de busca</a> em vez de <a href="https://www.elastic.co/docs/explore-analyze/query-filter/languages/querydsl">consultas DSL</a> completas. Assim, você sempre recebe a consulta que quer, e o LLM só precisa preencher as lacunas e não carregar a responsabilidade de construir a consulta que você precisa toda vez.</p><h2>O que você precisa para começar</h2><ul><li><p>APIKey do Elasticsearch</p></li><li><p>APIKey do OpenAPI</p></li><li><p>Node 18 ou mais recente</p></li></ul><h2>Instruções passo a passo</h2><p>Nesta seção, vamos ver como o app ficará. Para isso, usaremos o <a href="https://www.typescriptlang.org/">TypeScript</a>, um superconjunto do JavaScript que adiciona tipos estáticos para tornar o código mais confiável, fácil de manter e mais seguro, detectando erros precocemente e, ao mesmo tempo, permanecendo totalmente compatível com o JavaScript existente.</p><p>O fluxo dos nós terá a seguinte aparência:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt90db8f03f372608c/6a170986dc55de6e16e00d93/b47d7f238c4964a6febc0de7fe5e68b186f539c3-363x555.png" alt="" /><p>A imagem acima é gerada pelo LangGraph e representa o fluxo de trabalho que define a ordem de execução e a lógica condicional entre nós:</p><ul><li><p><strong>decideStrategy: </strong>utiliza um LLM para analisar a consulta do usuário e decidir entre duas estratégias de busca especializadas: focada em investimento ou focada no mercado.</p></li><li><p><strong>prepareInvestSearch: </strong>extrai valores de filtro da consulta e constrói um modelo pré-definido enfatizando parâmetros financeiros e relacionados ao financiamento.</p></li><li><p><strong>prepareMarketSearch</strong>: também extrai valores de filtro, mas constrói parâmetros dinamicamente enfatizando o mercado, o setor e o contexto geográfico.</p></li><li><p><strong>executeSearch: </strong>envia a consulta construída para o Elasticsearch usando um modelo de busca e recupera os documentos correspondentes de inicialização.</p></li><li><p><strong>visualizeResults: </strong>formata os resultados finais em um resumo claro e legível que mostra atributos-chave da startup, como financiamento, setor e receita.</p></li></ul><p>Esse fluxo inclui uma <a href="https://langchain-ai.github.io/langgraphjs/how-tos/branching/?h=conditional#how-to-create-branches-for-parallel-node-execution">ramificação condicional</a>, funcionando como uma instrução “if”, que determina se deve usar o caminho de busca de investimentos ou de mercado com base na entrada do usuário. Essa lógica de decisão, conduzida pelo LLM, torna o fluxo de trabalho adaptável e sensível ao contexto, um mecanismo que exploraremos com mais detalhes nas próximas seções.</p><h3>Estado do LangGraph</h3><p>Antes de ver cada nó individualmente, precisamos entender como os nós se comunicam e compartilham dados. Para isso, o LangGraph nos permite definir o estado do fluxo de trabalho. Isso define o estado compartilhado que será passado entre os nós.</p><p>O estado funciona como um container compartilhado que armazena dados intermediários ao longo do fluxo de trabalho: começa com a consulta em linguagem natural do usuário, depois mantém a estratégia de busca selecionada, os parâmetros preparados para o Elasticsearch, os resultados de busca recuperados e, finalmente, a saída formatada.</p><p>Essa estrutura permite que cada nó leia e atualize o estado, garantindo um fluxo consistente de informações desde a entrada do usuário até a visualização final.</p>const VCState = Annotation.Root({
  input: Annotation&lt;string&gt;(), // User's natural language query
  searchStrategy: Annotation&lt;string&gt;(), // Search strategy chosen by LLM
  searchParams: Annotation&lt;any&gt;(), // Prepared search parameters
  results: Annotation&lt;any[]&gt;(), // Search results
  final: Annotation&lt;string&gt;(), // Final formatted response
});<h3>Configure o aplicativo</h3><p>Todo o código desta seção pode ser encontrado no <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch">repositório elasticsearch-labs</a>.</p><p>Abra um terminal na pasta em que o app estará localizado e inicialize um app Node.js com o comando:</p>npm init -y<p>Agora podemos instalar as dependências necessárias para este projeto:</p>npm install @elastic/elasticsearch @langchain/langgraph @langchain/openai @langchain/core dotenv zod &amp;&amp; npm install --save-dev @types/node tsx typescript<ul><li><p><strong><code>@elastic/elasticsearch</code></strong>: Nos ajuda a lidar com requisições do Elasticsearch, como ingestão e recuperação de dados.</p></li><li><p><strong><code>@langchain/langgraph</code></strong>: dependência de JS para fornecer todas as ferramentas LangGraph.</p></li><li><p><strong><code>@langchain/openai</code></strong>Cliente OpenAI LLM para LangChain.</p></li><li><p>@langchain/núcleo: fornece os blocos de construção fundamentais para apps LangChain, incluindo modelos de prompt.</p></li><li><p><strong><code>dotenv</code></strong>: Dependência necessária para usar variáveis de ambiente em JavaScript.</p></li><li><p><strong><code>zod</code></strong>: Dependência para digitar dados.</p></li></ul><p><code>@types/node</code> <code>tsx</code> <code>typescript</code> nos permite escrever e executar o código TypeScript.</p><p>Agora, crie os seguintes arquivos:</p><ul><li><p><code>elasticsearchSetup</code><a href="http://ingest.ts/"><code>.ts</code></a>: Criará os mapeamentos de índice, carregará o conjunto de dados de um arquivo JSON e fará a ingestão dos dados no Elasticsearch.</p></li><li><p><a href="http://main.ts/"><code>main.ts</code></a>: incluirá o aplicativo LangGraph.</p></li><li><p><code>.env</code>: arquivo para armazenar as variáveis de ambiente</p></li></ul><p>No arquivo <code>.env</code>, vamos adicionar as seguintes variáveis de ambiente:</p>ELASTICSEARCH_ENDPOINT="your-endpoint-here"
ELASTICSEARCH_API_KEY="your-key-here"
OPENAI_API_KEY="your-key-here"<p>O APIKey da OpenAPI não será usado diretamente no código; em vez disso, será usado internamente pela biblioteca <code>@langchain/openai</code>.</p><p>Toda a lógica relacionada à criação de mapeamentos, modelos de busca e ingestão de conjuntos de dados pode ser encontrada no arquivo <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/elasticsearchSetup.ts"><code>elasticsearchSetup.ts</code></a>. Nos próximos passos, vamos focar no arquivo <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/main.ts"><code>main.ts</code></a> . Além disso, você pode verificar o conjunto de dados para entender melhor como os dados aparecem no <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/dataset.json"><code>dataset.json</code></a>.</p><h3>Aplicativo LangGraph</h3><p>No arquivo <code>main.ts</code>, vamos importar algumas dependências necessárias para consolidar a aplicação LangGraph. Neste arquivo, você também deve incluir as funções de nós e a declaração de estado. A declaração do gráfico será feita em um método <code>main</code> nos próximos passos. O arquivo <code>elasticsearchSetup.ts</code> conterá ajudantes Elasticsearch que vamos usar dentro dos nós em etapas futuras.</p>import { writeFileSync } from "node:fs";
import { StateGraph, Annotation, START, END } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { z } from "zod";
import {
  esClient,
  ingestDocuments,
  createSearchTemplates,
  INDEX_NAME,
  INVESTMENT_FOCUSED_TEMPLATE,
  MARKET_FOCUSED_TEMPLATE,
  createIndex,
} from "./elasticsearchSetup.js";

const llm = new ChatOpenAI({ model: "gpt-4o-mini" });<p>Como mencionado anteriormente, o cliente LLM será usado para gerar os parâmetros de busca do Elasticsearch com base na pergunta do usuário.</p>async function saveGraphImage(app: any): Promise&lt;void&gt; {
  try {
    const drawableGraph = app.getGraph();
    const image = await drawableGraph.drawMermaidPng();
    const arrayBuffer = await image.arrayBuffer();

    const filePath = "./workflow_graph.png";
    writeFileSync(filePath, new Uint8Array(arrayBuffer));
    console.log(`📊 Workflow graph saved as: ${filePath}`);
  } catch (error: any) {
    console.log("⚠️  Could not save graph image:", error.message);
  }
}<p>O método acima gera a imagem do gráfico em formato PNG e usa a <a href="https://mermaid.ink/">API Mermaid.INK</a> nos bastidores. Isso é útil se você quiser ver como os nós do app interagem entre si com uma visualização estilizada.</p><h3>Nós do LangGraph</h3><p>Agora vamos analisar cada nó em detalhes:</p><h3>nó decideSearchStrategy</h3><p>O node <code>decideSearchStrategy</code> analisa a entrada do usuário e determina se realiza uma buscar focada em investimento ou no mercado. Ele utiliza um LLM com um esquema de saída estruturado (definido com Zod) para classificar o tipo de consulta. Antes de tomar a decisão, o sistema recupera os filtros disponíveis do índice por meio de uma agregação, garantindo que o modelo tenha um contexto atualizado sobre setores, locais e dados de financiamento.</p><p>Para extrair os valores possíveis dos filtros e enviá-los ao LLM, vamos usar uma consulta de <a href="https://www.elastic.co/docs/explore-analyze/query-filter/aggregations">agregação</a> para recuperá-los diretamente do índice do Elasticsearch. Essa lógica é alocada em um método chamado <code>getAvailableFilters</code>:</p>async function getAvailableFilters() {
  try {
    const response = await esClient.search({
      index: INDEX_NAME,
      size: 0,
      aggs: {
        industries: {
          terms: { field: "industry", size: 100 },
        },
        locations: {
          terms: { field: "location", size: 100 },
        },
        funding_stages: {
          terms: { field: "funding_stage", size: 20 },
        },
        business_models: {
          terms: { field: "business_model", size: 10 },
        },
        lead_investors: {
          terms: { field: "lead_investor", size: 100 },
        },
        funding_amount_stats: {
          stats: { field: "funding_amount" },
        },
      },
    });

    return response.aggregations;
  } catch (error) {
    console.error("❌ Error getting available filters:", error);
    return {};
  }
}<p>Com a consulta de agregação acima, temos os seguintes resultados:</p>{
  "industries": {
    "doc_count_error_upper_bound": 0,
    "sum_other_doc_count": 0,
    "buckets": [
      {
        "key": "logistics",
        "doc_count": 5
      },
      ...
    ]
  },
  "locations": {
    "doc_count_error_upper_bound": 0,
    "sum_other_doc_count": 0,
    "buckets": [
      {
        "key": "San Francisco, CA",
        "doc_count": 4
      },
      {
        "key": "New York, NY",
        "doc_count": 3
      },
      ...
    ]
  },
  "funding_stages": {
    "doc_count_error_upper_bound": 0,
    "sum_other_doc_count": 0,
    "buckets": [
      {
        "key": "Series A",
        "doc_count": 8
      },
      ...
    ]
  },
  "business_models": {
    "doc_count_error_upper_bound": 0,
    "sum_other_doc_count": 0,
    "buckets": [
      {
        "key": "B2B",
        "doc_count": 13
      },
      ...
    ]
  },
  "lead_investors": {
    "doc_count_error_upper_bound": 0,
    "sum_other_doc_count": 0,
    "buckets": [
      {
        "key": "Battery Ventures",
        "doc_count": 1
      },
      {
        "key": "Benchmark Capital",
        "doc_count": 1
      },
      ...
    ]
  },
  "funding_amount_stats": {
    "count": 20,
    "min": 4500000,
    "max": 35000000,
    "avg": 14075000,
    "sum": 281500000
  }
}<p>Veja todos os resultados <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/responses/aggregationsResponse.json">aqui</a>.</p><p>Para ambas as estratégias, usaremos busca híbrida para detectar tanto a parte estruturada da pergunta (filtros) quanto as partes mais subjetivas (semântica). Aqui está um exemplo de ambas as consultas usando <a href="https://www.elastic.co/docs/solutions/search/search-templates">templates de busca</a>:</p>await esClient.putScript({
      id: INVESTMENT_FOCUSED_TEMPLATE,
      script: {
        lang: "mustache",
        source: `{
          "size": 5,
          "retriever": {
            "rrf": {
              "retrievers": [
                {
                  "standard": {
                    "query": {
                      "semantic": {
                        "field": "semantic_field",
                        "query": "{{query_text}}"
                      }
                    }
                  }
                },
                {
                  "standard": {
                    "query": {
                      "bool": {
                        "filter": [
                          {"terms": {"funding_stage": {{#join}}{{#toJson}}funding_stage{{/toJson}}{{/join}}}},
                          {"range": {"funding_amount": {"gte": {{funding_amount_gte}}{{#funding_amount_lte}},"lte": {{funding_amount_lte}}{{/funding_amount_lte}}}}},
                          {"terms": {"lead_investor": {{#join}}{{#toJson}}lead_investor{{/toJson}}{{/join}}}},
                          {"range": {"monthly_revenue": {"gte": {{monthly_revenue_gte}}{{#monthly_revenue_lte}},"lte": {{monthly_revenue_lte}}{{/monthly_revenue_lte}}}}}
                        ]
                      }
                    }
                  }
                }
              ],
              "rank_window_size": 100,
              "rank_constant": 20
            }
          }
        }`,
      },
    });<p>Veja as consultas detalhadas no arquivo <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/elasticsearchSetup.ts#L119"><code>elasticsearchSetup.ts</code></a> . No nó a seguir, será decidido qual das duas consultas será usada:</p>// Node 1: Decide search strategy using LLM
async function decideSearchStrategy(state: typeof VCState.State) {
  // Zod schema for specialized search strategy decision
  const SearchDecisionSchema = z.object({
    search_type: z
      .enum(["investment_focused", "market_focused"])
      .describe("Type of specialized search strategy to use"),
    reasoning: z
      .string()
      .describe("Brief explanation of why this search strategy was chosen"),
  });

  const decisionLLM = llm.withStructuredOutput(SearchDecisionSchema);

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

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

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

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

    return {
      searchStrategy: result.search_type,
    };
  } catch (error: any) {
    console.error("❌ Error in decideSearchStrategy:", error.message);
    return {
      searchStrategy: "investment_focused",
    };
  }
}<h3>nós prepareInvestmentSearch e prepareMarketSearch</h3><p>Ambos os nós usam uma função auxiliar compartilhada, <code>extractFilterValues</code>, que utiliza o LLM para identificar filtros relevantes mencionados na entrada do usuário, como setor, localização, estágio de financiamento, modelo de negócios, etc. Estamos usando este esquema para construir nosso <a href="https://www.elastic.co/docs/solutions/search/search-templates">modelo de busca</a>.</p>// Extract all possible filter values from user input
async function extractFilterValues(input: string) {
  const FilterValuesSchema = z.object({
    // Investment-focused filters
    funding_stage: z
      .array(z.string())
      .default([])
      .describe("Funding stage values mentioned in query"),
    funding_amount_gte: z
      .number()
      .default(0)
      .describe("Minimum funding amount in USD"),
    funding_amount_lte: z
      .number()
      .default(100000000)
      .describe("Maximum funding amount in USD"),
    lead_investor: z
      .array(z.string())
      .default([])
      .describe("Lead investor values mentioned in query"),
    monthly_revenue_gte: z
      .number()
      .default(0)
      .describe("Minimum monthly revenue in USD"),
    monthly_revenue_lte: z
      .number()
      .default(10000000)
      .describe("Maximum monthly revenue in USD"),
    industry: z
      .array(z.string())
      .default([])
      .describe("Industry values mentioned in query"),
    location: z
      .array(z.string())
      .default([])
      .describe("Location values mentioned in query"),
    business_model: z
      .array(z.string())
      .default([])
      .describe("Business model values mentioned in query"),
  });

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

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

  return await extractorLLM.invoke(extractPrompt);
}<p>Dependendo da intenção detectada, o fluxo de trabalho seleciona um de dois caminhos:</p><p><strong>prepareInvestmentSearch:</strong> desenvolve parâmetros de busca orientados financeiramente, incluindo estágio de financiamento, valor do investimento, investidor e informações de renovação. Você pode encontrar o modelo completo de consulta no arquivo <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/elasticsearchSetup.ts"><code>elasticsearchSetup.ts</code></a>:</p>// Node 2A: Prepare Investment-Focused Search Parameters 
async function prepareInvestmentSearch(state: typeof VCState.State) {
  console.log(
    "💰 Preparing INVESTMENT-FOCUSED search parameters with financial emphasis..."
  );

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

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

    return { searchParams };
  } catch (error) {
    console.error("❌ Error preparing investment-focused params:", error);
    return {
      searchParams: {},
    };
  }
}<p><strong>prepareMarketSearch:</strong> cria parâmetros orientados pelo mercado, focados em setores, geografias e modelos de negócios. Veja a consulta completa no arquivo <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/elasticsearchSetup.ts"><code>elasticsearchSetup.ts</code></a> :</p>// Node 2B: Prepare Market-Focused Search Parameters
async function prepareMarketSearch(state: typeof VCState.State) {
  console.log(
    "🔍 Preparing MARKET-FOCUSED search parameters with market emphasis..."
  );

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

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

    return { searchParams };
  } catch (error) {
    console.error("❌ Error preparing market-focused params:", error);
    return {};
  }
}<h3>nó executeSearch</h3><p>Este nó pega os parâmetros de busca gerados do estado e os envia primeiro para o Elasticsearch, usando a <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-render-search-template">API _render</a> para visualizar a consulta para fins de depuração, e então envia uma solicitação para buscar os resultados.</p>// Node 3: Execute Search
async function executeSearch(state: typeof VCState.State) {
  const { searchParams } = state;

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

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

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

    return {
      results: results.hits.hits.map((hit: any) =&gt; hit._source),
    };
  } catch (error: any) {
    console.error(`❌ ${state.searchParams.search_type} search error:`, error);
    return { results: [] };
  }
}<h3>nó visualizeResults</h3><p>Por fim, este nó exibe os resultados do Elasticsearch.</p>// Node 4: Visualize results
async function visualizeResults(state: typeof VCState.State) {
  const results = state.results || [];

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

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

  return {
    final: formattedResults,
  };
}<p>Programaticamente, o gráfico completo tem a seguinte aparência:</p>  const workflow = new StateGraph(VCState)
    // Register nodes - these are the processing functions
    .addNode("decideStrategy", decideSearchStrategy)
    .addNode("prepareInvestment", prepareInvestmentSearch)
    .addNode("prepareMarket", prepareMarketSearch)
    .addNode("executeSearch", executeSearch)
    .addNode("visualizeResults", visualizeResults)
    // Define execution flow with conditional branching
    .addEdge(START, "decideStrategy") // Start with strategy decision
    .addConditionalEdges(
      "decideStrategy",
      (state: typeof VCState.State) =&gt; state.searchStrategy, // Conditional function
      {
        investment_focused: "prepareInvestment", // If investment focused -&gt; RRF template preparation
        market_focused: "prepareMarket", // If market focused -&gt; dynamic query preparation
      }
    )
    .addEdge("prepareInvestment", "executeSearch") // Investment prep -&gt; execute
    .addEdge("prepareMarket", "executeSearch") // Market prep -&gt; execute
    .addEdge("executeSearch", "visualizeResults") // Execute -&gt; visualize
    .addEdge("visualizeResults", END); // End workflow<p>Como você pode ver, temos uma aresta condicional onde o app decide qual "caminho" ou nó será executado em seguida. Esse recurso é útil quando fluxos de trabalho precisam de lógica de ramificação, como escolher entre várias ferramentas ou incluir uma etapa com uma pessoa no ciclo.</p><p>Com os recursos do núcleo do LangGraph entendidos, podemos configurar o aplicativo onde o código será executado:</p><p>Junte tudo em um método <code>main</code>; aqui declaramos o gráfico com todos os elementos sob a variável fluxo de trabalho:</p>async function main() {
  await createIndex();
  await createSearchTemplates();
  await ingestDocuments();

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


  const app = workflow.compile();

  await saveGraphImage(app);

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

  const marketResult = await app.invoke({ input: query });
  console.log(marketResult.final);
}<p>A variável de consulta simula a entrada do usuário inserida em uma barra de busca hipotética:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltba7189d5f4e63403/6a1709880e2e49cc3041a076/e8d76909eb2bc1bb62f3ca9a8b3e4b85fcec2893-1600x164.png" alt="" /><p>A partir da frase em linguagem natural "Encontre startups com financiamento Série A ou Série B entre US$ 8M–US$ 25M e receita mensal acima de US$ 500K", todos os filtros serão extraídos.</p><p>Finalmente, invoque o método principal:</p>main().catch(console.error);<h3>Resultados</h3>🔍 Checking if index exists...
🏗️ Creating index...
✅ Index created successfully!
Ingesting documents...
✅ Documents ingested successfully!
✅ Investment-focused template created successfully!
✅ Market-focused template created successfully!

📊 Workflow graph saved as: ./workflow_graph.png

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

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

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

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

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

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

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

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

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

✨  Done in 18.80s.<p>Para a entrada enviada, a aplicação escolhe o caminho <strong>focado no investimento</strong> e, como resultado, podemos ver a consulta Elasticsearch gerada pelo fluxo de trabalho LangGraph, que extrai os valores e intervalos a partir da entrada do usuário. Também podemos ver a consulta enviada para o Elasticsearch com os valores extraídos aplicados e, finalmente, os resultados formatados pelo node <code>visualizeResults</code> com os resultados.</p><p>Agora vamos testar o nó <strong>focado no mercado</strong> usando a consulta "Encontre startups de fintech e saúde em São Francisco, Nova York ou Boston":</p>...

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

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

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

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

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

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

...

✨  Done in 7.41s.<h2>Aprendizados</h2><p>Durante o processo de escrita, aprendi:</p><ul><li><p>Devemos mostrar ao LLM os valores exatos dos filtros; caso contrário, dependemos de o usuário digitar os valores exatos das coisas. Para baixa cardinalidade, essa abordagem é válida; mas, quando a cardinalidade é alta, precisamos de algum mecanismo para filtrar os resultados.</p></li><li><p>Usar templates para busca torna os resultados muito mais consistentes do que deixar o LLM escrever a consulta Elasticsearch, e também é mais rápido</p></li><li><p>Arestas condicionais são um mecanismo poderoso para construir aplicações com múltiplas variantes e caminhos ramificados.</p></li><li><p>A saída estruturada é extremamente útil ao gerar informações com LLMs porque impõe respostas previsíveis e seguras para tipos. Isso melhora a confiabilidade e reduz as interpretações errôneas imediatas.</p></li></ul><p>Combinar busca semântica e estruturada por meio da recuperação híbrida produz resultados melhores e mais relevantes, equilibrando precisão e compreensão do contexto.</p><h2>Conclusão</h2><p>Neste exemplo, combinamos LangGraph.js com o Elasticsearch para criar um fluxo de trabalho dinâmico capaz de interpretar consultas em linguagem natural e decidir entre estratégias de busca voltadas para finanças ou para o mercado. Essa abordagem reduz a complexidade de elaborar consultas manuais, ao mesmo tempo em que melhora a flexibilidade e a precisão para analistas de capital de risco.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/ai-agent-workflow-finance-langgraph-elasticsearch</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/ai-agent-workflow-finance-langgraph-elasticsearch</guid>
    <category><![CDATA[AI]]></category>
    <category><![CDATA[IA agêntica]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt013eba5d152f11f3/6a1709892b835f6784f4b1a6/12b6057d84c6356267cd178a3c6c1a5c61123ece-2000x1256.png" length="0" type="image/png"/>
    <pubDate>Fri, 05 Dec 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Painéis de controle com inteligência artificial: da visão ao Kibana]]></title>
    <description><![CDATA[Gere um painel de controle usando um LLM para processar uma imagem e transformá-la em um painel do Kibana.
]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/kibana/kibana-lens">O Kibana Lens</a> torna o arrastar e soltar de dashboards muito simples, mas quando você precisa de dezenas de painéis, o número de cliques aumenta. E se você pudesse esboçar um painel de controle, tirar uma captura de tela e deixar um profissional de Direito concluir todo o processo para você?</p><p>Neste artigo, vamos fazer isso acontecer. Criaremos um aplicativo que captura uma imagem de um painel, analisa nossos mapeamentos e, em seguida, gera um painel sem que precisemos usar o Kibana!</p><p><strong>Passos</strong>:</p><ol><li><p><a href="https://www.elastic.co/search-labs/blog/ai-powered-dashboards#background-&amp;-application-workflow">Contexto e fluxo de trabalho do aplicativo</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/ai-powered-dashboards#prepare-data">Preparar dados</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/ai-powered-dashboards#llm-configuration">Configuração LLM</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/ai-powered-dashboards#application-functions">Funções do aplicativo</a></p></li></ol><h2>Contexto e fluxo de trabalho do aplicativo</h2><p>A primeira ideia que me veio à mente foi deixar o LLM gerar todo o formato NDJSON <a href="https://www.elastic.co/docs/explore-analyze/find-and-organize/saved-objects">dos objetos salvos</a> pelo Kibana e, em seguida, importá-los para o Kibana.</p><p>Experimentamos alguns modelos:</p><ul><li><p>Gemini 2.5 pro</p></li><li><p>GPT o3 / o4-mini-high / 4.1</p></li><li><p>Soneto 4 de Claude</p></li><li><p>Grok 3</p></li><li><p>Deepseek (Deepthink R1)</p></li></ul><p>E para as sugestões, começamos com algo tão simples quanto:</p>You are an Elasticsearch Saved-Object generator (Kibana 9.0).
INPUTS
=====
1. PNG screenshot of a 4-panel dashboard (attached).
2. Index mapping (below) – trimmed down to only the fields present in the screenshot.
3. Example NDJSON of *one* metric visualization (below) for reference.

TASK
====
Return **only** a valid NDJSON array that recreates the dashboard exactly:
* 2 metric panels (Visits, Unique Visitors)
* 1 pie chart (Most used OS)
* 1 vertical bar chart (State Geo Dest)
* Use index pattern `kibana_sample_data_logs`.
* Preserve roughly the same layout (2×2 grid).
* Use `panelIndex` values 1-4 and random `id` strings.
* Kibana version: 9.0<p>Apesar de termos analisado <a href="https://www.elastic.co/search-labs/blog/function-calling-with-elastic#:~:text=Few%2Dshot%20prompting%20involves%20providing%20examples%20of%20the%20types%20of%20queries%20you%20want%20it%20to%20return%2C%20which%20helps%20in%20increasing%20consistency.">poucos exemplos</a> e explicações detalhadas sobre como construir cada visualização, não tivemos sucesso. Se você estiver interessado nessa experiência, pode encontrar detalhes <a href="https://gist.github.com/TomasMurua/a78dc283e115624731beffc98984b70b">aqui</a>.</p><p>O resultado com essa abordagem foi a visualização dessas mensagens ao tentar carregar os arquivos produzidos pelo LLM no Kibana:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9ea005966a783057/6a1707d266c4f90e4ef8bf88/2b599443b5613c9f0fc3235581614add5b4b3900-891x98.png" alt="" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5e5632d6d95b998c/6a1707d3a6c2b9441de79661/d87ccfc033bc00ee8188c5cae18043fbca22784c-741x233.png" alt="" /><p>Isso significa que o JSON gerado é inválido ou está mal formatado. Os problemas mais comuns foram o LLM produzir NDJSON incompleto, apresentar parâmetros incorretos ou retornar JSON comum em vez de NDJSON, independentemente de quanto nos esforçássemos para forçar o contrário.</p><p>Inspirados por <a href="https://www.elastic.co/search-labs/blog/llm-functions-elasticsearch-intelligent-query">este artigo</a> – onde <a href="https://www.elastic.co/docs/solutions/search/search-templates">os modelos de pesquisa</a> funcionaram melhor do que o método freestyle do LLM – decidimos fornecer modelos ao LLM em vez de solicitar a geração do arquivo NDJSON completo e, em seguida, usar os parâmetros fornecidos pelo LLM no código para criar as visualizações adequadas. Essa abordagem não decepcionou, além de ser previsível e extensível, já que agora o código realiza o trabalho pesado, e não o LLM.</p><p>O fluxo de trabalho da aplicação será o seguinte:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9f7738a4c7ddd0cd/6a1707d52b835f0a25f4b166/52c587cf0cf3517fdd4ee7ab95581dd4f2bce030-725x668.png" alt="" /><p></p><p><em>Para simplificar, omitiremos parte do código, mas você pode encontrar o código funcional da aplicação completa neste </em><a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/from-image-idea-to-kibana-dashboard-using-ai/from-image-idea-to-kibana-dashboard-using-ai.ipynb"><em><strong>notebook</strong></em></a><em>.</em></p><h2>Pré-requisitos</h2><p>Antes de começar o desenvolvimento, você precisará do seguinte:</p><ol><li><p>Python 3.8 ou superior</p></li><li><p>Um ambiente Python <a href="https://docs.python.org/3/library/venv.html">Venv</a></p></li><li><p>Uma instância do Elasticsearch em execução, juntamente com seu endpoint e chave de API.</p></li><li><p>Uma chave de API da OpenAI armazenada na variável de ambiente com o nome OPENAI_API_KEY:</p></li></ol>export OPENAI_API_KEY="your-openai-api-key"<h2>Preparar dados</h2><p>Para os dados, vamos manter a simplicidade e usar os logs de amostra da Elastic. Você pode aprender como importar esses dados para o seu cluster <a href="https://www.elastic.co/docs/manage-data/ingest/sample-data#add-sample-data-sets">aqui</a>.</p><p>Cada documento inclui detalhes sobre o host que enviou as solicitações ao aplicativo, juntamente com informações sobre a própria solicitação e seu status de resposta. Segue abaixo um exemplo de documento:</p>{
    "agent": "Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24",
    "bytes": 8509,
    "clientip": "70.133.115.149",
    "extension": "css",
    "geo": {
        "srcdest": "US:IT",
        "src": "US",
        "dest": "IT",
        "coordinates": {
            "lat": 38.05134111,
            "lon": -103.5106908
        }
    },
    "host": "cdn.elastic-elastic-elastic.org",
    "index": "kibana_sample_data_logs",
    "ip": "70.133.115.149",
    "machine": {
        "ram": 5368709120,
        "os": "osx"
    },
    "memory": null,
    "message": "70.133.115.149 - - [2018-08-30T23:35:31.492Z] \"GET /styles/semantic-ui.css HTTP/1.1\" 200 8509 \"-\" \"Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\"",
    "phpmemory": null,
    "referer": "http://twitter.com/error/john-phillips",
    "request": "/styles/semantic-ui.css",
    "response": 200,
    "tags": [
        "success",
        "info"
    ],
    "@timestamp": "2025-07-03T23:35:31.492Z",
    "url": "https://cdn.elastic-elastic-elastic.org/styles/semantic-ui.css",
    "utc_time": "2025-07-03T23:35:31.492Z",
    "event": {
        "dataset": "sample_web_logs"
    },
    "bytes_gauge": 8509,
    "bytes_counter": 51201128
}<p>Agora, vamos obter os mapeamentos do índice que acabamos de carregar, <code>kibana_sample_data_logs</code>:</p>INDEX_NAME = "kibana_sample_data_logs"

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

result = es_client.indices.get_mapping(index=INDEX_NAME)
index_mappings = result[list(result.keys())[0]]["mappings"]["properties"]<p>Vamos passar os mapeamentos junto com a imagem que carregaremos posteriormente.</p><h2>Configuração LLM</h2><p>Vamos configurar o LLM para usar <a href="https://python.langchain.com/docs/concepts/structured_outputs/">saída estruturada</a> para receber uma imagem como entrada e obter um JSON com as informações necessárias para passar à nossa função e gerar os objetos JSON.</p><p>Instalamos as dependências:</p>pip install elasticsearch pydantic langchain langchain-openai -q<p>O Elasticsearch nos ajudará a recuperar os <a href="https://www.elastic.co/docs/manage-data/data-store/mapping">mapeamentos de índice</a>. Pydantic permite definir esquemas em Python para depois solicitar que o LLM os siga, e <a href="https://www.elastic.co/search-labs/integrations/langchain">LangChain</a> é a estrutura que facilita a chamada de LLMs e ferramentas de IA.</p><p>Criaremos um esquema Pydantic para definir a saída desejada do LLM. O que precisamos saber da imagem é o tipo de gráfico, campo, título da visualização e título do painel:</p>class Visualization(BaseModel):
    title: str = Field(description="The dashboard title")
    type: List[Literal["pie", "bar", "metric"]]
    field: str = Field(
        description="The field that this visualization use based on the provided mappings"
    )


class Dashboard(BaseModel):
    title: str = Field(description="The dashboard title")
    visualizations: List[Visualization]<p>Para a entrada de imagem, enviaremos um painel que acabei de desenhar:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7870f6421986d11d/6a1707d78b73cb3408189fa3/36441d7b5dc1f3ff2ac2a30710208d57ad41c716-1600x898.jpg" alt="" /><p>Agora declaramos a chamada do modelo LLM e o carregamento da imagem. Essa função receberá os mapeamentos do índice do Elasticsearch e uma imagem do painel que desejamos gerar.</p><p>Com <code>with_structured_output</code> podemos usar nosso esquema Pydantic <code>Dashboard</code> como o objeto de resposta que o LLM produzirá. Com <a href="https://docs.pydantic.dev/latest/">o Pydantic</a>, podemos definir modelos de dados com validação, o que garante que a saída do modelo linear linear (LLM) corresponda à estrutura esperada.</p><p>Para converter a imagem para base64 e enviá-la como entrada, você pode usar um <a href="https://www.base64-image.de/">conversor online</a> ou fazer isso <a href="https://www.geeksforgeeks.org/python-convert-image-to-string-and-vice-versa/">por meio de código</a>.</p>prompt = f"""
    You are an expert in analyzing Kibana dashboards from images for the version 9.0.0 of Kibana.

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

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

    Index Mappings:
    {index_mappings}

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

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


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

    print("Dashboard values generated by the LLM successfully")
    print(dashboard_values)
except Exception as e:
    print(f"Failed to analyze image and match fields: {str(e)}")<p>O LLM já possui contexto sobre os dashboards do Kibana, então não precisamos explicar tudo no prompt, apenas alguns detalhes para garantir que ele não se esqueça de que está trabalhando com o Elasticsearch e o Kibana.</p><p>Vamos analisar a pergunta:</p><p>Seção</p><p>Razão</p><p>Você é especialista em analisar dashboards do Kibana a partir de imagens para a versão 9.0.0 do Kibana.</p><p>Ao reforçar isso no Elasticsearch e na versão do Elasticsearch, reduzimos a probabilidade de o LLM gerar parâmetros antigos/inválidos.</p><p>Você receberá uma imagem do painel de controle e um mapeamento do índice do Elasticsearch.</p><p>Explicamos que a imagem se refere a painéis de controle para evitar quaisquer interpretações errôneas por parte do LLM.</p><p>Abaixo estão os mapeamentos de índice para o índice no qual o painel se baseia. Use-os para ajudá-lo a entender os dados e os campos disponíveis. Mapeamentos de índice: {index_mappings}</p><p>É crucial fornecer os mapeamentos para que o LLM possa selecionar campos válidos dinamicamente. Caso contrário, poderíamos codificar os mapeamentos diretamente aqui, o que é muito rígido, ou confiar na imagem que contém os nomes de campo corretos, o que não é confiável.</p><p>Inclua apenas os campos relevantes para cada visualização, com base no que está visível na imagem.</p><p>Precisávamos adicionar esse reforço porque, às vezes, o programa tenta adicionar campos que não são relevantes para a imagem.</p><p>Isso retornará um objeto com uma matriz de visualizações para exibir:</p>"Dashboard values generated by the LLM successfully
title=""Client, Extension, OS, and Response Keyword Analysis""visualizations="[
   "Visualization(title=""Count of Client IP",
   "type="[
      "metric"
   ],
   "field=""clientip"")",
   "Visualization(title=""Extension Keyword Distribution",
   "type="[
      "pie"
   ],
   "field=""extension.keyword"")",
   "Visualization(title=""Most Used OS",
   "type="[
      "bar"
   ],
   "field=""machine.os.keyword"")",
   "Visualization(title=""Response Keyword Distribution",
   "type="[
      "bar"
   ],
   "field=""response.keyword"")"
]<h2>Processando a resposta do LLM</h2><p>NósCriamos um painel de exemplo 2x2 e o exportamos em JSON usando a <a href="https://www.elastic.co/docs/api/doc/kibana/operation/operation-get-dashboards-dashboard">API "Obter um painel"</a>. Em seguida, armazenamos os painéis como modelos de visualização (pizza, barra, métrica), onde podemos substituir alguns parâmetros para criar novas visualizações com campos diferentes, dependendo da pergunta.</p><p>Você pode ver os arquivos JSON do modelo <a href="https://github.com/Delacrobix/elasticsearch-labs/tree/supporting-blog-content/from-image-idea-to-kibana-dashboard-using-ai/supporting-blog-content/from-image-idea-to-kibana-dashboard-using-ai/templates"><strong>aqui</strong></a>. Observe como alteramos os valores dos objetos que queremos substituir posteriormente por {<code>variable_name</code>}
</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc55d69d84a08e668/6a1707d8a2929903acd00fb8/ec7e1ac0cd8b470df13e60940162b56778acb386-315x234.png" alt="" /><p>Com as informações fornecidas pelo LLM, podemos decidir qual modelo usar e quais valores substituir.</p><p><code>fill_template_with_analysis</code> receberão os parâmetros para um único painel, incluindo o modelo JSON da visualização, um título, um campo e as coordenadas da visualização na grade.</p><p>Em seguida, substituirá os valores do modelo e retornará a visualização JSON final.</p>def fill_template_with_analysis(
    template: Dict[str, Any],
    visualization: Visualization,
    grid_data: Dict[str, Any],
):
    template_str = json.dumps(template)
    replacements = {
	 "{visualization_id}": str(uuid.uuid4()),
        "{title}": visualization.title,
        "{x}": grid_data["x"],
        "{y}": grid_data["y"],
    }

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

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

    return json.loads(template_str)<p>Para simplificar, teremos coordenadas estáticas que atribuiremos aos painéis que o LLM decidir criar e produziremos um painel de controle em grade 2x2, como na imagem acima.</p># Filling templates fields
panels = []    
grid_data = [
    {"x": 0, "y": 0},
    {"x": 12, "y": 0},
    {"x": 0, "y": 12},
    {"x": 12, "y": 12},
]


i = 0

for vis in dashboard_values.visualizations:
    for vis_type in vis.type:
        template = templates.get(vis_type, templates.get("bar", {}))
        filled_panel = fill_template_with_analysis(template, vis, grid_data[i])
        panels.append(filled_panel)
        i += 1<p>Dependendo do tipo de visualização decidido pelo LLM, escolheremos um modelo de arquivo JSON e substituiremos as informações relevantes usando <code>fill_template_with_analysis</code> , depois adicionaremos o novo painel a uma matriz que usaremos posteriormente para criar o painel de controle.</p><p>Quando o painel estiver pronto, usaremos a <a href="https://www.elastic.co/docs/api/doc/kibana/operation/operation-post-dashboards-dashboard-id">API Criar um painel</a> para enviar o novo arquivo JSON ao Kibana e gerar o painel:
</p>try:
    dashboard_id = str(uuid.uuid4())

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

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

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

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

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

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

except Exception as e:
    print(f"Failed to create dashboard: {str(e)}")<p>Para executar o script e gerar o painel de controle, execute o seguinte comando no console:</p>python &lt;file_name&gt;.py<p>O resultado final será semelhante a este:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5ceffed004153a4f/6a1707d9a929cf9147ae0901/e909afbf0e47d9a6e0f7bd07dfb2efcfa5cf06ac-921x715.png" alt="" /><h2>Conclusão</h2><p>Os profissionais com formação em Letras demonstram suas fortes habilidades visuais ao realizar tarefas de conversão de texto em código ou ao transformar imagens em código. A API de dashboards também permite transformar arquivos JSON em dashboards e, com um LLM e algum código, podemos transformar imagens em um dashboard do Kibana.</p><p>O próximo passo é melhorar a flexibilidade dos elementos visuais do painel de controle, utilizando diferentes configurações de grade, tamanhos e posições do painel. Além disso, oferecer suporte a visualizações e tipos de visualização mais complexos seria uma adição útil a este aplicativo.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/ai-powered-dashboards</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/ai-powered-dashboards</guid>
    <category><![CDATA[Kibana]]></category>
    <category><![CDATA[AI]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo,Tomás Murúa]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt41727cbee6155a68/6a1707dbb0367dd2fd72bc86/eb60ceb2fbc3941745b21ae3357cbb6ea8fab18c-1443x811.png" length="0" type="image/png"/>
    <pubDate>Wed, 16 Jul 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elasticsearch em JavaScript da maneira correta, parte II]]></title>
    <description><![CDATA[Conheça as práticas recomendadas de produção e como executar o cliente Elasticsearch Node.js em ambientes serverless para reduzir erros na codificação. ]]></description>
    <content:encoded><![CDATA[<p>Esta é a segunda parte da nossa série sobre Elasticsearch em JavaScript. Na<a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i"> primeira parte,</a> aprendemos como configurar nosso ambiente corretamente, configurar o cliente Node.js, indexar dados e realizar buscas. Nesta segunda parte, aprenderemos como implementar as melhores práticas de produção e executar o cliente Elasticsearch <a href="http://node.js">Node.js</a> em ambientes Serverless.</p><p>Analisaremos:</p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#production-best-practices">Melhores práticas de produção</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#error-handling">Tratamento de erros</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#testing">Teste</a></p></li></ul></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#serverless-environments">Ambientes sem servidor</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#running-the-client-on-elastic-serverless">Executando o cliente no Elastic Serverless</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#running-the-client-on-function-as-a-service-environment">Executando o cliente em um ambiente de função como serviço.</a></p></li></ul></li></ul><p><em>Você pode conferir o código-fonte com os exemplos </em><a href="https://github.com/Delacrobix/JS-client-best-practices_article"><em><strong>aqui</strong></em></a><em><strong>.</strong></em></p><h2>Melhores práticas de produção</h2><h3>Tratamento de erros no Elasticsearch</h3><p>Uma funcionalidade útil do cliente Elasticsearch em Node.js é que ele expõe objetos para os possíveis erros no Elasticsearch, permitindo que você os valide e trate de diferentes maneiras.</p><p>Para <a href="https://www.elastic.co/docs/reference/elasticsearch/clients/javascript/connecting#client-error-handling">ver todos</a>, execute o seguinte comando: </p>const { errors } = require('@elastic/elasticsearch')
console.log(errors)<p>Vamos voltar ao exemplo de pesquisa e tratar de alguns dos possíveis erros:</p>app.get("/search/lexic", async (req, res) =&gt; {
 ....
  } catch (error) {
    if (error instanceof errors.ResponseError) {
      let errorMessage =
        "Response error!, query malformed or server down, contact the administrator!";

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

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

    res.status(500).json({
      success: false,
      results: null,
      error: error.message,
    });
  }
});<p><code>ResponseError</code> em particular, ocorrerá quando a resposta for <code>4xx</code> ou <code>5xx</code>, o que significa que a solicitação está incorreta ou o servidor não está disponível.</p><p>Podemos testar esse tipo de erro gerando consultas incorretas, como tentar <strong>fazer uma consulta de termo em um campo do tipo texto:</strong></p><p>Erro padrão:</p> {
    "success": false,
    "results": null,
    "error": "parsing_exception\n\tRoot causes:\n\t\tparsing_exception: [terms] query does not support [visit_details]"
}<p>Erro personalizado: </p>{
    "erroStatus": 400,
    "success": false,
    "results": null,
    "error": "Response error!, query malformed or server down; contact the administrator!"
}<p>Também podemos capturar e lidar com cada tipo de erro de uma determinada maneira. Por exemplo, podemos adicionar lógica de repetição em um <code>TimeoutError</code>.</p>app.get("/search/semantic", async (req, res) =&gt; {
    try {
  ...
  } catch (error) {
    if (error instanceof errors.TimeoutError) {


     // Retry logic...

      res.status(error.meta.statusCode).json({
        erroStatus: error.meta.statusCode,
        success: false,
        results: null,
        error:
          "The request took more than 10s after 3 retries. Try again later.",
      });
    }
  }
});<h3>Teste</h3><p>Os testes são essenciais para garantir a estabilidade do aplicativo. Para testar o código de forma isolada do Elasticsearch, podemos usar a biblioteca <a href="https://github.com/elastic/elasticsearch-js-mock">elasticsearch-js-mock</a> ao criar nosso cluster.</p><p>Esta biblioteca permite instanciar um cliente muito semelhante ao real, mas que responderá à nossa configuração substituindo apenas a camada HTTP do cliente por uma camada simulada, mantendo o restante igual ao original.</p><p>Vamos instalar a biblioteca mocks e <a href="https://github.com/avajs/ava">o AVA</a> para testes automatizados.</p><p><code>npm install @elastic/elasticsearch-mock</code></p><p><code>npm install --save-dev ava</code></p><p>Vamos configurar o arquivo <code>package.json</code> para executar os testes. Certifique-se de que esteja assim:</p>"type": "module",
	"scripts": {
		"test": "ava"
	},
	"devDependencies": {
		"ava": "^5.0.0"
	}<p>Vamos agora criar um arquivo <code>test.js</code> e instalar nosso cliente de simulação:</p>const { Client } = require('@elastic/elasticsearch')
const Mock = require('@elastic/elasticsearch-mock')

const mock = new Mock()
const client = new Client({
  node: 'http://localhost:9200',
  Connection: mock.getConnection()
})<p>Agora, adicione uma simulação para pesquisa semântica:</p>function createSemanticSearchMock(query, indexName) {
  mock.add(
    {
      method: "POST",
      path: `/${indexName}/_search`,
      body: {
        query: {
          semantic: {
            field: "semantic_field",
            query: query,
          },
        },
      },
    },
    () =&gt; {
      return {
        hits: {
          total: { value: 2, relation: "eq" },
          hits: [
            {
              _id: "1",
              _score: 0.9,
              _source: {
                owner_name: "Alice Johnson",
                pet_name: "Buddy",
                species: "Dog",
                breed: "Golden Retriever",
                vaccination_history: ["Rabies", "Parvovirus", "Distemper"],
                visit_details:
                  "Annual check-up and nail trimming. Healthy and active.",
              },
            },
            {
              _id: "2",
              _score: 0.7,
              _source: {
                owner_name: "Daniel Kim",
                pet_name: "Mochi",
                species: "Rabbit",
                breed: "Mixed",
                vaccination_history: [],
                visit_details:
                  "Nail trimming and general health check. No issues.",
              },
            },
          ],
        },
      };
    }
  );
}<p>Agora podemos criar um teste para o nosso código, garantindo que a parte do Elasticsearch sempre retorne os mesmos resultados:</p>import test from 'ava';

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

  createSemanticSearchMock(query, indexName);

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

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

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

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

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

  if (result.results.length &gt; 0) {
    t.true(
      "_source" in result.results[0],
      "Each result must have a _source property"
    );
    t.true(
      "pet_name" in result.results[0]._source,
      "Results must include the pet_name field"
    );
    t.true(
      "visit_details" in result.results[0]._source,
      "Results must include the visit_details field"
    );
  }
});<p>Vamos executar os testes.</p><p><code>npm run test</code></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt36304e286146f362/6a170559d7c02237b2de638f/42feae845ae8eae03c37ad7ad114e8db35984812-1186x302.png" alt="" /><p>Pronto! A partir de agora, podemos testar nosso aplicativo focando 100% no código e não em fatores externos.</p><h2>Ambientes sem servidor</h2><h3>Como executar o cliente no Elastic Serverless</h3><p>Já abordamos a execução do Elasticsearch na nuvem ou em infraestrutura local; no entanto, o cliente Node.js também oferece suporte a conexões com o <a href="https://www.elastic.co/guide/en/serverless/current/intro.html">Elastic Cloud Serverless</a>.</p><p>O Elastic Cloud Serverless permite que você crie um projeto onde não precisa se preocupar com a infraestrutura, já que a Elastic cuida disso internamente, e você só precisa se preocupar com os dados que deseja indexar e por quanto tempo deseja ter acesso a eles.</p><p>Do ponto de vista da utilização, o Serverless separa o processamento do armazenamento, proporcionando recursos de escalonamento automático tanto para <a href="https://www.elastic.co/search-labs/blog/elasticsearch-serverless-tier-autoscaling">pesquisa</a> quanto para <a href="https://www.elastic.co/search-labs/blog/elasticsearch-ingest-autoscaling">indexação</a>. Isso permite que você cultive apenas os recursos de que realmente precisa.</p><p>O cliente realiza as seguintes adaptações para se conectar ao Serverless:</p><ul><li><p>Desativa a detecção de pacotes e ignora quaisquer opções relacionadas a ela.</p></li><li><p>Ignora todos os nós passados na configuração, exceto o primeiro, e ignora quaisquer opções de filtragem e seleção de nós.</p></li><li><p>Habilita a compressão e o método `TLSv1_2_method` (igual à configuração para o Elastic Cloud).</p></li><li><p>Adiciona um cabeçalho HTTP `elastic-api-version` a todas as requisições.</p></li><li><p>Utiliza `CloudConnectionPool` por padrão em vez de `WeightedConnectionPool`.</p></li><li><p>Desativa os cabeçalhos `content-type` e `accept` fornecidos pelo fornecedor, em favor dos tipos MIME padrão.</p></li></ul><p>Para conectar seu projeto sem servidor, você precisa usar o parâmetro serverMode: serverless.</p>const { Client } = require('@elastic/elasticsearch')
const client = new Client({
  node: 'ELASTICSEARCH_ENDPOINT',
  auth: { apiKey: 'ELASTICSEARCH_API_KEY' },
  serverMode: "serverless",
});<h3>Como executar o cliente em um ambiente de função como serviço</h3><p>No exemplo, usamos um servidor Node.js, mas você também pode se conectar usando um ambiente de função como serviço com funções como AWS Lambda, GCP Run, etc.</p>'use strict'

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

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

exports.handler = async function (event, context) {
  // use the client
}<p>Outro exemplo é conectar-se a serviços como o Vercel, que também é serverless. Você pode conferir este <a href="https://github.com/elastic/elasticsearch-js/blob/main/docs/examples/proxy/README.md">exemplo completo</a> de como fazer isso, mas a parte mais relevante do <a href="https://github.com/elastic/elasticsearch-js/blob/main/docs/examples/proxy/api/search.js">endpoint de pesquisa</a> se parece com isto:</p>const response = await client.search(
  {
    index: INDEX,
    // You could directly send from the browser
    // the Elasticsearch's query DSL, but it will
    // expose you to the risk that a malicious user
    // could overload your cluster by crafting
    // expensive queries.
    query: {
      match: { field: req.body.text },
    },
  },
  {
    headers: {
      Authorization: `ApiKey ${token}`,
    },
  }
);<p>Este endpoint reside na pasta /api e é executado a partir do lado do servidor, de forma que o cliente só tenha controle sobre o parâmetro “texto” que corresponde ao termo de pesquisa.</p><p>A implicação de usar a função como serviço é que, ao contrário de um servidor que funciona 24 horas por dia, 7 dias por semana, as funções apenas ativam a máquina que executa a função e, assim que ela termina, a máquina entra em modo de repouso para consumir menos recursos.</p><p>Essa configuração pode ser conveniente se o aplicativo não receber muitas solicitações; caso contrário, os custos podem ser elevados. Você também precisa levar em consideração o <a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtime-environment.html">ciclo de vida das funções</a> e os tempos de execução (que, em alguns casos, podem ser de apenas alguns segundos).</p><h2>Conclusão</h2><p>Neste artigo, aprendemos como lidar com erros, o que é crucial em ambientes de produção. Também abordamos os testes da nossa aplicação enquanto simulávamos o serviço Elasticsearch, o que proporciona testes confiáveis independentemente do estado do cluster e nos permite focar no nosso código.</p><p>Por fim, demonstramos como criar uma infraestrutura totalmente sem servidor, provisionando tanto o Elastic Cloud Serverless quanto um aplicativo Vercel.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii</guid>
    <category><![CDATA[Javascript]]></category>
    <category><![CDATA[Noções básicas]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc58be329ffebcd60/6a17043e47d49c0bc62d88ab/70fb0ff949f6db9ac9b8a28ecb4329ab915ebf46-720x420.png" length="0" type="image/png"/>
    <pubDate>Mon, 19 May 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elasticsearch em JavaScript da maneira correta, parte I]]></title>
    <description><![CDATA[Explicando como criar um backend Elasticsearch pronto para produção em JavaScript.  

Saiba como usar o Elasticsearch com JavaScript para criar um servidor com diferentes endpoints de busca para consultar documentos do Elasticsearch, seguindo as melhores práticas de cliente/servidor.]]></description>
    <content:encoded><![CDATA[<p>Este é o primeiro artigo de uma série que aborda como usar o Elasticsearch com JavaScript. Nesta série, você aprenderá o básico de como usar o Elasticsearch em um ambiente JavaScript e revisará os recursos mais relevantes e as melhores práticas para criar um aplicativo de busca. Ao final, você saberá tudo o que precisa para executar o Elasticsearch usando JavaScript.</p><p>Nesta primeira parte, vamos analisar:</p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#environment">Ambiente</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#frontend,-backend,-or-serverless?">Frontend, backend ou serverless?</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#connecting-the-client">Conectando o cliente</a></p></li></ul></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#indexing-documents">Documentos de indexação</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#elasticsearch-client">Cliente Elasticsearch</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#semantic-mappings">Mapeamentos semânticos</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#bulk-helper">Auxiliar em massa</a></p></li></ul></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#searching-data">Dados de pesquisa</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#lexical-query-(/search/lexic?q=%3Cquery-term%3E)">Consulta Lexical</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#semantic-query-(/search/semantic?q=%3Cquery-term%3E)">Consulta semântica</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#hybrid-query-(/search/hybrid?q=%3Cquery-term%3E)">Consulta híbrida</a></p></li></ul></li></ul><p><em>Você pode conferir o código-fonte com os exemplos </em><a href="https://github.com/Delacrobix/JS-client-best-practices_article"><em><strong>aqui</strong></em></a><em><strong>.</strong></em></p><h3>O que é o cliente Elasticsearch para Node.js?</h3><p>O <a href="https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html">cliente Elasticsearch para Node.js</a> é uma biblioteca JavaScript que converte as chamadas HTTP REST da API do Elasticsearch em código JavaScript. Isso facilita o manuseio e permite o uso de ferramentas auxiliares que simplificam tarefas como a indexação de documentos em lotes.</p><h2>Ambiente</h2><h3>Frontend, backend ou serverless?</h3><p>Para criar nosso aplicativo de busca usando o cliente JavaScript, precisamos de pelo menos dois componentes: um cluster Elasticsearch e um ambiente de execução JavaScript para executar o cliente.</p><p>O cliente JavaScript é compatível com todas as soluções Elasticsearch (Cloud, on-premise e Serverless), e não há grandes diferenças entre elas, já que o cliente lida com todas as variações internamente, então você não precisa se preocupar com qual usar.</p><p>O ambiente de execução JavaScript, no entanto, deve ser executado a partir do <strong>servidor</strong> e <strong>não diretamente do navegador.</strong></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd3ec469c83e3a71a/6a17e3d5445de91da44d00b6/92ce6cfd923c8008fa44f617a58193642d9d5879-661x410.png" alt="Elasticsearch em ambiente JavaScript." /><p>Isso ocorre porque, ao acessar o Elasticsearch pelo navegador, o usuário pode obter informações confidenciais, como a chave da API do cluster, o host ou a própria consulta. A Elasticsearch recomenda <strong>nunca expor o cluster diretamente à internet </strong>e usar uma camada intermediária que abstraia todas essas informações, de forma que o usuário possa ver apenas os parâmetros. Você pode ler mais sobre este tópico <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/es-security-principles.html#security-protect-cluster-traffic">aqui</a>.</p><p>Sugerimos usar um esquema como este:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4d7f215f2e70230a/6a17e3d6fbc5f83de6491a13/a08769f08ec73fe57bf2e961cfdfbb1cdd57919d-972x429.png" alt="Configurando o cliente Node.js do Elasticsearch." /><p>Nesse caso, o cliente envia apenas os termos de pesquisa e uma chave de autenticação para o seu servidor, enquanto o seu servidor mantém o controle total da consulta e da comunicação com o Elasticsearch.</p><h3>Conectando o cliente</h3><p>Comece criando uma chave de API seguindo <a href="https://www.elastic.co/search-labs/tutorials/install-elasticsearch/elastic-cloud">estes passos</a>.</p><p>Seguindo o exemplo anterior, criaremos um servidor Express simples e nos conectaremos a ele usando um cliente de um servidor Node.js.</p><p>Vamos inicializar o projeto com o NPM e instalar o cliente Elasticsearch e <a href="https://expressjs.com/">o Express.</a> Esta última é uma biblioteca para iniciar servidores em Node.js. Usando o Express, podemos interagir com nosso backend via HTTP.</p><p>Vamos inicializar o projeto:</p><p><code>npm init -y</code></p><p>Instalar dependências:</p><p><code>npm install @elastic/elasticsearch express split2 dotenv</code></p><p>Deixe-me explicar melhor:</p><ul><li><p><a href="https://www.npmjs.com/package/@elastic/elasticsearch"><em><strong>@elastic/elasticsearch</strong></em></a>: É o cliente oficial do Node.js.</p></li><li><p><a href="https://www.npmjs.com/package/express"><em><strong>Express</strong></em></a>: Isso nos permitirá criar um servidor Node.js leve para expor o Elasticsearch.</p></li><li><p><a href="https://www.npmjs.com/package/split2"><em><strong>split2</strong></em></a>: Divide linhas de texto em um fluxo. Útil para processar nossos arquivos ndjson linha por linha.</p></li><li><p><a href="https://www.npmjs.com/package/dotenv"><em><strong>dotenv</strong></em></a>: Permite gerenciar variáveis de ambiente usando um arquivo .env. arquivo</p></li></ul><p>Crie um arquivo .env Abra o arquivo na raiz do projeto e adicione as seguintes linhas:</p>ELASTICSEARCH_ENDPOINT="Your Elasticsearch endpoint"
ELASTICSEARCH_API_KEY="Your Elasticssearch API"<p>Dessa forma, podemos importar essas variáveis usando o pacote <code>dotenv</code> .</p><p>Crie um arquivo <code>server.js</code> :</p>const express = require("express");
const bodyParser = require("body-parser");
const { Client } = require("@elastic/elasticsearch");
 
require("dotenv").config(); //environment variables setup

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


const app = express();

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


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

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

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

    res.status(500).json({
      success: false,
      clusterInfo: null,
      error: error.message,
    });
  }
});<p>Este código configura um servidor Express.js básico que escuta na porta 3000 e se conecta a um cluster Elasticsearch usando uma chave de API para autenticação. Inclui um endpoint /ping que, quando acessado por meio de uma solicitação GET, consulta o cluster Elasticsearch para obter informações básicas usando o método <code>.info()</code> do cliente Elasticsearch. </p><p>Se a consulta for bem-sucedida, ela retorna as informações do cluster em formato JSON; caso contrário, retorna uma mensagem de erro. O servidor também utiliza o middleware body-parser para lidar com os corpos das requisições JSON.</p><p>Execute o arquivo para iniciar o servidor:</p><p><code>node server.js</code></p><p>A resposta deve ser semelhante a esta:</p>Server running on port 3000<p>E agora, vamos consultar o endpoint <code>/ping</code> para verificar o status do nosso cluster Elasticsearch.</p>curl http://localhost:3000/ping
{
    "success": true,
    "clusterInfo": {
        "name": "instance-0000000000",
        "cluster_name": "61b7e19eec204d59855f5e019acd2689",
        "cluster_uuid": "BIfvfLM0RJWRK_bDCY5ldg",
        "version": {
            "number": "9.0.0",
            "build_flavor": "default",
            "build_type": "docker",
            "build_hash": "112859b85d50de2a7e63f73c8fc70b99eea24291",
            "build_date": "2025-04-08T15:13:46.049795831Z",
            "build_snapshot": false,
            "lucene_version": "10.1.0",
            "minimum_wire_compatibility_version": "8.18.0",
            "minimum_index_compatibility_version": "8.0.0"
        },
        "tagline": "You Know, for Search"
    }
}<h2>Documentos de indexação</h2><p>Uma vez conectados, podemos indexar documentos usando mapeamentos como <a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text">semantic_text</a> para pesquisa semântica e text para consultas de texto completo. Com esses dois tipos de campo, também podemos fazer <a href="https://www.elastic.co/what-is/hybrid-search">buscas híbridas</a>.</p><p>Criaremos um novo arquivo <code>load.js</code> para gerar os mapeamentos e carregar os documentos.</p><h3>Cliente Elasticsearch</h3><p>Primeiro precisamos instanciar e autenticar o cliente:</p>const { Client } = require("@elastic/elasticsearch");

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

const esClient = new Client({
  node: ELASTICSEARCH_ENDPOINT,
  auth: { apiKey: ELASTICSEARCH_API_KEY },
});<h3>Mapeamentos semânticos</h3><p>Criaremos um índice com dados sobre um hospital veterinário. Armazenaremos as informações do dono, do animal de estimação e os detalhes da visita.</p><p>Os dados nos quais desejamos realizar uma busca de texto completo, como nomes e descrições, serão armazenados como texto. Os dados das categorias, como a espécie ou raça do animal, serão armazenados como palavras-chave.</p><p>Além disso, copiaremos os valores de todos os campos para um campo semantic_text para podermos executar também uma pesquisa semântica nessas informações.</p>const INDEX_NAME = "vet-visits";

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

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

await createMappings(INDEX_NAME, {
  properties: {
    owner_name: {
      type: "text",
      copy_to: "semantic_field",
    },
    pet_name: {
      type: "text",
      copy_to: "semantic_field",
    },
    species: {
      type: "keyword",
      copy_to: "semantic_field",
    },
    breed: {
      type: "keyword",
      copy_to: "semantic_field",
    },
    vaccination_history: {
      type: "keyword",
      copy_to: "semantic_field",
    },
    visit_details: {
      type: "text",
      copy_to: "semantic_field",
    },
    semantic_field: {
      type: "semantic_text",
    },
  },
});<h3>Auxiliar em massa</h3><p>Outra vantagem do cliente é que podemos usar a <a href="https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/client-helpers.html#bulk-helper">função auxiliar</a> de indexação em lotes. A função auxiliar de processamento em lote nos permite lidar facilmente com aspectos como concorrência, novas tentativas e o que fazer com cada documento que passa pela função, seja com sucesso ou com falha.</p><p>Uma característica interessante dessa ferramenta auxiliar é a possibilidade de trabalhar com fluxos de dados. Essa função permite enviar um arquivo linha por linha, em vez de armazenar o arquivo inteiro na memória e enviá-lo para o Elasticsearch de uma só vez.</p><p>Para enviar os dados para o Elasticsearch, crie um arquivo chamado data.ndjson na raiz do projeto e adicione as informações abaixo (alternativamente, você pode baixar o arquivo com o conjunto de dados <a href="https://github.com/Delacrobix/JS-client-best-practices_article/blob/main/data.ndjson">aqui</a>):</p>{"owner_name":"Alice Johnson","pet_name":"Buddy","species":"Dog","breed":"Golden Retriever","vaccination_history":["Rabies","Parvovirus","Distemper"],"visit_details":"Annual check-up and nail trimming. Healthy and active."}
{"owner_name":"Marco Rivera","pet_name":"Milo","species":"Cat","breed":"Siamese","vaccination_history":["Rabies","Feline Leukemia"],"visit_details":"Slight eye irritation, prescribed eye drops."}
{"owner_name":"Sandra Lee","pet_name":"Pickles","species":"Guinea Pig","breed":"Mixed","vaccination_history":[],"visit_details":"Loss of appetite, recommended dietary changes."}
{"owner_name":"Jake Thompson","pet_name":"Luna","species":"Dog","breed":"Labrador Mix","vaccination_history":["Rabies","Bordetella"],"visit_details":"Mild ear infection, cleaning and antibiotics given."}
{"owner_name":"Emily Chen","pet_name":"Ziggy","species":"Cat","breed":"Mixed","vaccination_history":["Rabies","Feline Calicivirus"],"visit_details":"Vaccination update and routine physical."}
{"owner_name":"Tomás Herrera","pet_name":"Rex","species":"Dog","breed":"German Shepherd","vaccination_history":["Rabies","Parvovirus","Leptospirosis"],"visit_details":"Follow-up for previous leg strain, improving well."}
{"owner_name":"Nina Park","pet_name":"Coco","species":"Ferret","breed":"Mixed","vaccination_history":["Rabies"],"visit_details":"Slight weight loss; advised new diet."}
{"owner_name":"Leo Martínez","pet_name":"Simba","species":"Cat","breed":"Maine Coon","vaccination_history":["Rabies","Feline Panleukopenia"],"visit_details":"Dental cleaning. Minor tartar buildup removed."}
{"owner_name":"Rachel Green","pet_name":"Rocky","species":"Dog","breed":"Bulldog Mix","vaccination_history":["Rabies","Parvovirus"],"visit_details":"Skin rash, antihistamines prescribed."}
{"owner_name":"Daniel Kim","pet_name":"Mochi","species":"Rabbit","breed":"Mixed","vaccination_history":[],"visit_details":"Nail trimming and general health check. No issues."}<p>Usamos o split2 para transmitir as linhas do arquivo enquanto o auxiliar de processamento em lote as envia para o Elasticsearch.</p>const { createReadStream } = require("fs");
const split = require("split2");
 
const indexData = async (filePath, indexName) =&gt; {
  try {
    console.log(`Indexing data from ${filePath} into ${indexName}...`);

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

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

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

await indexData("./data.ndjson", INDEX_NAME);<p>O código acima lê um arquivo .ndjson. indexa cada objeto JSON em um índice Elasticsearch especificado usando o método <code>helpers.bulk</code> . Ele transmite o arquivo usando <code>createReadStream</code> e <code>split2</code>, configura metadados de indexação para cada documento e registra quaisquer documentos que não puderem ser processados. Após a conclusão, registra o número de itens indexados com sucesso.</p><p>Alternativamente à função <code>indexData</code> , você pode fazer o upload do arquivo diretamente pela interface do usuário usando o Kibana e usar a <a href="https://www.elastic.co/docs/manage-data/ingest/upload-data-files">interface de upload de arquivos de dados.</a></p><p>Executamos o arquivo para enviar os documentos para o nosso cluster Elasticsearch.</p><p><code>node load.js</code></p>Creating mappings for index vet-visits...
Index created successfully: { acknowledged: true, shards_acknowledged: true, index: 'vet-visits' }
Indexing data from ./data.ndjson into vet-visits...
Bulk indexing completed. Total documents: 10, Failed: 0<h2>Buscando dados no Elasticsearch</h2><p>Voltando ao nosso arquivo <code>server.js</code> , criaremos diferentes endpoints para realizar buscas lexicais, semânticas ou híbridas.</p><p>Em resumo, esses tipos de pesquisa não são mutuamente exclusivos, mas dependerão do tipo de pergunta que você precisa responder.</p><p>Tipo de consulta</p><p>Caso de uso</p><p>Exemplo de pergunta</p><p>Consulta lexical</p><p>As palavras ou radicais presentes na pergunta provavelmente aparecerão nos documentos indexados. Similaridade entre tokens na pergunta e nos documentos.</p><p>Estou procurando uma camiseta esportiva azul.</p><p>Consulta semântica</p><p>É improvável que as palavras da pergunta apareçam nos documentos. Similaridade conceitual entre a pergunta e os documentos.</p><p>Estou procurando roupas para clima frio.</p><p>Busca híbrida</p><p>A questão contém componentes lexicais e/ou semânticos. Similaridade semântica e de tokens entre a pergunta e os documentos.</p><p>Estou procurando um vestido tamanho P para um casamento na praia.</p><p>As partes <em><strong>lexicais </strong></em>da pergunta provavelmente fazem parte de títulos e descrições, ou nomes de categorias, enquanto as partes <em><strong>semânticas </strong></em>são conceitos relacionados a esses campos. <em><strong>"Azul"</strong></em> provavelmente será o nome de uma categoria ou parte de uma descrição, e <em><strong>"casamento na praia"</strong></em> provavelmente não será, mas pode estar semanticamente relacionado a roupas de linho.</p><h3>Consulta lexical (/search/lexic?q=)&lt;query_term&gt;</h3><p>A busca lexical, também chamada de busca de texto completo, significa pesquisar com base na similaridade de tokens; ou seja, após uma análise, os documentos que incluem os tokens da busca serão retornados.</p><p>Você pode conferir nosso tutorial prático de busca lexical <a href="https://www.elastic.co/demo-gallery/lexical-search">aqui</a>.</p>app.get("/search/lexic", async (req, res) =&gt; {
  const { q } = req.query;

  const INDEX_NAME = "vet-visits";

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

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

    res.status(500).json({
      success: false,
      results: null,
      error: error.message,
    });
  }
});<p>Testamos com: <em><strong>corte de unhas</strong></em></p>curl http://localhost:3000/search/lexic?q=nail%20trimming<p>Responder:</p>{
    "success": true,
    "results": [
        {
            "_index": "vet-visits",
            "_id": "-RY6RJYBLe2GoFQ6-9n9",
            "_score": 2.7075968,
            "_source": {
                "pet_name": "Mochi",
                "owner_name": "Daniel Kim",
                "species": "Rabbit",
                "visit_details": "Nail trimming and general health check. No issues.",
                "breed": "Mixed",
                "vaccination_history": []
            }
        },
        {
            "_index": "vet-visits",
            "_id": "8BY6RJYBLe2GoFQ6-9n9",
            "_score": 2.560356,
            "_source": {
                "pet_name": "Buddy",
                "owner_name": "Alice Johnson",
                "species": "Dog",
                "visit_details": "Annual check-up and nail trimming. Healthy and active.",
                "breed": "Golden Retriever",
                "vaccination_history": [
                    "Rabies",
                    "Parvovirus",
                    "Distemper"
                ]
            }
        }
    ]
}<h3>Consulta semântica&lt;query_term&gt; (/search/semantic?q=)</h3><p>A busca semântica, diferentemente da busca lexical, encontra resultados que são semelhantes ao significado dos termos de busca por meio de busca vetorial.</p><p>Você pode conferir nosso tutorial prático de busca semântica <a href="https://www.elastic.co/demo-gallery/semantic-search">aqui</a>.</p>app.get("/search/semantic", async (req, res) =&gt; {
  const { q } = req.query;

  const INDEX_NAME = "vet-visits";

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

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

    res.status(500).json({
      success: false,
      results: null,
      error: error.message,
    });
  }
});<p>Fizemos o teste com a seguinte pergunta: <em><strong>Quem fez pedicure?</strong></em></p>curl http://localhost:3000/search/semantic?q=Who%20got%20a%20pedicure?<p>Responder:</p>{
    "success": true,
    "results": [
        {
            "_index": "vet-visits",
            "_id": "-RY6RJYBLe2GoFQ6-9n9",
            "_score": 4.861466,
            "_source": {
                "owner_name": "Daniel Kim",
                "pet_name": "Mochi",
                "species": "Rabbit",
                "breed": "Mixed",
                "vaccination_history": [],
                "visit_details": "Nail trimming and general health check. No issues."
            }
        },
        {
            "_index": "vet-visits",
            "_id": "8BY6RJYBLe2GoFQ6-9n9",
            "_score": 4.7152824,
            "_source": {
                "pet_name": "Buddy",
                "owner_name": "Alice Johnson",
                "species": "Dog",
                "visit_details": "Annual check-up and nail trimming. Healthy and active.",
                "breed": "Golden Retriever",
                "vaccination_history": [
                    "Rabies",
                    "Parvovirus",
                    "Distemper"
                ]
            }
        },
        {
            "_index": "vet-visits",
            "_id": "9RY6RJYBLe2GoFQ6-9n9",
            "_score": 1.6717153,
            "_source": {
                "pet_name": "Rex",
                "owner_name": "Tomás Herrera",
                "species": "Dog",
                "visit_details": "Follow-up for previous leg strain, improving well.",
                "breed": "German Shepherd",
                "vaccination_history": [
                    "Rabies",
                    "Parvovirus",
                    "Leptospirosis"
                ]
            }
        },
        {
            "_index": "vet-visits",
            "_id": "9xY6RJYBLe2GoFQ6-9n9",
            "_score": 1.5600781,
            "_source": {
                "pet_name": "Simba",
                "owner_name": "Leo Martínez",
                "species": "Cat",
                "visit_details": "Dental cleaning. Minor tartar buildup removed.",
                "breed": "Maine Coon",
                "vaccination_history": [
                    "Rabies",
                    "Feline Panleukopenia"
                ]
            }
        },
        {
            "_index": "vet-visits",
            "_id": "-BY6RJYBLe2GoFQ6-9n9",
            "_score": 1.2696637,
            "_source": {
                "pet_name": "Rocky",
                "owner_name": "Rachel Green",
                "species": "Dog",
                "visit_details": "Skin rash, antihistamines prescribed.",
                "breed": "Bulldog Mix",
                "vaccination_history": [
                    "Rabies",
                    "Parvovirus"
                ]
            }
        }
    ]
}<h3>Consulta híbrida (/search/hybrid?q=)&lt;query_term&gt;</h3><p>A busca híbrida permite combinar a busca semântica e a busca lexical, obtendo assim o melhor dos dois mundos: a precisão da busca por token, juntamente com a proximidade de significado da busca semântica.</p>app.get("/search/hybrid", async (req, res) =&gt; {
  const { q } = req.query;

  const INDEX_NAME = "vet-visits";

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

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

    res.status(500).json({
      success: false,
      results: null,
      error: error.message,
    });
  }
});<p>Fizemos o teste com a pergunta: “<em><strong>Quem fez pedicure ou tratamento dentário?”</strong></em></p>curl http://localhost:3000/search/hybrid?q=who%20got%20a%20pedicure%20or%20dental%20treatment<p>Resposta.</p>{
    "success": true,
    "results": [
        {
            "_index": "vet-visits",
            "_id": "9xY6RJYBLe2GoFQ6-9n9",
            "_score": 0.032522473,
            "_source": {
                "pet_name": "Simba",
                "owner_name": "Leo Martínez",
                "species": "Cat",
                "visit_details": "Dental cleaning. Minor tartar buildup removed.",
                "breed": "Maine Coon",
                "vaccination_history": [
                    "Rabies",
                    "Feline Panleukopenia"
                ]
            }
        },
        {
            "_index": "vet-visits",
            "_id": "-RY6RJYBLe2GoFQ6-9n9",
            "_score": 0.016393442,
            "_source": {
                "pet_name": "Mochi",
                "owner_name": "Daniel Kim",
                "species": "Rabbit",
                "visit_details": "Nail trimming and general health check. No issues.",
                "breed": "Mixed",
                "vaccination_history": []
            }
        },
        {
            "_index": "vet-visits",
            "_id": "8BY6RJYBLe2GoFQ6-9n9",
            "_score": 0.015873017,
            "_source": {
                "pet_name": "Buddy",
                "owner_name": "Alice Johnson",
                "species": "Dog",
                "visit_details": "Annual check-up and nail trimming. Healthy and active.",
                "breed": "Golden Retriever",
                "vaccination_history": [
                    "Rabies",
                    "Parvovirus",
                    "Distemper"
                ]
            }
        },
        {
            "_index": "vet-visits",
            "_id": "9RY6RJYBLe2GoFQ6-9n9",
            "_score": 0.015625,
            "_source": {
                "pet_name": "Rex",
                "owner_name": "Tomás Herrera",
                "species": "Dog",
                "visit_details": "Follow-up for previous leg strain, improving well.",
                "breed": "German Shepherd",
                "vaccination_history": [
                    "Rabies",
                    "Parvovirus",
                    "Leptospirosis"
                ]
            }
        },
        {
            "_index": "vet-visits",
            "_id": "8xY6RJYBLe2GoFQ6-9n9",
            "_score": 0.015384615,
            "_source": {
                "pet_name": "Luna",
                "owner_name": "Jake Thompson",
                "species": "Dog",
                "visit_details": "Mild ear infection, cleaning and antibiotics given.",
                "breed": "Labrador Mix",
                "vaccination_history": [
                    "Rabies",
                    "Bordetella"
                ]
            }
        }
    ]
}<h2>Conclusão</h2><p>Nesta primeira parte da nossa série, explicamos como configurar nosso ambiente e criar um servidor com diferentes endpoints de pesquisa para consultar os documentos do Elasticsearch, seguindo as melhores práticas de cliente/servidor. Confira <a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i">a segunda parte</a> da nossa série, na qual você aprenderá as melhores práticas de produção e como executar o cliente Elasticsearch Node.js em ambientes Serverless.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i</guid>
    <category><![CDATA[Javascript]]></category>
    <category><![CDATA[Noções básicas]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt16d00c8a548b32e8/6a17e3d8fbc5f8c740491a19/72200540ed258779d87e53a72ea189f8a138540c-1600x901.png" length="0" type="image/png"/>
    <pubDate>Thu, 15 May 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Usando Ollama com a API de Inferência]]></title>
    <description><![CDATA[Aprenda como integrar o Ollama com o Elasticsearch usando a API de Inferência.]]></description>
    <content:encoded><![CDATA[<p>Neste artigo, aprenderemos como conectar modelos locais ao modelo de inferência do Elasticsearch usando o Ollama e, em seguida, fazer perguntas aos seus documentos usando o Playground.</p><p>O Elasticsearch permite que os usuários se conectem a LLMs usando a Open <a href="https://www.elastic.co/pt/guide/en/elasticsearch/reference/current/inference-apis.html">Inference API</a>, oferecendo suporte a provedores como Amazon Bedrock, Cohere, Google AI, Azure AI Studio, HuggingFace - como serviço, entre outros.</p><p><a href="https://ollama.com">Ollama</a> é uma ferramenta que permite baixar e executar modelos LLM usando sua própria infraestrutura (sua máquina/servidor local). <a href="https://ollama.com/library">Aqui</a> você encontra uma lista dos modelos disponíveis que são compatíveis com o Ollama.</p><p>O Ollama é uma ótima opção se você deseja hospedar e testar diferentes modelos de código aberto sem ter que se preocupar com as diferentes maneiras como cada um dos modelos pode ter que ser configurado ou sobre como criar uma API para acessar as funções do modelo, pois o Ollama cuida de tudo.</p><p>Como a API Ollama é compatível com a API OpenAI, podemos integrar facilmente o modelo de inferência e criar um aplicativo RAG usando o Playground.</p><h2>Pré-requisitos</h2><ol><li><p>Elasticsearch 8.17</p></li><li><p>Kibana 8.17</p></li><li><p>Python</p></li></ol><h2>Etapas</h2><ol><li><p><a href="https://www.elastic.co/pt/search-labs/blog/ollama-with-inference-api#setting-up-ollama-llm-server">Configurando o servidor Ollama LLM</a></p></li><li><p><a href="https://www.elastic.co/pt/search-labs/blog/ollama-with-inference-api#creating-mappings">Criando mapeamentos</a></p></li><li><p><a href="https://www.elastic.co/pt/search-labs/blog/ollama-with-inference-api#indexing-data">Indexação de dados</a></p></li><li><p><a href="https://www.elastic.co/pt/search-labs/blog/ollama-with-inference-api#asking-questions-using-playground">Fazendo perguntas usando o Playground</a></p></li></ol><h2>Configurando o servidor Ollama LLM</h2><p>Vamos configurar um servidor LLM para conectá-lo à nossa instância do Playground usando o Ollama. Precisaremos:</p><ul><li><p>Baixe e execute o Ollama.</p></li><li><p>Use o ngrok para acessar seu servidor web local que hospeda o Ollama pela internet</p></li></ul><h3>Baixe e execute o Ollama</h3><p>Para usar o Ollama, primeiro precisamos <a href="https://ollama.com/download">baixá-lo</a>. O Ollama oferece suporte para Linux, Windows e macOS, então basta baixar a versão do Ollama compatível com seu sistema operacional <a href="https://ollama.com/download">aqui.</a> Depois que o Ollama estiver instalado, podemos escolher um modelo desta <a href="https://ollama.com/library">lista</a> de LLMs suportados. Neste exemplo, usaremos o modelo <a href="https://ollama.com/library/llama3.2">llama3.2</a>, um modelo multilíngue geral. No processo de configuração, você habilitará a ferramenta de linha de comando do Ollama. Depois que o download for concluído, você pode executar a seguinte linha:</p>ollama pull llama3.2<p>O que produzirá:</p>pulling manifest
pulling dde5aa3fc5ff... 100% ▕█████████████████████████████████████████████████████████████████████████████████████████▏ 2.0 GB
pulling 966de95ca8a6... 100% ▕█████████████████████████████████████████████████████████████████████████████████████████▏ 1.4 KB
pulling fcc5a6bec9da... 100% ▕█████████████████████████████████████████████████████████████████████████████████████████▏ 7.7 KB
pulling a70ff7e570d9... 100% ▕█████████████████████████████████████████████████████████████████████████████████████████▏ 6.0 KB
pulling 56bb8bd477a5... 100% ▕█████████████████████████████████████████████████████████████████████████████████████████▏   96 B
pulling 34bb5ab01051... 100% ▕█████████████████████████████████████████████████████████████████████████████████████████▏  561 B
verifying sha256 digest
writing manifest
success<p>Uma vez instalado, você pode testá-lo com este comando:</p>ollama run llama3.2<p>Vamos fazer uma pergunta:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc12f240920e897f5/6a17f39425daab32a508a367/ad1eff81c1b04d2a747c3afd0ecbc215e5bd96fd-800x501.gif" alt="Execute Ollama e faça uma pergunta" /><p>Com o modelo em execução, o Ollama habilita uma API que seria executada por padrão na porta "11434". Vamos fazer uma requisição para essa API, seguindo a <a href="https://github.com/ollama/ollama/blob/main/docs/api.md">documentação oficial</a>:</p>curl http://localhost:11434/api/generate -d '{                                          
  "model": "llama3.2",               
  "prompt": "What is the capital of France?"
}' <p>Esta é a resposta que obtivemos:</p>{"model":"llama3.2","created_at":"2024-11-28T21:48:42.152817532Z","response":"The","done":false}
{"model":"llama3.2","created_at":"2024-11-28T21:48:42.251884485Z","response":" capital","done":false}
{"model":"llama3.2","created_at":"2024-11-28T21:48:42.347365913Z","response":" of","done":false}
{"model":"llama3.2","created_at":"2024-11-28T21:48:42.446837322Z","response":" France","done":false}
{"model":"llama3.2","created_at":"2024-11-28T21:48:42.542367394Z","response":" is","done":false}
{"model":"llama3.2","created_at":"2024-11-28T21:48:42.644580384Z","response":" Paris","done":false}
{"model":"llama3.2","created_at":"2024-11-28T21:48:42.739865362Z","response":".","done":false}
{"model":"llama3.2","created_at":"2024-11-28T21:48:42.834347518Z","response":"","done":true,"done_reason":"stop","context":[128006,9125,128007,271,38766,1303,33025,2696,25,6790,220,2366,18,271,128009,128006,882,128007,271,3923,374,279,6864,315,9822,30,128009,128006,78191,128007,271,791,6864,315,9822,374,12366,13],"total_duration":6948567145,"load_duration":4386106503,"prompt_eval_count":32,"prompt_eval_duration":1872000000,"eval_count":8,"eval_duration":684000000}<p><em>Observe que a resposta específica para esse ponto de extremidade é um streaming.</em></p><h3>Exponha o endpoint à internet usando o ngrok</h3><p>Como nosso endpoint funciona em um ambiente local, ele não pode ser acessado de outro ponto, como nossa instância do Elastic Cloud, pela Internet. <a href="https://ngrok.com">O ngrok</a> nos permite expor uma porta que oferece um IP público. Crie uma conta no ngrok e siga o <a href="https://dashboard.ngrok.com/get-started/setup">guia oficial de configuração</a>.</p><p>Depois que o agente ngrok for instalado e configurado, podemos expor a porta que o Ollama está usando:</p>ngrok http 11434 --host-header="localhost:11434"<p><em>Observação: o cabeçalho </em><em><code>--host-header="localhost:11434"</code></em><em> garante que o cabeçalho "Host" nas solicitações corresponda a "localhost:11434"</em></p><p>Executar este comando retornará um link público que funcionará enquanto o ngrok e o servidor Ollama forem executados localmente.</p>Session Status                online                                                                                                                                                                              
Account                       xxxx@yourEmailProvider.com (Plan: Free)                                                                                                                                             
Version                       3.18.4                                                                                                                                                                              
Region                        United States (us)                                                                                                                                                                  
Latency                       561ms                                                                                                                                                                               
Web Interface                 http://127.0.0.1:4040                                                                                                                                                               
Forwarding                    https://your-ngrok-url.ngrok-free.app -&gt; http://localhost:11434                                                                                                                   


Connections                   ttl     opn     rt1     rt5     p50     p90                                                                                                                                         
                              0       0       0.00    0.00    0.00    0.00                                                ```<p>Em "Encaminhamento" podemos ver que o ngrok gerou uma URL. Guarde para mais tarde.</p><p>Vamos tentar fazer uma solicitação HTTP para o endpoint novamente, agora usando a URL gerada pelo ngrok:</p>curl https://your-ngrok-endpoint.ngrok-free.app/api/generate -d '{                                          
  "model": "llama3.2",               
  "prompt": "What is the capital of France?"
}'<p>A resposta deve ser semelhante à anterior.</p><h2>Criando mapeamentos</h2><h3>Ponto final ELSER</h3><p>Neste exemplo, <a href="https://www.elastic.co/pt/guide/en/elasticsearch/reference/current/put-inference-api.html">criaremos um ponto de extremidade de inferência usando a API de inferência do Elasticsearch</a>. Além disso, usaremos <a href="https://www.elastic.co/pt/guide/en/machine-learning/current/ml-nlp-elser.html">o ELSER</a> para gerar os embeddings.</p>PUT _inference/sparse_embedding/medicines-inference
{
  "service": "elasticsearch",
  "service_settings": {
    "num_allocations": 1,
    "num_threads": 1,
    "model_id": ".elser_model_2_linux-x86_64"
  }
}<p>Para este exemplo, vamos imaginar que você tem uma farmácia que vende dois tipos de medicamentos:</p><ul><li><p>Medicamentos que exigem receita médica.</p></li><li><p>Medicamentos que NÃO exigem receita médica.</p></li></ul><p>Essas informações seriam incluídas no campo de descrição de cada medicamento.</p><p>O LLM deve interpretar esse campo, então estes são os mapeamentos de dados que usaremos:</p>PUT medicines
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "copy_to": "semantic_field"
      },
      "semantic_field": {
        "type": "semantic_text",
        "inference_id": "medicines-inference"
      },
      "text_description": {
        "type": "text",
        "copy_to": "semantic_field"
      }
    }
  }
}<p>O campo <code>text_description</code> armazenará o texto simples das descrições, enquanto <code>semantic_field</code>, que é um tipo de campo <a href="https://www.elastic.co/pt/guide/en/elasticsearch/reference/current/semantic-text.html">semantic_text</a> , armazenará os embeddings gerados pelo ELSER.</p><p>A propriedade <a href="https://www.elastic.co/pt/guide/en/elasticsearch/reference/current/copy-to.html">copy_to</a> copiará o conteúdo dos campos name e <code>text_description</code> para o campo semântico para que os embeddings para esses campos sejam gerados.</p><h2>Indexação de dados</h2><p>Agora, vamos indexar os dados usando a <a href="https://www.elastic.co/pt/guide/en/elasticsearch/reference/current/docs-bulk.html">API _bulk</a>.</p>POST _bulk
{"index":{"_index":"medicines"}}
{"id":1,"name":"Paracetamol","text_description":"An analgesic and antipyretic that does NOT require a prescription."}
{"index":{"_index":"medicines"}}
{"id":2,"name":"Ibuprofen","text_description":"A nonsteroidal anti-inflammatory drug (NSAID) available WITHOUT a prescription."}
{"index":{"_index":"medicines"}}
{"id":3,"name":"Amoxicillin","text_description":"An antibiotic that requires a prescription."}
{"index":{"_index":"medicines"}}
{"id":4,"name":"Lorazepam","text_description":"An anxiolytic medication that strictly requires a prescription."}
{"index":{"_index":"medicines"}}
{"id":5,"name":"Omeprazole","text_description":"A medication for stomach acidity that does NOT require a prescription."}
{"index":{"_index":"medicines"}}
{"id":6,"name":"Insulin","text_description":"A hormone used in diabetes treatment that requires a prescription."}
{"index":{"_index":"medicines"}}
{"id":7,"name":"Cold Medicine","text_description":"A compound formula to relieve flu symptoms available WITHOUT a prescription."}
{"index":{"_index":"medicines"}}
{"id":8,"name":"Clonazepam","text_description":"An antiepileptic medication that requires a prescription."}
{"index":{"_index":"medicines"}}
{"id":9,"name":"Vitamin C","text_description":"A dietary supplement that does NOT require a prescription."}
{"index":{"_index":"medicines"}}
{"id":10,"name":"Metformin","text_description":"A medication used for type 2 diabetes that requires a prescription."}<p>Resposta.</p>{
   "errors": false,
   "took": 34732020848,
   "items": [
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "mYoeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 0,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "mooeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 1,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "m4oeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 2,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "nIoeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 3,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "nYoeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 4,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "nooeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 5,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "n4oeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 6,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "oIoeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 7,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "oYoeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 8,
     	"_primary_term": 1,
     	"status": 201
   	}
 	},
 	{
   	"index": {
     	"_index": "medicines",
     	"_id": "oooeMpQBF7lnCNFTfdn2",
     	"_version": 1,
     	"result": "created",
     	"_shards": {
       	"total": 2,
       	"successful": 2,
       	"failed": 0
     	},
     	"_seq_no": 9,
     	"_primary_term": 1,
     	"status": 201
   	}
 	}
   ]
 }<h2>Fazendo perguntas usando o Playground</h2><p><a href="https://www.elastic.co/pt/guide/en/kibana/current/playground.html">Playground</a> é uma ferramenta do Kibana que permite criar rapidamente um sistema RAG usando índices do Elasticsearch e um provedor LLM. Você pode ler este <a href="https://www.elastic.co/pt/search-labs/blog/playground-connectors-data-chat">artigo</a> para saber mais sobre isso.</p><h3>Conectando o LLM local ao Playground</h3><p>Primeiro, precisamos criar um conector que use a URL pública que acabamos de criar. No Kibana, vá em <strong>Pesquisar&gt;Playground</strong> e depois clique em "Conectar a um LLM".</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt22148eabfabf6d3f/6a17f3963e9e459f97ba15c6/1854f0808f8150e359fe62ba5d901d32a88d477c-1600x867.png" alt="Conectando o LLM local ao Playground para Ollama" /><p>Esta ação revelará um menu no lado esquerdo da interface do Kibana. Lá, clique em "OpenAI".</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6e28194d9012f141/6a17f39725daab500a08a36b/c83d3c4d7035a518124ad7d22b38764db57b6800-933x1007.png" alt="Selecione um conector: Open AI Ollama" /><p>Agora podemos começar a configurar o conector OpenAI.</p><p>Vá para "Configurações do conector" e, para o provedor OpenAI, selecione "Outro (Serviço compatível com OpenAI)":</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9984dce6f78a7c08/6a17f3990b0bed0b7add36c4/ecfcdc4b575c309bd55b4e61ca0ddb348aa84f64-917x268.png" alt="Definir a configuração do conector para usar o Ollama com a API de Inferência" /><p>Agora, vamos configurar os outros campos. Para este exemplo, vamos nomear nosso modelo como "medicines-llm". No campo URL, use o gerado pelo ngrok (/v1/chat/completions). No campo "Modelo padrão", selecione "llama3.2". Não usaremos uma chave de API, então digite qualquer texto aleatório para prosseguir:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8a24b93a39d380fb/6a17f39b96142a15c7eb1c3c/5d3b5027c8096cbe49fb740d70aa24e849611a9d-916x688.png" alt="Adicionar configurações" /><p>Clique em "Salvar" e adicione os medicamentos de índice clicando em "Adicionar fontes de dados":</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4f107a54d5be25f9/6a17f39d4b055deb9d432338/525113da59e902c8235f62bde8fb62371a63e11b-1579x753.png" alt="Adicione fontes de dados para fazer perguntas aos seus documentos usando o Playground" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt03fb36fe05dcdbf5/6a17f39ebe608602f40048be/96138de0bbe2c2ac619f64889d3487df62739ca4-466x805.png" alt="Adicionar dados de consulta" /><p>Ótimo! Agora temos acesso ao Playground usando o LLM que estamos executando localmente como mecanismo RAG.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb1b27580107259b3/6a17f3a096142abefceb1c40/cfb48b33c70f4534ab77eb01f58008237f65e6f4-1600x851.png" alt="Selecione as configurações do modelo no Playground" /><p>Antes de testar, vamos adicionar instruções mais específicas ao agente e aumentar o número de documentos enviados ao modelo para 10, para que a resposta tenha o maior número possível de documentos disponíveis. O campo de contexto será <code>semantic_field</code>, que inclui o nome e a descrição dos medicamentos, graças à propriedade copy_to.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4fbc97dc87c6fc62/6a17f3a1e8fbce052c3a1aa0/0c57c9c0e1a0e7b58fffdd3ef81d67d41e2990c4-580x806.png" alt="Configurações Moel no Elastic Playground" /><p>Agora vamos fazer a pergunta: <em><strong>Posso comprar Clonazepam sem receita?</strong></em> e veja o que acontece:</p><p>Como esperado, obtivemos a resposta correta.</p><h3>Próximas etapas</h3><p>O próximo passo é criar seu próprio aplicativo! O Playground fornece um script de código em Python que você pode executar em sua máquina e personalizá-lo para atender às suas necessidades. Por exemplo, colocando-o atrás de um servidor <a href="https://fastapi.tiangolo.com/">FastAPI</a> para criar um chatbot de medicamentos de controle de qualidade consumido pela sua interface de usuário.</p><p>Você pode encontrar esse código clicando no botão <em><strong>Exibir código</strong></em> na seção superior direita do Playground:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt42fe193b8aa08830/6a17f3a33e9e4569e8ba15ca/816bfd0e5f936ad65dbe719d5df10714e550a40b-380x121.png" alt="Botão Ver código" /><p>E você usa os <em><strong>Endpoints e as chaves de API</strong></em> para gerar a variável de ambiente <code>ES_API_KEY</code> necessária no código.</p><p>Para este exemplo específico, o código é o seguinte:</p>## Install the required packages
## pip install -qU elasticsearch openai
import os
from elasticsearch import Elasticsearch
from openai import OpenAI
es_client = Elasticsearch(
    "https://your-deployment.us-central1.gcp.cloud.es.io:443",
    api_key=os.environ["ES_API_KEY"]
)
openai_client = OpenAI(
    api_key=os.environ["OPENAI_API_KEY"],
)
index_source_fields = {
    "medicines": [
        "semantic_field"
    ]
}
def get_elasticsearch_results():
    es_query = {
        "retriever": {
            "standard": {
                "query": {
                    "nested": {
                        "path": "semantic_field.inference.chunks",
                        "query": {
                            "sparse_vector": {
                                "inference_id": "medicines-inference",
                                "field": "semantic_field.inference.chunks.embeddings",
                                "query": query
                            }
                        },
                        "inner_hits": {
                            "size": 2,
                            "name": "medicines.semantic_field",
                            "_source": [
                                "semantic_field.inference.chunks.text"
                            ]
                        }
                    }
                }
            }
        },
        "size": 3
    }
    result = es_client.search(index="medicines", body=es_query)
    return result["hits"]["hits"]
def create_openai_prompt(results):
    context = ""
    for hit in results:
        inner_hit_path = f"{hit['_index']}.{index_source_fields.get(hit['_index'])[0]}"
        ## For semantic_text matches, we need to extract the text from the inner_hits
        if 'inner_hits' in hit and inner_hit_path in hit['inner_hits']:
            context += '\n --- \n'.join(inner_hit['_source']['text'] for inner_hit in hit['inner_hits'][inner_hit_path]['hits']['hits'])
        else:
            source_field = index_source_fields.get(hit["_index"])[0]
            hit_context = hit["_source"][source_field]
            context += f"{hit_context}\n"
    prompt = f"""
  Instructions:
  - You are an assistant specializing in answering questions about the sale of medicines.
  - Answer questions truthfully and factually using only the context presented.
  - If you don't know the answer, just say that you don't know, don't make up an answer.
  - You must always cite the document where the answer was extracted using inline academic citation style [], using the position.
  - Use markdown format for code examples.
  - You are correct, factual, precise, and reliable.
  Context:
  {context}
  """
    return prompt
def generate_openai_completion(user_prompt, question):
    response = openai_client.chat.completions.create(
        model="gpt-3.5-turbo",
        messages=[
            {"role": "system", "content": user_prompt},
            {"role": "user", "content": question},
        ]
    )
    return response.choices[0].message.content
if __name__ == "__main__":
    question = "my question"
    elasticsearch_results = get_elasticsearch_results()
    context_prompt = create_openai_prompt(elasticsearch_results)
    openai_completion = generate_openai_completion(context_prompt, question)
    print(openai_completion)<p>Para que funcione com o Ollama, você precisa alterar o cliente OpenAI para se conectar ao servidor Ollama em vez do servidor OpenAI. Você pode encontrar a lista completa de exemplos do OpenAI e endpoints compatíveis aqui.</p>openai_client = OpenAI(
    # you can use http://localhost:11434/v1/ if running this code locally.
    base_url='https://your-ngrok-url.ngrok-free.app/v1/',
    # required but ignored
    api_key='ollama',
)<p>E também altere o modelo para llama3.2 ao chamar o método de conclusão:</p>def generate_openai_completion(user_prompt, question):
    response = openai_client.chat.completions.create(
        model="llama3.2",
        messages=[
            {"role": "system", "content": user_prompt},
            {"role": "user", "content": question},
        ]
    )
    return response.choices[0].message.content<p>Vamos adicionar nossa pergunta: <em><strong>Posso comprar Clonazepam sem receita? </strong></em>Para a consulta do Elasticsearch:</p>def get_elasticsearch_results():
    es_query = {
        "retriever": {
            "standard": {
                "query": {
                    "nested": {
                        "path": "semantic_field.inference.chunks",
                        "query": {
                            "sparse_vector": {
                                "inference_id": "medicines-inference",
                                "field": "semantic_field.inference.chunks.embeddings",
                                "query": "Can I buy Clonazepam without a prescription?"
                            }
                        },
                        "inner_hits": {
                            "size": 2,
                            "name": "medicines.semantic_field",
                            "_source": [
                                "semantic_field.inference.chunks.text"
                            ]
                        }
                    }
                }
            }
        },
        "size": 3
    }
    result = es_client.search(index="medicines", body=es_query)
    return result["hits"]["hits"]<p>E também para a chamada de conclusão com algumas impressões, para que possamos confirmar que estamos enviando os resultados do Elasticsearch como parte do contexto da pergunta:</p>if __name__ == "__main__":
    question = "Can I buy Clonazepam without a prescription?"
    elasticsearch_results = get_elasticsearch_results()
    context_prompt = create_openai_prompt(elasticsearch_results)
    print("========== Context Prompt START ==========")
    print(context_prompt)
    print("========== Context Prompt END ==========")
    print("========== Ollama Completion START ==========")
    openai_completion = generate_openai_completion(context_prompt, question)
    print(openai_completion)
    print("========== Ollama Completion END ==========")<p>Agora vamos executar o comando</p><p><code>pip install -qU elasticsearch openai</code></p><p><code>python main.py</code></p><p>Você deverá ver algo assim:</p>========== Context Prompt START ==========
  Instructions:
  - You are an assistant specializing in answering questions about the sale of medicines.
  - Answer questions truthfully and factually using only the context presented.
  - If you don't know the answer, just say that you don't know, don't make up an answer.
  - You must always cite the document where the answer was extracted using inline academic citation style [], using the position.
  - Use markdown format for code examples.
  - You are correct, factual, precise, and reliable.
  Context:
  Clonazepam
 ---
An antiepileptic medication that requires a prescription.A nonsteroidal anti-inflammatory drug (NSAID) available WITHOUT a prescription.
 ---
IbuprofenAn anxiolytic medication that strictly requires a prescription.
 ---
Lorazepam


========== Context Prompt END ==========
========== Ollama Completion START ==========
No, you cannot buy Clonazepam over-the-counter (OTC) without a prescription [1]. It is classified as a controlled substance in the United States due to its potential for dependence and abuse. Therefore, it can only be obtained from a licensed healthcare provider who will issue a prescription for this medication.
========== Ollama Completion END ==========<h2>Conclusão</h2><p>Neste artigo, podemos ver o poder e a versatilidade de ferramentas como o Ollama quando as usamos em conjunto com a API de inferência do Elasticsearch e o Playground.</p><p>Após alguns passos simples, tínhamos um aplicativo RAG funcional com um chat que usava um LLM em execução em nossa própria infraestrutura a custo zero. Isso também nos permite ter mais controle sobre recursos e informações confidenciais, além de nos dar acesso a uma variedade de modelos para diferentes tarefas.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/ollama-with-inference-api</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/ollama-with-inference-api</guid>
    <category><![CDATA[AI]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd9c8eb0fc946920e/6a17f3a46864a4b2fbb688f0/399b9ef527be633845fb6505b68132cc03bc9e09-1150x628.png" length="0" type="image/png"/>
    <pubDate>Fri, 14 Feb 2025 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>