<?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[Experiencia del desarrollador - 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[Experiencia del desarrollador - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/es/search-labs/blog/category/developer-experience</link>
    </image>
    <link>https://www.elastic.co/es/search-labs/blog/category/developer-experience</link>
    <atom:link href="https://www.elastic.co/es/search-labs/rss/category/developer-experience.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[es]]></language>
    <lastBuildDate>Tue, 29 Sep 2026 14:28:22 GMT</lastBuildDate>
  <item>
    <title><![CDATA[API de paneles de Kibana: un contrato estable para cada tipo de panel, probado por más de 50 equipos antes de GA]]></title>
    <description><![CDATA[Administra los dashboards de Kibana como código: haz commit con Git, promueve en todos los entornos y automatiza los despliegues con la API de Kibana y Terraform.]]></description>
    <content:encoded><![CDATA[<p>Las<a href="https://dashboardsapispec.kibana.dev/dashboards#tag/Dashboards"> API de dashboards y visualizaciones de Kibana</a> están listas para producción en Elastic 9.5, disponibles en todos los niveles de suscripción, con total compatibilidad con versiones anteriores. Define tus paneles como JSON, haz commit en Git y luego despliega en diferentes entornos usando pipelines de integración continua y despliegue continuo (CI/CD),<a href="https://registry.terraform.io/providers/elastic/elasticstack/latest/docs/resources/kibana_dashboard"> Terraform</a> o cualquier herramienta que ya tengas. Más de 50 equipos probaron la API durante<a href="https://www.elastic.co/search-labs/blog/kibana-dashboards-as-code-terraform-api"> la vista previa técnica en la versión 9.4</a>, algunos ya ejecutándola en producción. La versión 9.5 también agrega nuevos endpoints (en vista previa técnica) para<a href="https://dashboardsapispec.kibana.dev/tags.html"> las etiquetas</a>, con endpoints <a href="https://dashboardsapispec.kibana.dev/markdowns.html"> de paneles Markdown</a> y<a href="https://dashboardsapispec.kibana.dev/links.html#tag/Links"> Enlaces</a> disponibles ahora en Elastic Cloud Serverless y que aterrizan en la 9.6.</p><h2>Qué significa la compatibilidad con versiones anteriores para la API de Dashboards de Kibana</h2><p>Durante la vista previa técnica, la forma de la API podría cambiar entre versiones.[1] Eso ya no es así. Disponibilidad general (GA) significa:</p><ul><li><p><strong>Compatibilidad total con versiones anteriores.</strong> Con el tiempo, se agregarán nuevos campos y tipos de panel, pero los campos y el comportamiento actuales se mantendrán sin cambios. Cualquier cambio futuro que rompa la compatibilidad será considerado con mucho cuidado y solo se introducirá en una nueva versión principal del stack.</p></li><li><p><strong>Listo para producción con soporte completo.</strong> La API ofrece las garantías completas de soporte de Elastic. Puedes usarla de manera segura en entornos de producción para despliegues automatizados, promoción del entorno y administración programática del dashboard.</p></li></ul><h2>Nuevos endpoints de la API Kibana para los paneles de etiquetas, markdown y enlaces</h2><p>Elastic 9.5 también introduce un nuevo endpoint independiente para <a href="https://dashboardsapispec.kibana.dev/tags.html"><strong>etiquetas</strong></a>, que permite categorizar y filtrar paneles. Ahora puedes administrarlos mediante programación a través de endpoints CRUD dedicados, lo que facilita organizar paneles a gran escala en todos los entornos.	</p><p>Los nuevos endpoints de paneles <a href="https://dashboardsapispec.kibana.dev/markdowns.html"><strong>Markdown</strong></a> y <a href="https://dashboardsapispec.kibana.dev/links.html#tag/Links"><strong>Enlaces</strong></a> ya están disponibles en Serverless y llegarán en la próxima versión de la pila (9.6).</p><h2>¿Qué tipos de paneles soporta la API de Kibana Dashboards?</h2><p>La API de dashboards admite todos los <em>paneles por valor</em> en la versión 9.5 (los definidos directamente en un dashboard, a diferencia de los paneles de biblioteca guardados para su reutilización). Cada tipo de panel admitido tiene un esquema tipificado y validado.</p><p><strong>Tipo de panel</strong></p><p><strong>Estado</strong></p><p>Gráficos XY</p><p>Con soporte</p><p>Métricas</p><p>Con soporte</p><p>Circular</p><p>Con soporte</p><p>Calibre</p><p>Con soporte</p><p>Mapa de calor</p><p>Con soporte</p><p>Tablas de datos</p><p>Con soporte</p><p>Mapa de árbol</p><p>Con soporte</p><p>Sesiones de Discover</p><p>Con soporte</p><p>Controles</p><p>Con soporte</p><p>Markdown</p><p>Con soporte</p><p>Enlaces</p><p>Con soporte</p><p>Paneles de ML</p><p>Con soporte</p><p>Paneles de Observability</p><p>Con soporte</p><p>Mapas</p><p>Próximamente</p><p>Vega</p><p>Próximamente</p><h2>Cómo gestionar los dashboards de Kibana como código</h2><p>La API de dashboards permite un flujo de trabajo completo de dashboards como código: exportar un dashboard como JSON limpio y diferenciable, hacer commit en Git como fuente de verdad, revisar los cambios en pull requests, y desplegar la misma definición en desarrollo, staging y producción. Una vez que un dashboard se administra como código, trata a Git como la única fuente de verdad: los cambios efectuados directamente en la UI se sobrescriben la próxima vez que despliegues.</p><p>El principal desafío al mover un dashboard entre espacios, clústeres o etapas es que los dashboards hacen referencia a objetos como Data view y visualizaciones de biblioteca mediante un ID. Debido a que estos ID se generan automáticamente y difieren de un entorno a otro, un dashboard exportado desde un entorno puede apuntar a objetos que no existen en otro. Hay tres formas de manejarlo, enumeradas aquí de la más a la menos automatizada:</p><ul><li><p><strong>Usa Terraform.</strong> El <a href="https://registry.terraform.io/providers/elastic/elasticstack/latest/docs/resources/kibana_dashboard">proveedor Elastic Stack Terraform</a> rastrea cada recurso y mapea automáticamente los ID por entorno, por lo que las referencias se mantienen estables mientras promocionas un dashboard desde el desarrollo hasta la producción.</p></li><li><p><strong>Definir por valor </strong><a href="https://www.elastic.co/docs/explore-analyze/visualize/esorql"><strong>Paneles de lenguaje de búsqueda de Elasticsearch (ES|QL)</strong></a><strong>.</strong> La forma más portátil de construir un panel es definir su visualización con ES|QL directamente en el dashboard. Una consulta <a href="https://www.elastic.co/docs/explore-analyze/query-filter/languages/esql-kibana">ES|QL</a> lee los índices que se especifican en ella, por lo que el panel no contiene referencias externas a vistas de datos ni a objetos de biblioteca. El resultado es un dashboard totalmente autónomo y portátil.</p></li><li><p><strong>Asigna ID coincidentes.</strong> Si haces referencia a objetos guardados, como Data view o visualizaciones de biblioteca, créalos con un ID elegido usando PUT (upsert) en lugar de POST (que genera automáticamente un ID). Emplea ID legibles por humanos, como logs-prod, para que sean fáciles de reutilizar y reconocer en diferentes entornos.</p></li></ul><p>Para un recorrido detallado de estos patrones de portabilidad y del flujo de trabajo completo de dashboards como código, consulta la <a href="https://www.elastic.co/docs/explore-analyze/dashboards/manage-dashboards-as-code#dashboards-as-code-portability">documentación de Gestionar dashboards como código</a>.</p><h3>Crea un dashboard de Kibana con la API de dashboards usando PUT</h3><p>Aquí hay un ejemplo rápido de crear un dashboard con un panel métrico usando PUT en lugar de POST para asignar un ID personalizado usando el nombre del dashboard (service-health-overview). La misma lógica funciona para crear visualizaciones independientes guardadas en la biblioteca.</p>PUT kbn:/api/dashboards/service-health-overview
{
  "title": "Service health overview",
  "description": "Key service metrics — managed via API",
  "tags": [
    "production",
    "sre-team"
  ],
  "panels": [
    {
      "type": "vis",
      "grid": {
        "x": 0,
        "y": 0,
        "w": 12,
        "h": 8
      },
      "config": {
        "title": "Error rate (5xx)",
        "type": "metric",
        "data_source": {
          "type": "esql",
          "query": "FROM logs-* | WHERE http.response.status_code &gt;= 500 | STATS error_rate=count(*) BY host.name"
        },
        "metrics": [
          {
            "type": "primary",
            "column": "count"
          }
        ]
      }
    }
  ]
}<h2>Roadmap de la API de paneles de Kibana: Maps, Vega y endpoints independientes</h2><p>Estamos ampliando activamente el alcance de la API. El siguiente paso es agregar compatibilidad con mapas y paneles Vega, incluyendo esquemas tipados para ellos. También estamos creando endpoints CRUD independientes para las sesiones de Discover (más allá de su compatibilidad actual como paneles del dashboard), Vega, Maps y Anotaciones, desacoplados del ciclo de vida del dashboard.</p><p>Para las definiciones completas de esquemas, visita la <a href="https://dashboardsapispec.kibana.dev/dashboards#tag/Dashboards">documentación de la API de Dashboards</a>. Para los usuarios de Terraform, el <a href="https://registry.terraform.io/providers/elastic/elasticstack/latest/docs/resources/kibana_dashboard">proveedor Terraform de Elastic Stack</a> es compatible con la API GA Dashboards.</p><h2>Nota</h2><ol><li><p>Los endpoints de núcleo no han cambiado desde la vista previa técnica. Si construiste integraciones contra 9.4, funcionan en 9.5. Los únicos cambios incompatibles son dos menores que afectan al listado del dashboard y a los formatos de unidades de duración, documentados <a href="https://www.elastic.co/docs/release-notes/kibana/breaking-changes">aquí</a>.</p></li></ol>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/dashboards-as-code-kibana-api</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/dashboards-as-code-kibana-api</guid>
    <category><![CDATA[Kibana]]></category>
    <category><![CDATA[Experiencia del desarrollador]]></category>
    <category><![CDATA[Integraciones]]></category>
    <dc:creator><![CDATA[Teresa Alvarez Soler]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8ed7e33de291f255/6a730619c8b7ac02b251f9d3/image1.png" length="0" type="image/png"/>
    <pubDate>Wed, 05 Aug 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Introducción de claves API unificadas para Elastic Cloud Serverless y Elasticsearch]]></title>
    <description><![CDATA[Aprende cómo Elastic unificó la autenticación del plano de control y del plano de datos en Serverless con una arquitectura de IAM distribuida globalmente. Usa una sola clave de API para las API de Cloud y de Elasticsearch.]]></description>
    <content:encoded><![CDATA[<p>Imagina que eres un ingeniero de confiabilidad de sitios (SRE, por sus siglas en inglés) responsable de una creciente cartera de proyectos Elastic Cloud Serverless: Elastic Observability para tu infraestructura de producción, Elastic Security para tu equipo del centro de operaciones de seguridad (SOC, por sus siglas en inglés) y Elasticsearch para tu aplicación orientada al cliente. Cada proyecto tiene su propia clave de API de Elasticsearch. Tu pipeline de integración continua y entrega continua (CI/CD) necesita una clave de API de Cloud independiente para aprovisionar y administrar esos proyectos. El día de rotación llega cada trimestre: recorres cada proyecto, acuñas nuevas claves, actualizas tu estado de Terraform, redistribuyes tus pipelines y esperas que nada se te escape. Cuando un incidente ocurre a las 2 a. m. y necesitas revocar el acceso rápidamente, estás consultando una hoja de cálculo de credenciales para averiguar qué clave pertenece a qué proyecto y qué servicio.</p><p>Hoy, esa historia se vuelve mucho más sencilla. Ahora puedes usar <strong>las claves de API de Elastic Cloud</strong> para autenticarte directamente contra las API de <strong>Elasticsearch</strong> y <strong>Kibana</strong> en <strong>Elastic Cloud Serverless</strong>. Ahora puedes usar una sola credencial para gestionar los recursos de tu organización <em>y</em> ejecutar operaciones de datos, como consultas de ES|QL (lenguaje de búsqueda de Elasticsearch), ingesta de datos y generación de alertas.</p><p>Veamos por qué lo creamos, cómo diseñamos una capa de identidad distribuida en todo el mundo para hacerlo posible y cómo sienta las bases para la búsqueda entre proyectos.</p><h2>La carga secreta de la gestión</h2><p>Construir pipelines fiables de CI/CD, flujos de trabajo de GitOps o automatización de Terraform alrededor de plataformas de datos conlleva un costo oculto: la proliferación de secretos.</p><p>En el modelo anterior, los desarrolladores se enfrentaban a un proceso de autenticación fragmentado:</p><ul><li><p><strong>Plano de control (claves de la API de Elastic Cloud):</strong> Claves con ámbito de organización que se emplean para crear proyectos, invitar a usuarios y gestionar la facturación a través de la <a href="https://www.elastic.co/docs/api/doc/cloud/">API de Elastic Cloud</a>.</p></li><li><p><strong>Plano de datos (claves de la API de Elasticsearch):</strong> Claves con alcance de proyecto <em>creadas dentro</em> de un proyecto Serverless específico para interactuar con las API <a href="https://www.elastic.co/docs/api/doc/elasticsearch-serverless/">de Elasticsearch</a> y <a href="https://www.elastic.co/docs/api/doc/serverless">Kibana</a>.</p></li></ul><p>Esto significaba que tu script de despliegue tenía que autenticarse en Elastic Cloud, aprovisionar un proyecto Serverless, extraer una clave de API de Elasticsearch recién creada de ese proyecto específico y luego inyectar <em>esa</em> segunda clave en la aplicación o herramienta de automatización downstream, lo que resultaba en pipelines complejos, logs de auditoría fragmentados y un mayor riesgo de fugas de credenciales.</p><h2>Autenticación unificada en Elastic Cloud Serverless</h2><p>Con este lanzamiento, la división ha desaparecido para los proyectos Serverless. Ahora puedes crear una clave de la API de Elastic Cloud que esté explícitamente autorizada para <strong>las API de Cloud, Elasticsearch y Kibana</strong>.</p><ul><li><p><strong>Antes:</strong> Una clave de API de Elastic Cloud era estrictamente un token del plano de control. Podía crear proyectos, gestionar la facturación e invitar a usuarios, pero tenía una limitación importante: no se podía usar para llamar a las API de Elasticsearch o Kibana dentro de esos proyectos. Siempre necesitabas una segunda clave específica del proyecto para las operaciones con datos.</p></li><li><p><strong>Ahora:</strong> Al optar por el acceso a <strong>la API de Cloud, Elasticsearch y Kibana</strong> cuando creas una clave de la API de Elastic Cloud, se elimina el límite rígido para Serverless. Esa clave de la API se convierte en una credencial verdaderamente unificada. Mantiene su capacidad para gestionar la infraestructura de tu organización, mientras obtiene acceso nativo para consultar, ingerir y analizar datos en cualquier proyecto Serverless autorizado.</p></li></ul><p>Al unificarlo bajo una única clave de API de Elastic Cloud, obtienes una única identidad que puede ser delimitada, sometida a auditoría, rotada y revocada como una sola unidad. Cada llamada a la API, ya sea que aprovisione un nuevo proyecto o ejecute un ES|QL, aparece bajo la misma credencial en tus logs de auditoría, dándote un único rastro a seguir durante investigaciones de incidentes o revisiones de cumplimiento. La rotación de credenciales se convierte en una operación de un solo paso en lugar de una actualización coordinada a través de secretos separados del plano de control y del plano de datos. Y dado que las asignaciones de roles son por proyecto, una única clave puede abarcar varios proyectos, gestionando la ingesta en tu proyecto de observabilidad y ejecutando consultas en tu proyecto de seguridad, sin tener que gestionar credenciales separadas para cada uno.</p><p>Es importante destacar que <em>unificado</em> no significa <em>todopoderoso</em>. Al usar la carga útil <code>role_assignments</code>, puedes asignar una clave unificada estrictamente a un solo proyecto y a un rol específico (como solo lectura), cerciorando que el radio de alcance permanezca completamente contenido si alguna vez se expone una credencial. Si un desarrollador se marcha o una aplicación es desactivada, puedes revocar una sola clave de la Consola de Elastic Cloud, terminando inmediatamente el acceso tanto en el plano de control como en todos los proyectos asociados de Elasticsearch.</p><p><em>(Nota: Para despliegues gestionados/alojados en Elastic Cloud Hosted, las claves de API de la cloud siguen gestionando solo el plano de control. La función para extenderlo a las API de pila alojadas está previsto para una versión futura).</em></p><h2>Automatiza tus flujos de trabajo</h2><p>Los primeros pasos son simples. Puedes configurarlo completamente a través de la consola de Elastic Cloud o automatizarlo empleando la <a href="https://www.elastic.co/docs/deploy-manage/api-keys/elastic-cloud-api-keys">API de Elastic Cloud</a>.</p><p>El proceso de la UI sigue igual, pero ahora puedes seleccionar <strong>acceso a la API de Cloud, Elasticsearch y Kibana</strong> bajo la asignación de rol del proyecto.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltda0a18945295aa84/6a1707bd509168fab4e1ba19/c4f802f130655290cd474b283001a954d14c3088-2801x1681.png" alt="Pantalla de Elastic Cloud mostrando la página de claves de API con un modal de Crear clave de API abierto, incluyendo campos para nombre, vencimiento y asignación de roles." /><p>Aquí te mostramos cómo crear una clave unificada mediante programación empleando la API de Elastic Cloud. Fíjate en el array <code>application_roles</code>, ya que es el que otorga acceso nativo al plano de datos de Elasticsearch:</p>curl -X POST \
  -H "Content-Type: application/json" \
  -H "Authorization: ApiKey $EC_API_KEY" \
  "https://api.elastic-cloud.com/api/v1/users/auth/keys" \
  -d '{
    "description": "unified-automation-key",
    "expiration": "90d",
    "role_assignments": {
      "project": {
        "elasticsearch": [
          {
            "role_id": "elasticsearch-admin",
            "organization_id": "YOUR_ORG_ID",
            "all": false,
            "project_ids": ["YOUR_PROJECT_ID"],
            "application_roles": ["admin"]
          }
        ]
      }
    }
  }'<p>Una vez creada, simplemente pasas esta misma clave en el encabezado <code>Authorization: ApiKey</code> tanto a <code>api.elastic-cloud.com</code> como a tus puntos finales específicos de Serverless Elasticsearch.</p><h2>Bajo el capó: construyendo una capa de identidad distribuida</h2><p>Hacer que una clave API de Cloud funcione tanto en el plano de control como en el plano de datos no es tan simple como pasar un token. Implica resolver un reto fundamental de los sistemas distribuidos.</p><p>Históricamente, las claves de API de Cloud residían en un clúster de seguridad global centralizado. Esto funciona bien para las operaciones del plano de control donde una latencia más alta es aceptable. Sin embargo, las solicitudes de datos de Elasticsearch requieren una latencia ultrabaja. No podemos permitirnos un viaje de ida y vuelta alrededor del mundo hasta un plano de control central para validar cada consulta de búsqueda o solicitud de ingesta.</p><p>Para resolverlo, implementamos una nueva arquitectura de autenticación respaldada por un almacén de datos distribuido a nivel mundial. El siguiente diagrama de secuencia muestra a un cliente enviando una consulta a Elasticsearch con una clave de API de Elastic Cloud, lo que ilustra cómo la autenticación se lleva a cabo íntegramente dentro de la región local, sin necesidad de un viaje de ida y vuelta al plano de control global. Elasticsearch delega la autenticación al servicio regional de IAM, que valida la clave y comprueba las asignaciones de roles en una réplica local de la base de datos distribuida en todo el mundo. Una vez autorizado, Elasticsearch ejecuta la consulta y devuelve los resultados al cliente.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf4fa84c3f33f88f7/6a1707baacf088989abe9a8e/3e38d7a862b9981523c5393c441b92eae13aeb90-2401x1351.webp" alt="Diagrama de secuencia que muestra una solicitud de un cliente con una clave de API de Cloud que pasa por Elasticsearch Serverless, un servicio regional de IAM y una réplica de base de datos distribuida antes de devolver los resultados." /><h3>Persistencia distribuida a nivel global</h3><p>En lugar de depender únicamente de un clúster de seguridad centralizado, las claves de la API de Elastic Cloud y las definiciones de roles asociadas ahora se almacenan permanentemente en una base de datos distribuida en todo el mundo y de alta disponibilidad. Esta base de datos sincroniza los datos de administración de identidad y acceso (IAM) a través del plano de control global y los planos de datos regionales donde realmente se ejecutan tus proyectos sin servidor.</p><h3>Validación local con IAM regional</h3><p>Cuando tu cliente envía una solicitud a Elasticsearch usando una clave de API de Elastic Cloud, la solicitud no se remite al plano de control mundial. En su lugar, es redirigido al nuevo servicio regional de IAM. Valida la clave contra la réplica de la base de datos local, asegurando que la autenticación ocurra con una latencia cercana a cero y esté completamente aislada de las interrupciones del plano de control mundial.</p><h3>Mapping dinámico de roles</h3><p>La autenticación es solo la mitad del camino; el sistema también tiene que autorizar la solicitud. El servicio regional de IAM traduce instantáneamente tus asignaciones de rol a nivel de cloud (por ejemplo, <code>application_roles</code>) en privilegios nativos de Elasticsearch. Elasticsearch puede entonces autorizar y ejecutar la solicitud localmente, sin necesidad de un índice <code>.security</code> local.</p><h2>La base para la búsqueda entre proyectos</h2><p>Esta arquitectura de identidad distribuida es un pilar fundamental para el futuro de la plataforma Elastic.</p><p>Como la identidad y el acceso ahora están unificados y sincronizados globalmente, contamos con el marco de trabajo necesario para transferir tu identidad de forma segura entre diferentes proyectos. Esto permite las próximas funcionalidades de <strong>búsqueda entre proyectos (CPS)</strong> para Serverless.</p><p>Con CPS, podrás consultar datos que abarcan varios proyectos serverless remotos, como combinar cargas de trabajo de seguridad y observabilidad, tan fácilmente como si fueran un solo set de datos. Al utilizar claves API unificadas, el sistema puede evaluar automáticamente tus permisos en todos los proyectos a la vez, sin que tengas que configurar relaciones de confianza complejas, certificados ni duplicar credenciales en cada proyecto de destino.</p><h2>Más información</h2><p>¿Listo para simplificar tu stack?</p><ul><li><p>Lee la <a href="https://www.elastic.co/docs/deploy-manage/api-keys/elastic-cloud-api-keys">documentación sobre las claves de la API de Elastic Cloud</a> para saber cómo asignar acceso a la pila.</p></li><li><p>Consulta la referencia <a href="https://www.elastic.co/docs/api/doc/cloud/operation/operation-create-api-key">Crear clave de la API (Elastic Cloud API)</a> para automatizar la generación de claves.</p></li><li><p>Consulta <a href="https://www.elastic.co/docs/deploy-manage/api-keys">las claves de API de Elastic</a> para ver una comparación completa de los tipos de claves disponibles en toda la Platform de Elastic.</p></li></ul><p>Empieza o continúa construyendo en <a href="https://cloud.elastic.co/registration">Elastic Cloud</a> hoy mismo.</p><h2>Descargo de responsabilidad</h2><p>El lanzamiento y el momento de cualquier característica o funcionalidad descrita en esta publicación quedan a exclusivo criterio de Elastic. Es posible que alguna característica o funcionalidad que no esté disponible en este momento no se lance a tiempo o no se lance en absoluto.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elastic-cloud-api-keys-unified-serverless</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elastic-cloud-api-keys-unified-serverless</guid>
    <category><![CDATA[Elastic Cloud Serverless]]></category>
    <category><![CDATA[Experiencia del desarrollador]]></category>
    <dc:creator><![CDATA[ Alex Chalkias]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt16ca1a6af7e5bab8/6a1707b7a6c2b900abe7965b/864e229f00eb2018084f13dd7f0e390e18383ed4-1980x1188.png" length="0" type="image/png"/>
    <pubDate>Mon, 20 Apr 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Monitoreo de las vistas del dashboard de Kibana con flujos de trabajo de Elastic]]></title>
    <description><![CDATA[Conoce cómo usar flujos de trabajo de Elastic para recopilar métricas de vista del dashboard de Kibana cada 30 minutos e indexarlas en Elasticsearch, para que puedas crear análisis y vistas personalizadas sobre tus propios datos.]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/kibana">Kibana</a> registra cuántas veces se visualiza cada dashboard, pero esos datos no se muestran de forma nativa en ningún dashboard integrado. En este artículo, usaremos los <strong>flujos de trabajo de Elastic</strong> para recopilar automáticamente esos datos cada 30 minutos e indexarlos en Elasticsearch, para poder crear nuestras propias analíticas sobre ellos.</p><p><a href="https://www.elastic.co/docs/explore-analyze/workflows">Los flujos de trabajo de Elastic</a> son un motor de automatización integrado dentro de Kibana que te permite definir procesos multipaso usando una configuración sencilla de YAML. Cada flujo de trabajo puede activarse según una programación o un evento, o como una herramienta en <a href="https://www.elastic.co/docs/explore-analyze/ai-features/elastic-agent-builder">Elastic Agent Builder</a>, y cada paso puede llamar a las API de Kibana, consultar Elasticsearch o transformar datos.</p><p>Usaremos los recuentos de vistas del dashboard como ejemplo concreto, pero el mismo patrón aplica a cualquier métrica expuesta a través de la API de objetos guardados de Kibana.</p><h2>Requisitos previos</h2><ul><li><p><a href="https://www.elastic.co/cloud">Elastic Cloud</a> o un clúster <a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed">autogestionado </a>ejecutando la versión 9.3</p></li><li><p><a href="https://www.elastic.co/docs/explore-analyze/workflows/get-started#workflows-prerequisites">Flujos de trabajo activados</a> (Configuración avanzada)</p></li></ul><h2>Paso 1: explora los datos sin procesar en <a href="https://www.elastic.co/docs/explore-analyze/query-filter/tools/console">Herramientas de desarrollo</a></h2><p>Antes de crear cualquier cosa, entendamos qué datos tenemos. Kibana almacena la mayor parte de su configuración y metadatos como <a href="https://www.elastic.co/docs/api/doc/kibana/group/endpoint-saved-objects">objetos guardados</a> en un índice interno dedicado. Una de las cosas que Kibana rastrea de esta manera es el número de vistas del dashboard, mediante un tipo especial de objeto guardado llamado contadores de uso. Puedes consultarlas directamente desde las Herramientas de desarrollo:</p>GET kbn:/api/saved_objects/_find?type=usage-counter&amp;filter=usage-counter.attributes.domainId:"dashboard"%20and%20usage-counter.attributes.counterType:"viewed"&amp;per_page=10000<p>La respuesta tiene el siguiente aspecto:</p>{
  "page": 1,
  "per_page": 10000,
  "total": 1,
  "saved_objects": [
    {
      "type": "usage-counter",
      "id": "dashboard:346f3c64-ebca-484d-9d57-ec600067d596:viewed:server:20260310",
      "attributes": {
        "domainId": "dashboard",
        "counterName": "346f3c64-ebca-484d-9d57-ec600067d596",
        "counterType": "viewed",
        "source": "server",
        "count": 1
      },
      ...
    }
  ]<p>El campo <code>counterName</code> es el ID del dashboard y <code>count</code> es el recuento acumulado de vistas para ese dashboard en ese día específico. Kibana crea un objeto de contador por dashboard al día; puedes ver el sufijo de fecha en el ID del objeto (...viewed:server:20260310). El conteo aumenta a lo largo del día a medida que los usuarios abren el dashboard.</p><p>En lugar de replicar este modelo de documento diario en nuestro índice, crearemos un documento por cada ejecución del flujo de trabajo. Cada documento registra cuántas vistas había acumulado ese dashboard durante el día en el momento de la captura.</p><h2>Paso 2: Crear el índice de destino</h2><p>Necesitamos un índice para almacenar las snapshots de la vista del dashboard. El siguiente comando lo crea con mapeos explícitos para que podamos agregar y visualizar más tarde. Ejecuta esto en herramientas de desarrollo:</p>PUT dashboard-views
{
  "mappings": {
    "properties": {
      "captured_at": {
        "type": "date"
      },
      "dashboard_id": {
        "type": "keyword"
      },
      "dashboard_name": {
        "type": "keyword"
      },
      "view_count": {
        "type": "integer"
      }
    }
  }
}<p>Usar<a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/keyword"><code>keyword</code></a> mapeos para ID y nombres permite <a href="https://www.elastic.co/docs/explore-analyze/query-filter/aggregations">agregaciones</a>. Usar <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/number"><code>integer</code></a> para <code>view_count</code> es un valor predeterminado seguro, ya que Kibana restablece el contador diariamente, por lo que alcanzar el límite de 32 bits (más de 2 mil millones de vistas en un solo día) no es una preocupación realista. Todavía admite operaciones numéricas, como <a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-metrics-max-aggregation"><code>max</code></a>, <a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-metrics-avg-aggregation"><code>avg</code></a> y <a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-metrics-min-aggregation"><code>min</code></a>, entre otras.</p><h2>Paso 3: Crear el flujo de trabajo</h2><p>Ve a <strong>Stack Management &gt; Flujos de trabajo &gt; Nuevo flujo de trabajo</strong>, y pega la siguiente configuración YAML del flujo de trabajo:</p>name: dashboard-views-ingestion
triggers:
  - type: scheduled
    with:
      every: 30m

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

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

      - name: index_doc
        type: elasticsearch.request
        with:
          method: POST
          path: /dashboard-views/_doc
          body:
            dashboard_id: "{{ foreach.item.attributes.counterName }}"
            dashboard_name: "{{ steps.fetch_dashboard_name.output.attributes.title }}"
            view_count: "${{ foreach.item.attributes.count | plus: 0 }}"
            captured_at: "{{ execution.startedAt | date: '%Y-%m-%dT%H:%M:%SZ' }}"<p>En la próxima sección, hagamos un desglose del flujo de trabajo paso a paso.</p><h3>Cómo funciona el flujo de trabajo</h3><h4>Desencadenantes</h4><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7672aa533b4bc9ed/6a17dc5b420229d07c29f4d4/5670991d65c64ee833924225c2d375a1be868b13-325x162.png" alt=" Desencadenantes programados" /><p>El flujo de trabajo se ejecuta mediante un desencadenante programado cada 30 minutos. Esto nos proporciona datos temporales sin saturar la API.</p><h4>fetch_dashboard_views</h4><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltab2a16f1f11304ea/6a17dc5d25daab26f608a117/66eaec147c3d01c524c67cf1c7f663ac56a3259d-812x215.png" alt=" Obtener dashboard" /><p>Usa<code>kibana.request</code> para llamar a la API de objetos guardados de Kibana. No hace falta configurar la autenticación: el motor de flujos de trabajo añade automáticamente los encabezados correctos según el contexto de ejecución.</p><h4>index_each_dashboard (foreach)</h4><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte6b2611b0216555e/6a17dc5f445de95b584cffe5/aad45e8aed8dc81ded6260cd6199ff78dcffe3b4-1892x290.png" alt="Indexa cada dashboard" /><p>Itera sobre la matriz<a href="https://www.elastic.co/docs/api/doc/kibana/group/endpoint-saved-objects"><code>saved_objects</code></a> devuelta por el paso anterior. El elemento actual en cada iteración está disponible como <code>foreach.item</code>. Dentro del bucle, ejecutamos dos pasos anidados para cada dashboard.</p><p><strong>1. </strong><strong><code>fetch_dashboard_name</code></strong><strong>:</strong></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb3733b3c24629abf/6a17dc60a292993fe3d02b75/db21ec5094b743018b9cd66c5052681f14c7d7e3-1999x431.png" alt=": obtener nombre del dashboard" /><p>Resuelve el título legible para los humanos del dashboard al llamar a <code>GET /api/saved_objects/dashboard/{id}</code>. Agregamos <code>on-failure: continue: true</code> para que, si un dashboard se eliminó pero aún tiene contadores de vistas, el bucle continúe en lugar de fallar toda la ejecución.</p><p><strong>2. </strong><strong><code>index_doc</code></strong><strong>:</strong></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt385c1f29d717c280/6a17dc62faa91353cb93c759/f49dd0c9f0817bb1e1e5d9f4a2b05d13ef331054-1999x626.png" alt=" Solicitud de Elasticsearch" /><p>Indexa cada documento usando <code>POST /dashboard-views/_doc</code> (sin un ID explícito), lo que permite que Elasticsearch genere automáticamente los ID. Esto crea un nuevo documento en cada ejecución, lo que crea un historial del número de vistas a lo largo del tiempo en lugar de sobreescribir el snapshot anterior.</p><p>Dos cosas que vale la pena destacar:</p><ul><li><p>El campo <code>captured_at</code> usa el filtro de fecha para formatear la marca de tiempo como <a href="https://www.iso.org/iso-8601-date-and-time-format.html">ISO 8601</a>. Sin ella, el valor sale como un texto de fechas en JavaScript, como <code>Tue Mar 10 2026 05:03:47 GMT+0000</code>, que Elasticsearch no asigna como fecha.</p></li><li><p>El <code>view_count</code> usa la sintaxis <code>${{ }}</code> con <code>| plus: 0</code> para preservar el tipo numérico. Usar <code>{{ }}</code> lo mostraría como un texto, lo que impediría realizar operaciones matemáticas en el dashboard.</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3b94bb6c22c0253e/6a17dc6425daab37a508a11b/6d48c8784d5df6192e8b5175e69dbab5098194bc-919x774.png" alt="" /><p><em>La UI te permite depurar fácilmente cada uno de los pasos del flujo de trabajo.</em></p><h2>Paso 4: Crea el dashboard de estadísticas</h2><p>Una vez que el flujo de trabajo se haya ejecutado varias veces y se hayan recopilado los datos, crea un nuevo dashboard en Kibana usando la Data view de vistas del dashboard.</p><p>Algunos paneles para empezar:</p><ul><li><p><strong>Los dashboards más vistos:</strong> Usa un <a href="https://www.elastic.co/docs/explore-analyze/visualize/charts/bar-charts"><strong>Gráfico de barras</strong></a> con <code>dashboard_name</code> en el eje X y <code>last_value(view_count)</code> en el eje Y. Aquí se muestra el número actual de vistas diarias por dashboard.</p></li><li><p><strong>Vistas a lo largo del tiempo:</strong> usa un <a href="https://www.elastic.co/docs/explore-analyze/visualize/charts/line-charts"><strong>gráfico de líneas</strong></a> con <code>captured_at</code> en el eje X y <code>last_value(view_count)</code> en el eje Y, desglosado por <code>dashboard_name</code>. Dado que cada ejecución agrega un nuevo documento, usa el último valor para obtener el recuento máximo por cubetas de tiempo en lugar de sumar duplicados.</p></li><li><p><strong>Snapshot actual:</strong> usa una <a href="https://www.elastic.co/docs/explore-analyze/visualize/charts/tables"><strong>tabla de datos</strong></a> con el <code>captured_at</code> más reciente para mostrar los recuentos de vistas más recientes en todos los dashboards.</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt18d0390e0526b215/6a17dc65a292991da7d02b79/e245b95f67daf76a2aaf4cb9df2c75ef4cfef582-1462x747.png" alt="" /><p>Dado que cada flujo de trabajo crea un nuevo documento, puedes filtrar por intervalo de tiempo para analizar la actividad en períodos específicos, comparar semana a semana o configurar alertas cuando un dashboard caiga por debajo de un umbral de visitas.</p><h2><strong>Conclusión</strong></h2><p>Elastic Flujos de trabajo es una buena opción para este tipo de recopilación periódica de datos porque tanto el origen (API de Kibana) como el destino (Elasticsearch) son nativos, lo que significa cero gestión de credenciales. El motor de flujo de trabajo maneja la autenticación automáticamente para los pasos <code>kibana.request</code> y <code>elasticsearch.request</code>, por lo que lo único que escribes es la lógica.</p><h2><strong>Recursos</strong></h2><ul><li><p><a href="https://www.elastic.co/docs/explore-analyze/workflows">Flujos de trabajo de Elastic</a></p></li><li><p><a href="https://www.elastic.co/docs/api/doc/kibana/">API de Kibana</a></p></li></ul>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/monitor-kibana-dashboard-views-elastic-workflows</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/monitor-kibana-dashboard-views-elastic-workflows</guid>
    <category><![CDATA[Experiencia del desarrollador]]></category>
    <dc:creator><![CDATA[Gustavo Llermaly]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltef604bbb6dee6be0/6a17dc67a29299db23d02b7d/0ed94ce00962287b5507f45c92ecb60fdcbf2718-1280x720.png" length="0" type="image/png"/>
    <pubDate>Fri, 03 Apr 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Gestión de dependencias en Kubernetes]]></title>
    <description><![CDATA[Cómo optimizar la gestión de dependencias en Kubernetes mediante Renovate CLI y Argo Workflows.]]></description>
    <content:encoded><![CDATA[<p>Así fue como construimos una plataforma de gestión de dependencias autohospedada mediante Kubernetes, Argo Workflows, Argo Events y Renovate CLI para automatizar actualizaciones, abordar rápidamente vulnerabilidades y exposiciones comunes (CVE), y propagar eficientemente nuevas versiones de paquetes en miles de repositorios.</p><h2><strong>Gestión de dependencias en Elastic</strong></h2><p>En Elastic, tenemos que gestionar cientos o incluso miles de repositorios, tanto privados como públicos. Cuando se descubre una CVE crítica, necesitamos respuestas y acciones inmediatas: ¿qué repositorios son vulnerables? ¿Con qué rapidez podemos solucionarlos? Además de la seguridad, surgen cuestiones relacionadas con la productividad: ¿cómo podemos propagar rápidamente el lanzamiento de una nueva versión de un paquete a todos los repositorios que dependen de él, sin dedicar demasiado tiempo a tareas manuales?</p><p>El disparador inicial para buscar formas de hacer la gestión de dependencias fue la necesidad de establecer una base segura con actualizaciones automatizadas para <a href="https://www.elastic.co/blog/reducing-cves-in-elastic-container-images">reducir los CVEs</a>. Después de considerar cuidadosamente las soluciones para la gestión de dependencias, empezamos a trabajar en una infraestructura autohospedada. Usábamos nuestro propio clúster de Kubernetes para ejecutar Mend Renovate Community Self-Hosted. La idea era poder ofrecer una plataforma de gestión de dependencias a la que nuestros usuarios pudieran acceder de forma autónoma.</p><p>El experimento inicial tuvo éxito, así que cada vez más equipos comenzaron a implementar nuestra plataforma y usarla en el ciclo de vida de sus repositorios diarios para actualizaciones y parches CVE. Esto sucedió tan rápido que pronto alcanzamos el límite de nuestra instalación autohospedada.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc99617fc3eed538d/6a170ea9964cea459d08bc67/e14d9f98d4eccaa08a335d5bd23d88e5debbb344-1600x1103.png" alt="Gestión de dependencias en Elastic" /><h3><strong>El reto: ¿Cómo podemos escalar una plataforma de gestión de dependencias en una gran organización con una cantidad significativa de repositorios?</strong></h3><p>Nuestra plataforma de gestión de dependencias procesaba un repositorio a la vez, entonces el modelo de procesamiento secuencial no podía seguir el ritmo debido a la gran cantidad de repositorios que tenemos. Ya habíamos identificado que el problema residía en el concepto de que <strong>una sola instancia</strong> de nuestra herramienta de gestión de dependencias podría procesar nuestra gran y siempre creciente lista de repositorios. Los repositorios esperaban en una cola, a veces durante muchas horas. Más del 50 % de nuestros repositorios ni siquiera se procesaban diariamente. Eso significa que más del 50 % de nuestros repositorios esperaron más de 24 horas entre los escaneos.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0d205fd379e3c07a/6a170eab961e691e1fc4cfca/45ade5bda08f82bed0b3d0d3736cbd6f056e7a4e-1312x816.jpg" alt="Problema de gestión de dependencias" /><p>Los grandes repositorios creaban cuellos de botella mayores, debido a sus grandes bases de código y a sus múltiples PRs abiertas. Los eventos de webhook de GitHub interrumpieron la secuencia. Automerge se volvió poco confiable debido a que el tiempo de escaneo era impredecible. Habíamos hecho una promesa a nuestros usuarios sobre la frecuencia de los escaneos y no pudimos cumplirla.</p><h3><strong>La decisión de integrarnos internamente: satisfacer las necesidades únicas de escalabilidad y seguridad de Elastic</strong></h3><p>Aunque considerábamos opciones comerciales, como <strong>la edición Renovate autohospedada empresarial de Mend</strong>, internamente en Elastic teníamos algunas iniciativas clave en marcha.</p><p>Nuestra decisión de crear una plataforma interna se basó en el reconocimiento de que solo una solución altamente personalizada podría satisfacer los requisitos específicos e innegociables de Elastic:</p><ol><li><p><strong>Inversión en nuestra plataforma interna para desarrolladores:</strong> en ese momento, ya habíamos comenzado a invertir considerablemente en nuestra plataforma interna para desarrolladores. Estábamos discutiendo y diseñando formas en las que cada uno de nuestros servicios pudiera encajar. Esto significaba que queríamos probar nuestras propias reglas y prácticas para nuestra plataforma de gestión de dependencias. Además de eso, entraban en juego nuevas pautas y queríamos diseñar la plataforma antes de los eventos.</p></li><li><p><strong>Integración nativa y personalización del flujo de trabajo:</strong> requeríamos una integración directa con nuestras herramientas y procesos internos. Por ejemplo, queríamos centralizar la configuración como código con nuestro Catálogo de servicios (Backstage). Tenemos necesidades específicas sobre el uso de Backstage con las que queríamos que nuestra plataforma fuera compatible. Así que, aunque fuera posible usar las API de Renovate autohospedadas junto con nuestra automatización de Backstage, esto no cubriría completamente nuestros procesos internos.</p></li><li><p><strong>Seguridad en profundidad específica de Elastic:</strong> nuestro cumplimiento de seguridad estricto requería mecanismos personalizados adaptados a nuestro ecosistema. Estábamos trabajando para <a href="https://entro.security/blog/how-elastic-scaled-secrets-nhi-security-elastics-playbook-from-visibility-to-automation/">fortalecer nuestro uso de “identidades no humanas”.</a> Las medidas de seguridad implementadas implicaban que los métodos no estándar de autenticación en GitHub no funcionarían con una herramienta comercial que no fuera compatible con esta implementación interna. Nuestro flujo de trabajo incluía la implementación de un patrón de cifrado secreto de flujo de trabajo principal-secundario y el uso de tokens de GitHub transitorios y de un solo uso. La creación interna era la única forma práctica de integrar estas capas de seguridad únicas y minimizar la superficie de ataque en nuestro complejo entorno multinube.</p></li></ol><h2><strong>La solución: Orquestación de flujo de trabajo para la gestión de dependencias</strong></h2><p>Nuestra solución partió del hecho de que queríamos basarnos en la herramienta de gestión de dependencias que ya usábamos, no reemplazarla y buscar otras soluciones. Había mostrado signos de su potencial, y su flexibilidad es importante para las distintas necesidades de toda nuestra organización. Consideramos diferentes soluciones, y lo que nos ayudó a decidir fueron las necesidades grandes y a veces especiales que tenemos que cubrir. Decidimos crear una plataforma de gestión de dependencias fiable y escalable, donde cada repositorio se procesa por sí mismo, lo que eliminaría los cuellos de botella y nos prepararía para crecer.</p><p>Diseñamos la plataforma mediante tres principios fundamentales:</p><h3><strong>1. Procesamiento en paralelo</strong></h3><p>Cada repositorio tiene su propio entorno de procesamiento de gestión de dependencias. No más colas. Nuestra concurrencia solo está limitada por la cantidad de recursos que gastamos. También aplicamos una programación distribuida inteligente para evitar que GitHub limite la velocidad.</p><h3><strong>2. Autoservicio</strong></h3><p>Usamos nuestro Catálogo de servicios (Backstage) para incorporar y gestionar automáticamente cualquier repositorio nuevo. Usamos nuestra propia definición de recursos para darle al usuario final la opción de seleccionar con qué frecuencia se procesará un repositorio, cuántos recursos quiere asignar a sus programaciones y si quiere desactivar o volver a activar el procesamiento por cualquier motivo. Planeamos agregar más opciones así a medida que las necesidades de nuestros usuarios evolucionen y se adapten mejor a la nueva instalación.</p><h3><strong>3. Alcance secreto reducido y aislamiento del espacio de nombres</strong></h3><p>Para aumentar la seguridad, suministramos a nuestros pods de gestión de dependencias tokens efímeros de GitHub que se generan al inicio de cada flujo de trabajo. Además, aislamos nuestras cargas de trabajo en espacios de nombres específicos para que solo se les proporcionen los secretos necesarios. Controlamos a qué secretos puede acceder cada uno de los flujos de trabajo de gestión de dependencias mediante Kubernetes RBAC. También utilizamos el cifrado para propagar el token de GitHub desde los flujos de trabajo principales a los secundarios.</p><p>Reconstruimos nuestra plataforma mediante Kubernetes y, al aprovechar el poder de Kubernetes, Argo Workflows impulsa la lógica de nuestros procesos, y Renovate CLI está configurado para escanear y procesar un repositorio a la vez.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3548539ab52fbb79/6a170eac0c48573da601ab26/5560ed20e2bd9ecdd574a9c835126d12b24c332f-1600x1157.png" alt="Visión general de los flujos de trabajo de gestión de dependencias en Kubernetes" /><p><strong>La belleza:</strong> estamos utilizando proyectos de código abierto probados en batalla de una manera original, lo que ofrece nuevos ejemplos de trabajo para todos esos proyectos y, al mismo tiempo, aumenta la velocidad de desarrollo y consolida la reducción de CVE para nuestros equipos.</p><h2><strong>Arquitectura de gestión de dependencias: Cuatro microservicios</strong></h2><p>La plataforma cuenta con cuatro componentes personalizados:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6451ff19da4db511/6a170eaec1e8a562e0f88378/2b3d4046c05bb261e45d40c59f864eb51fb9eaa9-1217x1600.png" alt="Componentes para la gestión de dependencias en Kubernetes" /><h3><strong>Operador de los flujos de trabajo (Go/Kubebuilder)</strong></h3><p>Un operador de Kubernetes que gestiona el ciclo de vida del flujo de trabajo a través de tres definiciones de recursos personalizados (CRD):</p><ul><li><p><strong>CRD de RepoConfig:</strong> Única fuente de verdad para la configuración de repositorios.</p></li></ul><p>Así es como se define RepoConfig en el operador:</p>// RepoConfig is the Schema for the repoconfigs API
type RepoConfig struct {
	metav1.TypeMeta `json:",inline"`

	// metadata is a standard object metadata
	// +optional
	metav1.ObjectMeta `json:"metadata,omitempty,omitzero"`

	// spec defines the desired state of RepoConfig
	// +required
	Spec RepoConfigSpec `json:"spec"`

	// status defines the observed state of RepoConfig
	// +optional
	Status RepoConfigStatus `json:"status,omitempty,omitzero"`
}<p>Y así es como se vería una instancia de RepoConfig:</p>apiVersion: workflows.elastic.co/v1
kind: RepoConfig
metadata:
  generation: 3
  name: elastic-test-repo
  namespace: dependency-management-operator
spec:
  owner: group:my-team
  renovate:
    config:
      resourceGroup: SMALL
      runFrequency: 4h
    enabled: true
  repository: elastic/test-repo<ul><li><p><strong>CRD principal:</strong> Gestiona CronWorkflows para escaneos programados.</p></li></ul><p>Dentro del bucle de reconciliación del controlador principal, nos aseguramos de que los flujos de trabajo se creen y se mantengan actualizados o incluso se eliminen si es necesario.</p><p>En primer lugar, obtienes algunos ajustes configurados globalmente para los flujos de trabajo:</p>func (r *ParentReconciler) reconcileSubResources(ctx context.Context, req ctrl.Request, parent *workflowsv1.Parent) error {
	logger := logf.FromContext(ctx)
	logger.Info("Reconcile SubResources for Parent", "name", req.NamespacedName)
	wfSet := workflowsettings.WorkflowSettings{
		RunFrequency:   parent.Spec.RunFrequency,
		ResourceGroups: "parent",
	}<p>Se asegura de que un configmap de mutex esté actualizado para evitar que flujos de trabajo similares se ejecuten juntos:</p>	cfMngr := resources.NewConfigMapManager(r.Client, r.Scheme, r.OperatorConfig.ParentNamespace)
	err := cfMngr.CreateOrUpdateSyncMutexConfigmap(ctx, fmt.Sprintf("%s%s", r.OperatorConfig.ResourcesPrefix, r.OperatorConfig.SyncMutexCfgMapName), strings.TrimPrefix(parent.Spec.Repository, "elastic/"), r.OperatorConfig.SemaphoreConcurrencyLimit)<p>Luego crea un gestor de flujo de trabajo que es la estructura que creará o actualizará los CronWorkflows y las plantillas de flujo de trabajo:</p>	wfMngr := resources.NewArgoWorkflowManager(r.Client,
		r.Scheme,
		curateResourceName(
			strings.ReplaceAll(parent.Spec.Repository, "/", "-"),
		),
		parent.Namespace,
		"parent-workflow",
		false).
		WithOrganization(r.OperatorConfig.GitHubOrg).
		WithRepoName(parent.Spec.Repository).
		Init(true, true).
		WithPrefix(r.OperatorConfig.ResourcesPrefix).
		WithWfTemplateName(r.OperatorConfig.ParentWorkflowTemplate).
		WithResources(wfSet.GetResourceCategory()).
		WithSchedule(wfSet.GetCronSchedule()).
		WithImagePullSecrets([]corev1.LocalObjectReference{{
			Name: r.OperatorConfig.WorkflowImagePullSecrets,
		}}).
		AddArgument(true, true, "extra_cli_args").
		SetArgument(true, false, "extra_cli_args", "none").
		AddTemplate(resources.NewParentDAGTemplateInstance()).
		AddTemplate(resources.NewWorkflowsTemplateInstance("check-child-workflows", r.OperatorConfig.WorkflowImagePullPolicy, r.OperatorConfig.WorkflowNodeSelector)).
		AddTemplate(resources.NewWorkflowsTemplateInstance("security", r.OperatorConfig.WorkflowImagePullPolicy, r.OperatorConfig.WorkflowNodeSelector)).
		AddTemplate(resources.NewWorkflowsTemplateInstance("submit-child-workflow", r.OperatorConfig.WorkflowImagePullPolicy, r.OperatorConfig.WorkflowNodeSelector))
	wfMngr.OverWriteCommand("submit-child-workflow", r.OperatorConfig.ChildNamespace)
	wfMngr.OverwriteWfTemplateName("parent-wftmpl")
	wfMngr.AddSynchronization(fmt.Sprintf("%s%s", r.OperatorConfig.ResourcesPrefix, r.OperatorConfig.SyncMutexCfgMapName), "{{workflow.parameters.repo_name}}")
	err = wfMngr.CreateOrUpdateCronWorkflow(ctx)
	if err != nil {
		return fmt.Errorf("failed to create or update cron workflow: %w", err)
	}
	err = wfMngr.CreateOrUpdateWorkflowTemplate(ctx)
	if err != nil {
		return fmt.Errorf("failed to create or update workflow template: %w", err)
	}
	return nil<ul><li><p><strong>CRD infantil:</strong> Gestiona las plantillas de flujo de trabajo con recursos por repositorio.</p></li></ul><p>El controlador secundario tiene un deber de reconciliación similar al del padre, pero esta vez es responsable de las plantillas de flujo de trabajo en el espacio de nombres secundario que se activarán por los flujos de trabajo principales.</p>func (r *ChildReconciler) reconcileSubResources(ctx context.Context, req ctrl.Request, child *workflowsv1.Child) error {
	logger := logf.FromContext(ctx)
	logger.Info("Reconcile SubResources for Child", "name", req.NamespacedName)
	wfSet := workflowsettings.WorkflowSettings{
		ResourceGroups: child.Spec.ResourceCategory,
	}
	wfMngr := resources.NewArgoWorkflowManager(r.Client,
		r.Scheme,
		curateResourceName(
			strings.ReplaceAll(child.Spec.Repository, "/", "-"),
		),
		child.Namespace,
		"runner",
		true).
		Init(false, true). // only manage workflow template
		WithPrefix(r.OperatorConfig.ResourcesPrefix).
		WithSuffix("-child-wftmpl").
		WithRepoName(child.Spec.Repository).
		WithOrganization(r.OperatorConfig.GitHubOrg).
		WithResources(wfSet.GetResourceCategory()). // will override resources of presets if set
		WithImagePullSecrets([]corev1.LocalObjectReference{{
			Name: r.OperatorConfig.WorkflowImagePullSecrets,
		}}).
		AddTemplate(resources.NewWorkflowsTemplateInstance("runner", r.OperatorConfig.WorkflowImagePullPolicy, r.OperatorConfig.WorkflowNodeSelector)).
		AddArgument(false, true, "repo_full_name").
		AddArgument(false, true, "repo_name").
		AddArgument(false, true, "encrypted_token").
		AddArgument(false, true, "extra_cli_args")
	wfMngr.OverWriteCommand("runner", r.OperatorConfig.ChildNamespace)
	err := wfMngr.CreateOrUpdateWorkflowTemplate(ctx)
	if err != nil {
		return fmt.Errorf("failed to create or update workflow template: %w", err)
	}
	return nil
}<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta735156ba6e370ef/6a170eaf7d8d6706fd70e7e4/7ac70492a1266ba02cb8afbafc5a486cb38a0edc-1600x1290.png" alt="Flujos de trabajo para la gestión de dependencias en Kubernetes" /><p>El patrón de controlador múltiple proporciona una clara separación: el controlador RepoConfig maneja la incorporación/eliminación, el controlador principal administra la programación y el controlador secundario maneja las plantillas de ejecución.</p><h3><strong>Puerta de enlace de eventos de GitHub (Go)</strong></h3><p>Un proxy de webhook seguro que recibe webhooks de GitHub, verifica firmas, filtra por organización/repositorio y los redirige a Argo Events. Creamos 10 sensores distintos que respondían a interacciones en paneles de dependencias, eventos de relaciones públicas y actualizaciones de paquetes.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7e748ceb93c13a5a/6a170eb1a6c2b908f8e797b4/4828625456cbd6efa8020a20f10d23f294f98a02-1306x1600.png" alt="Interacciones del panel de dependencias en Kubernetes" /><p>Este gateway permite la integración con las aplicaciones de GitHub de la siguiente manera:</p><ul><li><p>Verifica las firmas entrantes de webhook de GitHub para mayor seguridad.</p></li><li><p>Reenvía eventos válidos al EventSource de Argo Events con todos los encabezados relevantes y la autenticación.</p></li><li><p>También configuramos un AuthSecret en EventSource y lo proporcionamos como un encabezado Bearer en las solicitudes reenviadas.</p></li><li><p>Proporcionamos logging, métricas y lógica de reintentos.</p></li></ul><p>Realiza varias validaciones en cada solicitud de evento de GitHub.</p><p>Se asegura de que algunos atributos HTTP estén presentes:</p>// ValidateRequestMethod checks if the request method is POST.
func ValidateRequestMethod(r *http.Request) error {
	if r.Method != http.MethodPost {
		return fmt.Errorf("method not allowed, only POST is accepted")
	}
	return nil
}

// ValidateRequiredHeaders checks for required GitHub headers.
func ValidateRequiredHeaders(r *http.Request) error {
	eventType := r.Header.Get("X-GitHub-Event")
	deliveryID := r.Header.Get("X-GitHub-Delivery")
	signature := r.Header.Get("X-Hub-Signature-256")
	if eventType == "" || deliveryID == "" || signature == "" {
		return fmt.Errorf("missing required GitHub headers")
	}
	return nil
}

// ValidateUserAgent checks that the User-Agent header starts with GitHub-Hookshot/
func ValidateUserAgent(r *http.Request) error {
	userAgent := r.Header.Get("User-Agent")
	if !strings.HasPrefix(userAgent, "GitHub-Hookshot/") {
		return fmt.Errorf("invalid User-Agent")
	}
	return nil
}<p>Al mismo tiempo que valida la firma de cada solicitud y su organización:</p>// ValidateSignature verifies the GitHub webhook signature.
func ValidateSignature(r *http.Request, secret string) ([]byte, error) {
	payload, err := GitHub.ValidatePayload(r, []byte(secret))
	if err != nil {
		return nil, fmt.Errorf("invalid GitHub signature: %w", err)
	}
	return payload, nil
}

// ValidateAllowedOwner checks if the organization login is in the allowed organizations list.
func ValidateAllowedOwner(payload []byte, allowedGitHubOrganizations []string) (string, error) {
	var orgLogin string
	var payloadMap map[string]any
	if err := json.Unmarshal(payload, &amp;payloadMap); err == nil {
		if orgObj, ok := payloadMap["organization"].(map[string]any); ok {
			if login, ok := orgObj["login"].(string); ok {
				orgLogin = login
			} else if name, ok := orgObj["name"].(string); ok {
				orgLogin = name
			}
		}
	}
	if !slices.Contains(allowedGitHubOrganizations, orgLogin) {
		return orgLogin, fmt.Errorf("organization login not allowed")
	}
	return orgLogin, nil
}<p>Por último, se redirige a Argo Events según el tipo de evento:</p>	// Map eventType to Argo `EventSource` path
	var endpoint string
	switch eventType {
	case "push":
		endpoint = "/push"
	case "issues":
		endpoint = "/issues"
	case "pull_request":
		endpoint = "/pull-requests"
	default:
		slog.Info("Ignoring unhandled event type", "event_type", eventType, "delivery_id", deliveryID)
		w.WriteHeader(http.StatusOK)
		_,  = w.Write([]byte("ok"))
		return
	}
	forwardURL := h.config.ArgoEventSourceForwardURL + endpoint<p>En lo que respecta a Argo Events, 10 sensores vigilan el EventBus de Argo Events en busca de nuevos eventos.</p>apiVersion: argoproj.io/v1alpha1
kind: Sensor
metadata:
  name: {{ .Values.sensors.packageUpdateOnDefaultBranch.name }}
  namespace: {{ .Release.Namespace }}
spec:
  eventBusName: {{ .Values.eventBus.name }}<p>Luego, el script aplica la lógica de cada sensor:</p>script: |
          local e = event
          if not e or not e.body or not e.body.repository then
            return false
          end

          -- e.g., "refs/heads/main"
          local ref = e.body.ref
          local default_branch = e.body.repository.default_branch
          if not ref or not default_branch then
            return false
          end

          local expected = "refs/heads/" .. default_branch
          if ref ~= expected then
            return false
          end

        {{- if .Values.sensors.packageUpdateOnDefaultBranch.packageFiles }}
          patterns = { {{- range $i, $f := .Values.sensors.packageUpdateOnDefaultBranch.packageFiles }}{{ if $i }}, {{ end }}"{{ $f }}"{{- end }} }
        {{- end }}

          local function anyMatch(path)
            if type(path) ~= "string" then return false end
            for _, pat in ipairs(patterns) do
              -- match filename at repo root, or anywhere under subdirs
              if path:match(pat) or path:match(".+/" .. pat) then
                return true
              end
            end
            return false
          end

          local function filesContainPackage(paths)
            if type(paths) ~= "table" then return false end
            for _, p in ipairs(paths) do
              if anyMatch(p) then return true end
            end
            return false
          end

          -- Inspect all commits (GitHub includes added/modified/removed lists)
          local commits = e.body.commits
          if type(commits) ~= "table" then
            -- Fallback: some payloads include only head_commit
            commits = {}
            if type(e.body.head_commit) == "table" then
              table.insert(commits, e.body.head_commit)
            end
          end

          for _, c in ipairs(commits) do
            if filesContainPackage(c.added) or filesContainPackage(c.modified) or filesContainPackage(c.removed) then
              return true
            end
          end

          return false<h3><strong>Backstage Syncer (Go)</strong></h3><p>Esto sondea nuestro catálogo de servicios (Backstage) para las entidades de recursos reales del repositorio, las transforma en CRD de RepoConfig y mantiene la Platform sincronizada con los cambios de configuración. Los cambios se aplican en tres minutos.</p>repoMap := make(map[string]map[string]interface{})
			for i := range entities {
				entity := &amp;entities[i]
				if entity.Spec.Type != "GitHub-repository" {
					continue
				}

				implRaw, err := json.Marshal(entity.Spec.Implementation)
				if err != nil {
					logger.Error("Failed to marshal implementation", "error", err)
					continue
				}

				var implMap map[string]interface{}
				err = json.Unmarshal(implRaw, &amp;implMap)
				if err != nil {
					logger.Error("Failed to unmarshal implementation map", "error", err)
					continue
				}
				var repoName string
				if specMap, ok := implMap["spec"].(map[string]interface{}); ok {
					if repo, ok := specMap["repository"].(string); ok {
						repoName = repo
					}
				}
				if repoName == "" {
					continue
				}

				var workflowsRaw []byte
				if v, ok := implMap["spec"].(map[string]interface{}); ok {
					if r, ok := v["renovate"]; ok {
						workflowsRaw,  = json.Marshal(r)
					} else {
						workflowsRaw = []byte(`{}`)
					}
				} else {
					workflowsRaw = []byte(`{}`)
				}

				var workflowsWithDefaults schema.WorkflowsMetadata
				err = json.Unmarshal(workflowsRaw, &amp;rworkflowsWithDefaults)
				if err != nil {
					logger.Error("Failed to unmarshal workflows config", "error", err)
					continue
				}

				workflowsMap := map[string]interface{}{
					"enabled":        workflowsWithDefaults.Enabled,
					"require_pr":     workflowsWithDefaults.RequirePr,
					"resource_group": string(workflowsWithDefaults.ResourceGroup),
					"run_frequency":  string(workflowsWithDefaults.RunFrequency),
				}
				repoMap[repoName] = map[string]interface{}{
					"renovate": workflowsMap,
					"owner":    entity.Spec.Owner,
				}
			}
			logger.Info("Fetched GitHub Repository data from Backstage", "repository_count", len(repoMap), "status_code", resp.StatusCode)<p>Por último, escribe esos datos en instancias de RepoConfig.</p><h3><strong>Base de flujos de trabajo (mixta: JavaScript, Go, Helm)</strong></h3><p>La capa base contiene gráficos de Helm, configuraciones de JavaScript, un contenedor Go para Renovate CLI con soporte de cifrado y un indexador APK personalizado para paquetes Alpine.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4c1b5b840854ddf5/6a170eb47d8d67694e70e7e8/908d19278face3ce1119dbee9146c1264b6e2f30-1600x873.png" alt=" Componentes fundamentales para la gestión de dependencias en Kubernetes" /><h2><strong>Configuración de autoservicio</strong></h2><p>Los equipos configuran sus repositorios de manera declarativa a través de Backstage:</p>spec:
  renovate:
    enabled: true
    config:
      resourceGroup: LARGE      # SMALL | MEDIUM | LARGE  
      runFrequency: "0 */4 * * *"  # Every 4 hours<p>Los grupos de recursos asignan CPU y memoria en función del tamaño del repositorio:</p><ul><li><p><strong>PEQUEÑO:</strong> CPU de 500m, memoria 1Gi.</p></li><li><p><strong>MEDIO:</strong> CPU 1000m, memoria 2Gi.</p></li><li><p><strong>GRANDE:</strong> CPU de 2000 m, memoria de 4 Gi.</p></li></ul><p>La configuración está bajo control de versiones, es auditable y se aplica automáticamente.</p><h2><strong>El patrón padre-hijo</strong></h2><p>El modelo de ejecución usa un patrón de flujo de trabajo primario y secundario:</p><ul><li><p><strong>Flujo de trabajo principal:</strong> CronWorkflow ligero que se ejecuta según lo programado. Cifra los secretos, determina si se debe ejecutar un escaneo y pasa la configuración al secundario.</p></li><li><p><strong>Flujo de trabajo infantil:</strong> pod efímero donde se ejecuta Renovate CLI. Asigna recursos de forma dinámica, descifra secretos de forma aislada y se cierra al completar la tarea.</p></li></ul><p>Esta separación proporciona seguridad (los secretos se cifran en el nivel superior), optimización de recursos (los niveles superiores utilizan recursos mínimos) y escalabilidad (los niveles inferiores se ejecutan en paralelo).</p><h2><strong>Los resultados</strong></h2><h3><strong>Transformación del rendimiento</strong></h3><ul><li><p><strong>Antes:</strong> Un repositorio a la vez, algunos repositorios no se procesaban posiblemente incluso por un día o más, menos de 1000 escaneos por día.</p></li><li><p><strong>Después:</strong> más de 100 escaneos simultáneos, normalmente 8000 escaneos y hasta 10 000 escaneos registrados al día, limitados únicamente por la cantidad de recursos que estamos dispuestos a invertir y cómo gestionamos los límites de GitHub.</p></li></ul><h3><strong>Rentabilidad</strong></h3><p>Sin embargo, por extraño que parezca, ejecutar 8000 pods al día puede darte el mismo resultado de forma mucho más económica que tener un pod de larga duración que intenta lograr los mismos resultados.</p><p>En la configuración anterior, ejecutábamos una sola instancia que, en un buen día, realizaba entre 500 y 600 escaneos. Al mismo tiempo, debido al hecho de que se ejecutarían diferentes tipos de repositorios en el mismo pod, necesitábamos dimensionar el pod para los más grandes. Ese tamaño sería mucho mayor que nuestra oferta extra grande actual, que usa 8 CPU para el pod y 16 GB de memoria.</p><p>Para cumplir con la salida diaria actual, el pod único tendría que ejecutarse durante 12 días. Entonces, al comparar el costo de un solo pod que funciona durante 12 días con 8000 pods de nuestro tamaño “MEDIO” funcionando cada día, nuestro nuevo diseño es mucho más eficiente para la misma salida de escaneos:</p><p>Métrica</p><p>Escenario A (Flujos de trabajo)</p><p>Escenario B (El pod único de larga duración)</p><p>Configuración</p><p>8000 pods (1 vCPU / 2 GB)</p><p>1 pod (8 vCPU / 16 GB)*</p><p>Duración</p><p>10 minutos cada uno</p><p>12 días continuos</p><p>Tiempo total de trabajo</p><p>1333 horas de procesamiento</p><p>288 horas de computación</p><p>Costo total</p><p>$65,83</p><p>$113,75</p><p>Sin embargo, tomemos en consideración que nuestro valor predeterminado para nuestras cargas de trabajo está configurado en “PEQUEÑO”, con la gran mayoría ejecutándose con éxito con 0.5 CPU y 1 G de RAM, y solo unos pocos necesitan cambiar a mediano y grande. Veamos qué sucede si el 60 % de nuestras cargas de trabajo se ejecutan en “PEQUEÑO”, el 30 % en “MEDIANO” y el 10 % en “GRANDE”, lo cual está más cerca de la realidad.</p><p>Métrica</p><p>Escenario A (Enjambre mixto)</p><p>Escenario B (El corredor de fondo)</p><p>Estrategia</p><p>8000 pods (tamaños mixtos)</p><p>1 pod (8 vCPU / 16 GB)*</p><p>Duración</p><p>10 minutos cada uno</p><p>12 días continuos</p><p>Costo total</p><p>$52,66</p><p>$113,75</p><p>Ahorros</p><p>$61,09 (54 % más barato)</p><p>—</p><p>Podemos ver que, con la misma salida, somos mucho más rentables en nuestra configuración actual.</p><h3><strong>Seguridad mejorada</strong></h3><ul><li><p>Tokens efímeros de GitHub (minutos de exposición versus días).</p></li><li><p>Aislamiento del espacio de nombres con límites de control de acceso basado en roles (RBAC).</p></li><li><p>Cifrado secreto en reposo en flujos de trabajo principales.</p></li><li><p>Acceso directo a Vault eliminado.</p></li></ul><h3><strong>Rendimiento previsible</strong></h3><p>Con una frecuencia de escaneo garantizada, finalmente podemos establecer Objetivos de nivel de servicio (SLO). La autofusión funciona de forma confiable. Los equipos confían en la plataforma para cumplir lo prometido.</p><h2><strong>Decisiones arquitectónicas clave</strong></h2><p>Aquí tienes algunas de las decisiones clave de diseño que moldearon el aspecto de la plataforma.</p><ul><li><p><strong>¿Por qué flujos de trabajo padre-hijo?</strong></p></li></ul><p>Adoptamos este patrón para aplicar una estrategia de <strong>defensa en profundidad</strong>. Al restringir las credenciales de alto valor (como los secretos de GitHub App) a un espacio de nombres dedicado y bloqueado, usamos <strong>RBAC para asegurarnos de que los pods de ejecución efímeros no puedan acceder</strong> arbitrariamente a datos confidenciales. Las vulnerabilidades recientes de la cadena de suministro (por ejemplo, los ataques <strong>"Shai Hulud"</strong> de integración continua/entrega continua [CI/CD]) han demostrado la importancia de aislar entornos de ejecución que ejecutan scripts dinámicos desde el almacén de credenciales.</p><p>Simultáneamente, este desacoplamiento permite una <strong>optimización granular de recursos</strong>. Los flujos de trabajo "primarios" actúan como orquestadores ligeros con una huella mínima, mientras que los flujos de trabajo "secundarios" manejan el escaneo de dependencia con uso intensivo de computación. Esta separación simplifica la <strong>gestión de ciclo de vida</strong> al permitirnos aplicar una lógica de reconciliación distinta a cada capa, lo que brinda a los usuarios control sobre los parámetros de ejecución (hijo) mientras conservamos el control administrativo sobre la programación y la infraestructura de seguridad (principal).</p><ul><li><p><strong>¿Por qué es de autoservicio?</strong></p></li></ul><p>Eliminar a nuestro equipo como un cuello de botella para la configuración del repositorio fue un requisito crítico. Nuestra misión era diseñar una <strong>plataforma</strong> escalable y de autoservicio capaz de admitir diversos casos de uso. Nos dimos cuenta de que actuar como <strong>filtros</strong> para cada cambio de configuración era insostenible, dado el gran volumen de repositorios. En su lugar, adoptamos una filosofía de habilitación: proporcionar los “rieles” (infraestructura y <strong>barandillas</strong>) mientras capacitamos a los usuarios para conducir los “trenes” (ejecución y personalización). Creemos que este cambio hacia la <strong>autonomía del equipo</strong> mejora significativamente la productividad al permitir a los usuarios adaptar el sistema a sus necesidades operativas específicas.</p><ul><li><p><strong>¿Por qué el patrón de Kubernetes Operator?</strong></p></li></ul><p>Como se mencionó anteriormente, un principio de diseño fundamental era garantizar que la plataforma fuera completamente de <strong>autoservicio</strong>. Necesitábamos un mecanismo automatizado para capturar la intención de los usuarios (como alternar escaneos, ajustar la frecuencia de programación o ajustar los límites de recursos de tiempo de ejecución) y propagar instantáneamente esos cambios a los flujos de trabajo subyacentes. Al anticipar los requisitos futuros, el sistema también necesitaba ser fácilmente <strong>extensible</strong>.</p><p>Para lograr esto, desarrollamos un <strong>Operador de Kubernetes para la gestión de dependencias</strong> personalizado. Al utilizar <strong>CRD</strong> como interfaz para la configuración, establecimos un <strong>bucle de reconciliación nativo de Kubernetes</strong>. Este operador monitoriza continuamente el estado deseado definido por el usuario y orquesta automáticamente las actualizaciones necesarias en la infraestructura del flujo de trabajo. Esto asegura una operación fluida y <strong>basada en eventos</strong>, donde la lógica de la plataforma maneja toda la complejidad detrás de escena.</p><ul><li><p><strong>¿Para qué sirve diseñar una puerta de enlace de eventos de GitHub?</strong></p></li></ul><p>Adoptar una <strong>arquitectura impulsada por eventos (EDA)</strong> fue esencial para la capacidad de respuesta de la plataforma. Aunque CronWorkflows proporcionaba un calendario de referencia fiable, requeríamos la agilidad para gestionar <strong>ejecuciones ad hoc, </strong>como que los usuarios activaran escaneos manualmente a través del panel. Para lograr esto, necesitábamos una <strong>puerta de enlace de ingestión</strong> dedicada para validar la integridad de la carga útil y enrutar las solicitudes de manera inteligente.</p><p>Evaluamos las soluciones existentes, incluido el GitHub EventSource nativo para Argo, pero identificamos riesgos significativos en cuanto a <strong>la sobrecarga operativa</strong> y las estrictas <strong>cuotas de la API de GitHub</strong> (por ejemplo, los límites de webhook por repositorio). En consecuencia, creamos una puerta de enlace personalizada para desacoplar nuestra infraestructura de estas limitaciones.</p><p>Crucialmente, esta puerta de enlace sirvió como un <strong>punto de control de tráfico</strong> estratégico durante nuestra migración. Actuó como un interruptor, lo que nos permitió realizar una <strong>implementación gradual y granular</strong> (cambio de tráfico) del sistema heredado a la nueva infraestructura. Esto garantizó que la incorporación de miles de repositorios fuera un proceso controlado y sin riesgos, en lugar de un cambio radical.</p><p></p><h2><strong>Lecciones aprendidas</strong></h2><p>Algunas lecciones que hemos aprendido van de la mano con el <a href="https://www.elastic.co/about/our-source-code">código fuente de Elastic</a>:</p><ol><li><p><strong>El cliente primero: </strong>las plataformas están diseñadas para los usuarios. Por eso es importante tener las necesidades de los usuarios como prioridad número uno. Esto moldea la plataforma en una infraestructura y aplicaciones diseñadas de manera eficiente que reducen la fricción con los usuarios, simplifican el escalado de la plataforma y facilitan la adopción.</p></li><li><p><strong>Espacio-tiempo: </strong>a veces el camino de menor resistencia lleva a <strong>arenas movedizas</strong>. Inicialmente, intentamos optimizar el modelo de procesamiento secuencial existente, pero esto no resolvió nuestros problemas. De hecho, solo introdujo más complejidad y cabos sueltos. La audaz decisión de <strong>rediseñar</strong> la plataforma con procesamiento paralelo requirió un esfuerzo inicial significativo. Sin embargo, finalmente allanó el camino para un crecimiento sostenible de la plataforma y prácticamente eliminó el tedioso trabajo administrativo diario.</p></li><li><p><strong>Depende: </strong>una plataforma no puede operar de forma aislada. Su éxito depende de qué tan bien se integre con el ecosistema más amplio. En nuestro caso, la integración con <strong>Backstage</strong> fue crítica, ya que sirve como la única fuente de verdad para la incorporación fluida de servicios. Del mismo modo, conectarnos a <strong>Artifactory</strong> nos permitió gestionar las actualizaciones de paquetes privados de manera eficiente, y la lista de integraciones esenciales continúa.</p></li><li><p><strong>Progreso, perfección simple: </strong>a lo largo de la implementación, sometimos constantemente a prueba nuestras hipótesis iniciales y nos adaptamos a los nuevos obstáculos que iban surgiendo. En lugar de quedar paralizados por el perfeccionismo, adoptamos un <strong>enfoque iterativo</strong>, abordamos los desafíos uno por uno y ajustamos nuestra estrategia de migración para cumplir con las condiciones del mundo real.</p></li></ol><h2><strong>Lo que se viene</strong></h2><p>La entrega de la plataforma nos permite realizar un trabajo más significativo que nos ayudará a mejorar la UX y la eficiencia de nuestra plataforma. Algunos ejemplos son:
</p><ul><li><p><strong>Aumento y protección de la adopción de la fusión automática</strong></p></li></ul><p>La característica de fusión automática acelera significativamente la velocidad del equipo al eliminar las tareas manuales tediosas. Sin embargo, tenemos que asegurarnos de que haya <strong>barreras</strong> estrictas para garantizar que este aumento de velocidad no se haga a expensas de la seguridad.
</p><ul><li><p><strong>Mejora la observabilidad en torno a la experiencia del usuario final</strong></p></li></ul><p>Una prioridad fundamental de nuestra hoja de ruta es mejorar la observabilidad, no solo a nivel de plataforma, sino también específicamente desde la <strong>perspectiva del usuario final</strong>. Aunque capturar métricas de infraestructura es sencillo, comprender la experiencia real del usuario requiere conocimientos más profundos. Estamos trabajando para definir los indicadores de rendimiento (KPI) centrados en el usuario de núcleo para que nuestra telemetría pueda detectar los puntos de fricción y los problemas de rendimiento <strong>antes</strong> de que se conviertan en quejas de los usuarios.</p><ul><li><p><strong>Elimina obstáculos para una mayor adopción</strong></p></li></ul><p>De cara al futuro, nuestra prioridad es identificar y eliminar cualquier barrera que dificulte la adopción de plataformas. Ya sea que esto requiera desarrollar nuevas integraciones o desplegar conjuntos de características específicas, estamos comprometidos con la planificación basada en datos. Construimos con éxito una plataforma diseñada para escalar; nuestro enfoque ahora cambia a <strong>maximizar su potencial</strong>.
</p><h2><strong>El panorama general</strong></h2><p>El proyecto de flujos de trabajo de gestión de dependencias demuestra un principio más amplio: <strong>cuando necesites escalar herramientas de código abierto más allá de su modelo de despliegue predeterminado, los patrones nativos de Kubernetes proporcionan un camino a seguir</strong>.</p><p>Al adoptar:</p><ul><li><p>CRDs para configuración.</p></li><li><p>Operadores para la gestión de ciclo de vida.</p></li><li><p>Arquitectura basada en eventos para mayor capacidad de respuesta.</p></li><li><p>GitOps para el despliegue.</p></li></ul><p>Creamos una orquestación que escala independientemente de la cantidad de repositorios que gestiona. El rendimiento de escanear un solo repositorio es el mismo tanto si gestionamos 100 como si gestionamos 1000.</p><p>Cuando se anuncia un CVE crítico, ahora tenemos respuestas en minutos, no en horas. Esa es la diferencia entre un cuello de botella y una ventaja competitiva.</p><h2><strong>Agradecimientos</strong></h2><p>Esta plataforma se basa en excelentes herramientas de código abierto:</p><ul><li><p><strong>Kubebuilder:</strong> el marco de trabajo de código abierto que usamos para poner en marcha nuestros operadores Kubernetes que inician y orquestan nuestros flujos de trabajo. [<a href="https://github.com/kubernetes-sigs/kubebuilder">1</a>][<a href="https://book.kubebuilder.io/">2</a>]</p></li><li><p><strong>Backstage:</strong> el marco de trabajo de código abierto sobre el cual hemos construido nuestro catálogo de servicios y que utilizamos como nuestra fuente de la verdad. [<a href="https://github.com/backstage/backstage">1</a>][<a href="https://backstage.io/">2</a>]</p></li><li><p><strong>Argo Workflows y Argo Events:</strong> la suite de código abierto que usábamos para orquestar procesos complejos y agregar procesamiento dinámico basado en eventos. [1][<a href="https://argo-workflows.readthedocs.io/en/stable/">2</a>][<a href="https://argoproj.github.io/argo-events/">3</a>][<a href="https://github.com/argoproj/argo-events">4</a>]</p></li><li><p><strong>Renovate CLI:</strong> la herramienta de gestión de dependencias de código abierto que procesa nuestros repositorios. [<a href="https://github.com/renovatebot/renovate">1</a>][<a href="https://docs.renovatebot.com/getting-started/running/">2</a>]</p></li></ul><p>* El modelo de precios de AWS Fargate se usó como referencia para el costo de un solo pod, aunque nuestras cargas de trabajo no se ejecutan necesariamente en AWS y se ejecutan en clústeres de Kubernetes completos.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/dependency-management-kubernetes</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/dependency-management-kubernetes</guid>
    <category><![CDATA[Experiencia del desarrollador]]></category>
    <dc:creator><![CDATA[Nikos Fotiou]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6033995d6660d149/6a170eb5839dfa63f1dcff9b/00519840e6eec7101c1fb096afcae976ee0c454e-1280x720.png" length="0" type="image/png"/>
    <pubDate>Thu, 19 Feb 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Introducción de la interfaz de reglas de consulta Elasticsearch en Kibana]]></title>
    <description><![CDATA[Aprende a usar la interfaz de Reglas de Consulta de Elasticsearch para agregar o excluir documentos de consultas de búsqueda usando conjuntos de reglas personalizables en Kibana, sin afectar al ranking orgánico.]]></description>
    <content:encoded><![CDATA[<p>La función de un motor de búsqueda es devolver resultados relevantes. Sin embargo, hay necesidades empresariales que van más allá de eso, como destacar las ventas, priorizar productos de temporada o mostrar artículos patrocinados, y los desarrolladores no siempre pueden hacer esto en la consulta de búsqueda.</p><p>Además, estos casos de uso suelen ser sensibles al tiempo, y pasar por las etapas típicas de desarrollo (crear una rama de código y luego esperar una nueva versión) es un proceso que consume mucho tiempo.</p><p>Entonces, ¿y si pudiéramos hacer todo este proceso solo con una llamada a la API, o mejor aún, con solo unos clics en Kibana?</p><h2>Interfaz de Reglas de Consulta</h2><p>Elasticsearch 8.10 introdujo Reglas de <a href="https://www.elastic.co/blog/introducing-query-rules-elasticsearch-8-10"><strong>Consulta</strong></a> y <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrievers/rule-retriever"><strong>Retriever de Reglas</strong></a>. Estas son herramientas diseñadas para inyectar <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-pinned-query"><em>resultados fijados</em></a> en las consultas sin afectar la clasificación de los resultados orgánicos según reglas. Solo agregan lógica de negocio encima de los resultados de forma declarativa y sencilla.</p><p>Algunos casos de uso comunes para las Reglas de Consulta son:</p><ul><li><p><strong>Destacar anuncios u ofertas promocionadas</strong>: Mostrar artículos en oferta o patrocinados en la parte superior.</p></li><li><p><strong>Excluyendo por contexto o geolocalización</strong>: ocultar ciertos objetos cuando la normativa local no permite mostrarlos.</p></li><li><p><strong>Priorizar los resultados clave</strong>: Cerciorar de que las búsquedas populares o fijas estén siempre en la cima, independientemente del ranking orgánico.</p></li></ul><p>Para acceder a la interfaz e interactuar con estas herramientas, necesitas hacer clic en el menú lateral de Kibana e ir a <strong>Reglas de consulta</strong>, en <strong>Relevancia:</strong></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltac12541cddd58e36/6a170853a29299941cd00fc2/242e33e89d1a07ffa0e76009c46b3a9236722741-458x1010.png" alt="Acceso a las reglas de consulta en Elasticsearch bajo relevancia" /><p>Cuando aparezca el menú de reglas de consulta, haz clic <strong>en Crear tu primer conjunto de reglas:</strong></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltcc28329c0f3c3aa9/6a17085547d49c67e22d893b/30b3a91bbbf243d314cf38298e01ca5cff784430-1600x945.png" alt="Crear tu primer conjunto de reglas de consulta en Elasticsearch" /><p>A continuación, tienes que nombrar tu conjunto de normas.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb37d271297a4f148/6a170856a29299782cd00fc6/26c5462f88678867776f933b5655ca0df0d72a16-708x446.png" alt="Nombrar tu conjunto de reglas de consulta en Elasticsearch" /><p>La forma para definir cada regla tiene tres componentes clave:</p><ul><li><p><strong>Criterios</strong>: Las condiciones que deben cumplir para que la norma se aplique. Por ejemplo, "cuando el campo query_string contiene el valor <em>Christmas</em>" o "cuando el campo del país es <em>CO."</em></p></li><li><p><strong>Acción</strong>: Esto es lo que quieres que ocurra cuando se cumplan las condiciones. Puede fijar (fijar un documento a los resultados superiores) o excluir (ocultar un documento).</p></li><li><p><strong>Metadatos</strong>: Estos son los campos que acompañan la consulta cuando se ejecuta. Pueden incluir la información del usuario (como ubicación o idioma), así como datos de búsqueda (query_string). Estos son los valores que emplean los criterios para decidir si aplicar o no una regla.</p></li></ul><h2>Ejemplo: objetos populares</h2><p>Imaginemos que tenemos un sitio de comercio electrónico con diferentes artículos. Al analizar las métricas, observamos que uno de los artículos más vendidos en la categoría de consolas es el "DualShock 4 Wireless Controller", especialmente cuando los usuarios buscan las palabras clave "PS4" o "PlayStation 4". Así que decidimos poner este producto encima de los resultados cada vez que un usuario busque esas palabras clave.</p><p>Primero, indexemos los documentos de cada elemento usando una solicitud Bulk API:</p>POST _bulk
{ "index": { "_index": "products", "_id": "1" } }
{ "id": "1", "name": "PlayStation 4 Slim 1TB", "category": "console", "brand": "Sony", "price": 1200 }
{ "index": { "_index": "products", "_id": "2" } }
{ "id": "2", "name": "DualShock 4 Wireless Controller", "category": "accessory", "brand": "Sony", "price": 250 }
{ "index": { "_index": "products", "_id": "3" } }
{ "id": "3", "name": "PlayStation 4 Camera", "category": "accessory", "brand": "Sony", "price": 200 }
{ "index": { "_index": "products", "_id": "4" } }
{ "id": "4", "name": "PlayStation 4 VR Headset", "category": "accessory", "brand": "Sony", "price": 900 }
{ "index": { "_index": "products", "_id": "5" } }
{ "id": "5", "name": "Charging Station for DualShock 4", "category": "accessory", "brand": "Sony", "price": 80 }<p>Si no intervenimos en la consulta, el elemento suele aparecer en cuarto lugar. Aquí está la pregunta:</p>GET products/_search
{
 "query": {
   "match": {
     "name": "PlayStation 4"
   }
 }
}<p>Y aquí están los resultados</p>{
 "took": 1,
 "timed_out": false,
 "_shards": {
   "total": 1,
   "successful": 1,
   "skipped": 0,
   "failed": 0
 },
 "hits": {
   "total": {
     "value": 5,
     "relation": "eq"
   },
   "max_score": 0.6973252,
   "hits": [
     {
       "_index": "products",
       "_id": "3",
       "_score": 0.6973252,
       "_source": {
         "id": "3",
         "name": "PlayStation 4 Camera",
         "category": "accessory",
         "brand": "Sony",
         "price": 200
       }
     },
     {
       "_index": "products",
       "_id": "1",
       "_score": 0.6260078,
       "_source": {
         "id": "1",
         "name": "PlayStation 4 Slim 1TB",
         "category": "console",
         "brand": "Sony",
         "price": 1200
       }
     },
     {
       "_index": "products",
       "_id": "4",
       "_score": 0.6260078,
       "_source": {
         "id": "4",
         "name": "PlayStation 4 VR Headset",
         "category": "accessory",
         "brand": "Sony",
         "price": 900
       }
     },
     {
       "_index": "products",
       "_id": "2",
       "_score": 0.08701137,
       "_source": {
         "id": "2",
         "name": "DualShock 4 Wireless Controller",
         "category": "accessory",
         "brand": "Sony",
         "price": 250
       }
     },
     {
       "_index": "products",
       "_id": "5",
       "_score": 0.07893815,
       "_source": {
         "id": "5",
         "name": "Charging Station for DualShock 4",
         "category": "accessory",
         "brand": "Sony",
         "price": 80
       }
     }
   ]
 }
}<p>Vamos a crear una regla de consulta para cambiar esto. Primero, vamos a agregarlo al reglamento de esta manera:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1576d4f4a2e60548/6a170858cdacbfccb07d298d/fdc42646fb3e76a09bca7d19047a76efe343f7a2-1600x650.png" alt="Cómo editar un conjunto de reglas de consulta en Elasticsearch" /><p>O <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-query-rules-put-ruleset">solicitud API</a> equivalente:</p>PUT _query_rules/my-rules
{
  "rules": [
    {
      "rule_id": "rule-1232",
      "type": "pinned",
      "criteria": [
        {
          "type": "exact",
          "metadata": "query_string",
          "values": [
            "PS4",
            "PlayStation 4"
          ]
        }
      ],
      "actions": {
        "docs": [
          {
            "_index": "products",
            "_id": "2"
          }
        ]
      }
    }
  ]
}<p>Para usar el <strong>conjunto de reglas </strong>en nuestra consulta, debemos usar un tipo de regla de consulta. Este tipo de consulta se compone de dos partes principales:</p>GET /products/_search
{
 "retriever": {
   "rule": {
     "retriever": {
       "standard": {
         "query": {
           "match": { "name": "PlayStation 4" }
         }
       }
     },
     "match_criteria": {
       "query_string": "PlayStation 4"
     },
     "ruleset_ids": ["my-rules"]
   }
 }
}<ul><li><p><strong>match_criteria</strong>: Estos son los metadatos que se emplean para comparar con la consulta del usuario. En este ejemplo, el conjunto de reglas se activa cuando el campo query_string tiene el valor "PlayStation 4."</p></li><li><p><strong>Consulta</strong>: La consulta real que se usará para buscar y obtener los resultados orgánicos.</p></li></ul><p>De este modo, primero ejecutas la consulta orgánica y luego Elasticsearch aplica las reglas de tu conjunto de reglas:</p>{
 "took": 17,
 "timed_out": false,
 "_shards": {
   "total": 1,
   "successful": 1,
   "skipped": 0,
   "failed": 0
 },
 "hits": {
   "total": {
     "value": 5,
     "relation": "eq"
   },
   "max_score": 1.7014122e+38,
   "hits": [
     {
       "_index": "products",
       "_id": "2",
       "_score": 1.7014122e+38,
       "_source": {
         "id": "2",
         "name": "DualShock 4 Wireless Controller",
         "category": "accessory",
         "brand": "Sony",
         "price": 250
       }
     },
     {
       "_index": "products",
       "_id": "3",
       "_score": 0.6973252,
       "_source": {
         "id": "3",
         "name": "PlayStation 4 Camera",
         "category": "accessory",
         "brand": "Sony",
         "price": 200
       }
     },
     {
       "_index": "products",
       "_id": "1",
       "_score": 0.6260078,
       "_source": {
         "id": "1",
         "name": "PlayStation 4 Slim 1TB",
         "category": "console",
         "brand": "Sony",
         "price": 1200
       }
     },
     {
       "_index": "products",
       "_id": "4",
       "_score": 0.6260078,
       "_source": {
         "id": "4",
         "name": "PlayStation 4 VR Headset",
         "category": "accessory",
         "brand": "Sony",
         "price": 900
       }
     },
     {
       "_index": "products",
       "_id": "5",
       "_score": 0.07893815,
       "_source": {
         "id": "5",
         "name": "Charging Station for DualShock 4",
         "category": "accessory",
         "brand": "Sony",
         "price": 80
       }
     }
   ]
 }
}<h2>Ejemplo: metadatos basados en el usuario</h2><p>Otra aplicación interesante de las Reglas de Consulta es usar metadatos para mostrar documentos específicos basar en información contextual del usuario o del sitio web.</p><p>Por ejemplo, imaginemos que queremos destacar artículos o ventas personalizadas basándonos en el nivel de fidelidad del usuario, representado como un valor numérico.</p><p>Podemos hacerlo ingiriendo estos metadatos directamente en la consulta para que las reglas se activen cuando dicho valor cumple ciertos criterios.</p><p>Primero, indexaremos un documento que solo los usuarios con un alto nivel de lealtad puedan ver:</p>POST _bulk
{ "index": { "_index": "products", "_id": "6" } }
{ "id": "6", "name": "PlayStation Plus Deluxe Card - 12 months", "category": "membership", "brand": "Sony", "price": 300 }<p>Ahora, creemos una nueva regla dentro del mismo conjunto de reglas para que cuando el loyalty_level sea igual o superior a 80, el elemento aparezca encima de los resultados.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt158578005df8c76d/6a17085aab7f086dc0db9de3/58de12dff93305440608f51465462fcc68653a08-1421x496.png" alt="Cómo editar un conjunto de reglas de consulta en Elasticsearch" /><p>Almacena la regla y el reglamento.</p><p>Aquí está la solicitud REST equivalente:</p>PUT _query_rules/my-rules
{
  "rules": [
    {
      "rule_id": "pin-premiun-user",
      "type": "pinned",
      "criteria": [
        {
          "type": "gte",
          "metadata": "loyalty_level",
          "values": [
            80
          ]
        }
      ],
      "actions": {
        "docs": [
          {
            "_index": "products",
            "_id": "6"
          }
        ]
      }
    }
  ]
}<p>Ahora, al ejecutar una consulta, necesitamos incluir el nuevo <strong>parámetro loyalty_level </strong>en los metadatos. Si se cumple la condición de la regla, el nuevo documento aparecerá encima de los resultados.</p><p>Por ejemplo, al enviar una consulta donde el loyalty_level es 80:</p>POST /products/_search
{
  "retriever": {
    "rule": {
      "retriever": {
        "standard": {
          "query": {
            "match": {
              "name": "PlayStation"
            }
          }
        }
      },
      "match_criteria": {
        "query_string": "PlayStation",
        "loyalty_level": 80
      },
      "ruleset_ids": ["my-rules"]
    }
  }
}<p>Veremos el documento de lealtad encima de los resultados:</p>{
  "took": 31,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 4,
      "relation": "eq"
    },
    "max_score": 1.7014122e+38,
    "hits": [
      {
        "_index": "products",
        "_id": "6",
        "_score": 1.7014122e+38,
        "_source": {
          "id": "6",
          "name": "PlayStation Plus Deluxe Card - 12 months",
          "category": "membership",
          "brand": "Sony",
          "price": 300
        }
      },
      {
        "_index": "products",
        "_id": "3",
        "_score": 0.5054567,
        "_source": {
          "id": "3",
          "name": "PlayStation 4 Camera",
          "category": "accessory",
          "brand": "Sony",
          "price": 200
        }
      },
      {
        "_index": "products",
        "_id": "1",
        "_score": 0.45618832,
        "_source": {
          "id": "1",
          "name": "PlayStation 4 Slim 1TB",
          "category": "console",
          "brand": "Sony",
          "price": 1200
        }
      },
      {
        "_index": "products",
        "_id": "4",
        "_score": 0.45618832,
        "_source": {
          "id": "4",
          "name": "PlayStation 4 VR Headset",
          "category": "accessory",
          "brand": "Sony",
          "price": 900
        }
      }
    ]
  }
}<p>En el caso siguiente, dado que el nivel de lealtad es 70, la regla no se cumple y el objeto no debería aparecer arriba:</p>POST /products/_search
{
  "retriever": {
    "rule": {
      "retriever": {
        "standard": {
          "query": {
            "match": {
              "name": "PlayStation"
            }
          }
        }
      },
      "match_criteria": {
        "query_string": "PlayStation",
        "loyalty_level": 70
      },
      "ruleset_ids": ["my-rules"]
    }
  }
}<p>Aquí están los resultados:</p>{
  "took": 7,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 4,
      "relation": "eq"
    },
    "max_score": 0.5054567,
    "hits": [
      {
        "_index": "products",
        "_id": "3",
        "_score": 0.5054567,
        "_source": {
          "id": "3",
          "name": "PlayStation 4 Camera",
          "category": "accessory",
          "brand": "Sony",
          "price": 200
        }
      },
      {
        "_index": "products",
        "_id": "1",
        "_score": 0.45618832,
        "_source": {
          "id": "1",
          "name": "PlayStation 4 Slim 1TB",
          "category": "console",
          "brand": "Sony",
          "price": 1200
        }
      },
      {
        "_index": "products",
        "_id": "4",
        "_score": 0.45618832,
        "_source": {
          "id": "4",
          "name": "PlayStation 4 VR Headset",
          "category": "accessory",
          "brand": "Sony",
          "price": 900
        }
      },
      {
        "_index": "products",
        "_id": "6",
        "_score": 0.3817649,
        "_source": {
          "id": "6",
          "name": "PlayStation Plus Deluxe Card - 12 months",
          "category": "membership",
          "brand": "Sony",
          "price": 300
        }
      }
    ]
  }
}<h2>Ejemplo: exclusión inmediata</h2><p>Supongamos que nuestro <strong>mando inalámbrico DualShock 4 (ID 2)</strong> está temporalmente indisponible y no puede vender. Así que, en lugar de eliminar manualmente el documento o esperar a que algún proceso de datos se active, el equipo de negocio decide eliminarlo de los resultados de búsqueda mientras tanto.</p><p>Usaremos un proceso similar al que acabamos de aplicar a los objetos populares, pero esta vez en lugar de seleccionar <em>Fijado</em>, elegiremos <em>Excluir</em>. Esta regla funciona como una especie de lista negra. Cambia los criterios a <strong>Siempre</strong> para que la exclusión funcione cada vez que se ejecute la consulta.</p><p>La regla debería ser así:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt38564c0b7f4a6ee2/6a17085c1949f78692e7a989/f10971e4f1bc9520105111adfa3a476581a27130-1600x623.png" alt="Ejemplo de un conjunto de reglas de exclusión inmediata en Elasticsearch" /><p>Almacena la regla y el conjunto de reglas para aplicar los cambios. Aquí está la solicitud REST equivalente:</p>PUT _query_rules/my-rules
{
  "rules": [
    {
      "rule_id": "rule-6358",
      "type": "pinned",
      "criteria": [
        {
          "type": "always"
        }
      ],
      "actions": {
        "docs": [
          {
            "_index": "products",
            "_id": "2"
          }
        ]
      }
    }
  ]
}<p>Ahora, cuando ejecutamos la consulta de nuevo, verás que el elemento ya no aparece en los resultados, aunque la regla anterior sea fijarlo. Esto se debe a <strong>que las exclusiones tienen prioridad sobre los resultados de fijación</strong>.</p>{
 "took": 6,
 "timed_out": false,
 "_shards": {
   "total": 1,
   "successful": 1,
   "skipped": 0,
   "failed": 0
 },
 "hits": {
   "total": {
     "value": 4,
     "relation": "eq"
   },
   "max_score": 2.205655,
   "hits": [
     {
       "_index": "products",
       "_id": "3",
       "_score": 2.205655,
       "_source": {
         "id": "3",
         "name": "PlayStation 4 Camera",
         "category": "accessory",
         "brand": "Sony",
         "price": 200
       }
     },
     {
       "_index": "products",
       "_id": "1",
       "_score": 1.9738505,
       "_source": {
         "id": "1",
         "name": "PlayStation 4 Slim 1TB",
         "category": "console",
         "brand": "Sony",
         "price": 1200
       }
     },
     {
       "_index": "products",
       "_id": "4",
       "_score": 1.9738505,
       "_source": {
         "id": "4",
         "name": "PlayStation 4 VR Headset",
         "category": "accessory",
         "brand": "Sony",
         "price": 900
       }
     },
     {
       "_index": "products",
       "_id": "5",
       "_score": 0.69247496,
       "_source": {
         "id": "5",
         "name": "Charging Station for DualShock 4",
         "category": "accessory",
         "brand": "Sony",
         "price": 80
       }
     }
   ]
 }
}<h2>Conclusión</h2><p><strong>Las Reglas de Consulta</strong> facilitan mucho ajustar la relevancia sin ningún cambio en el código. La nueva interfaz <strong>Kibana</strong> tepermite realizar estos cambios en cuestión de segundos, dándote a ti y a tu equipo empresarial más control sobre los resultados de búsqueda.</p><p>Más allá del comercio electrónico, las Reglas de Consulta pueden impulsar muchos otros escenarios: destacar guías de resolución de problemas en portales de soporte, mostrar documentos internos clave en bases de conocimiento, promover noticias de última hora en sitios de noticias o filtrar ofertas de empleo o contenido caducado. Incluso pueden hacer cumplir normas de cumplimiento, como ocultar material restringido por rol de usuario o región.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-query-rules-ui-introduction</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-query-rules-ui-introduction</guid>
    <category><![CDATA[Conceptos básicos]]></category>
    <category><![CDATA[Experiencia del desarrollador]]></category>
    <dc:creator><![CDATA[Jhon Guzmán]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt565ed0eb407e098d/6a17085d8b73cb363d189fb1/1fb10bd31c509cc9b9bb4f71f49970f140e6c36f-1600x945.png" length="0" type="image/png"/>
    <pubDate>Fri, 07 Nov 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Construir un agente de conocimiento con recordación semántica usando Mastra y Elasticsearch]]></title>
    <description><![CDATA[Aprende a construir un agente de conocimiento con recordación semántica usando Mastra y Elasticsearch como almacén vectorial para la recuperación de memoria e información.]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/search-labs/blog/context-engineering-overview">La Ingeniería del Contexto</a> está ganando cada vez más importancia para construir agentes y arquitecturas de IA fiables. A medida que los modelos mejoran, su eficacia y fiabilidad dependen menos de sus datos capacitados y más de lo bien que estén fundamentados en el contexto adecuado. Los agentes que pueden recuperar y aplicar la información más relevante en el momento adecuado tienen muchas más probabilidades de producir resultados precisos y fiables.</p><p>En este blog, emplearemos <a href="https://mastra.ai/">Mastra</a> para construir un agente de conocimiento que recuerda lo que dicen los usuarios y puede recuperar información relevante más adelante, empleando Elasticsearch como backend de memoria y recuperación. Puedes extender fácilmente este mismo concepto a casos de uso reales, piensa en agentes de soporte que puedan recordar conversaciones y resoluciones pasadas, permitiéndoles adaptar las respuestas a usuarios específicos o a soluciones superficiales más rápido basar en contextos previos.</p><p>Sigue aquí para ver cómo construirlo paso a paso. Si te pierdes o simplemente quieres ejecutar un ejemplo terminado, echa un vistazo al <a href="https://github.com/jdarmada/getting-started-mastra-elastic/tree/main">repositorio aquí</a>.</p><h2>¿Qué es Mastra?</h2><p>Mastra es un framework TypeScript de código abierto para construir agentes de IA con partes intercambiables para razonamiento, memoria y herramientas. Su función <a href="https://mastra.ai/docs/memory/semantic-recall">de recuperación semántica</a> permite a los agentes recordar y recuperar interacciones pasadas almacenando mensajes como incrustaciones en una base de datos vectorial. Esto permite a los agentes mantener el contexto y la continuidad de la conversación a largo plazo. Elasticsearch es un excelente almacén vectorial para habilitar esta función, ya que soporta una búsqueda vectorial densa eficiente. Cuando se activa la recuperación semántica, el agente extrae mensajes pasados relevantes en la ventana de contexto del modelo, permitiendo que el modelo emplee ese contexto recuperado como base para su razonamiento y respuestas.</p><h2>Lo que necesitas para empezar</h2><ul><li><p>Nodo v18+</p></li><li><p>Elasticsearch (versión 8.15 o posterior)</p></li><li><p>Clave API de Elasticsearch</p></li><li><p><a href="https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key">Clave API de OpenAI</a></p></li></ul><p>Nota: Necesitarás esto porque la demo usa el proveedor OpenAI, pero Mastra soporta otros SDKs de IA y proveedores de modelos comunitarios, así que puedes cambiarlo fácilmente según tu configuración.</p><h2>Construyendo un proyecto de Mastra</h2><p>Emplearemos la CLI integrada de Mamra para proporcionar el andamiaje de nuestro proyecto. Ejecuta el comando:</p>npm create mastra@latest<p>Recibirás un conjunto de indicaciones, que empiezan por:</p><p>1. Pon un nombre a tu proyecto.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt87f941f654d03827/6a16f7af67045b214d45bfa1/2b9fe559e0276140dd539e24f916a73c60870405-620x84.png" alt="Nombrar un prompt en la app Mastra" /><p>2. Podemos mantener este valor predeterminado; No dudes en dejar esto en blanco.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7dbb3d4f27435cac/6a16f7b0cdacbf29497d27de/e04729eb03bce8499e973e18c28642402340d0e5-852x68.png" alt="Indicándole a Mastra dónde almacenar los archivos de prompt" /><p>3. Para este proyecto, emplearemos un modelo proporcionado por OpenAI.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1f654f6cb9397e94/6a16f7b2964cea899a08b942/a86596a469a71bdf8bd99cbaf528d0f0cf7272c0-436x222.png" alt="Selección de un modelo proporcionado por OpenAI en Mastra" /><p>4. Selecciona la opción "Saltar por ahora" porque almacenaremos todas nuestras variables de entorno en un archivo '.env' que configuraremos en un paso posterior.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltff106117521a3519/6a16f7b3c1e8a5031af880d8/02b19ccc34af0bdacf52fd94b519d036540ca2e6-426x114.png" alt="Seleccionando saltar por ahora para la clave OpenAI" /><p>5. También podemos saltar esta opción.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltcda7d9c51c3878d7/6a16f7b450916809dbe1b892/b3fe63d19d270bc2e0de1dd92033bf8b26750819-990x208.png" alt="" /><p>Una vez que termines de inicializar, podemos pasar al siguiente paso.</p><h3>Instalación de dependencias</h3><p>A continuación, necesitamos instalar algunas dependencias:</p>npm install ai @ai-sdk/openai @elastic/elasticsearch dotenv<ul><li><p><code>ai</code> - Paquete básico de SDK de IA que proporciona herramientas para gestionar modelos de IA, prompts y flujos de trabajo en JavaScript/TypeScript. Mastra está construido sobre el <a href="https://ai-sdk.dev/">SDK de IA</a> por Vercel, así que necesitamos esta dependencia para permitir la interacción del modelo con tu agente.</p></li><li><p><code>@ai-sdk/openai</code> - Plugin que conecta el SDK de IA con modelos OpenAI (como GPT-4, GPT-4o, etc.), habilitando llamadas API usando tu clave API OpenAI.</p></li><li><p><code>@elastic/elasticsearch</code> - <a href="https://www.elastic.co/docs/reference/elasticsearch/clients/javascript">Cliente oficial de Elasticsearch para Node.js</a>, se emplea para conectarse a tu Elastic Cloud o a un clúster local para operaciones de indexación, búsqueda y vectores.</p></li><li><p><code>dotenv</code> - Carga variables de entorno desde un .env archivar en process.env, permitiendo inyectar de forma segura credenciales como claves API y endpoints Elasticsearch.</p></li></ul><h3>Configuración de variables de entorno</h3><p>Crea un archivo <code>.env</code> en el directorio raíz de tu proyecto si aún no ves uno. Alternativamente, puedes copiar y renombrar el ejemplo <code>.env</code> que proporcioné en el <a href="https://github.com/jdarmada/getting-started-mastra-elastic/blob/main/.env.example">repositorio</a>. En este archivo, podemos agregar las siguientes variables:</p>ELASTICSEARCH_ENDPOINT="your-endpoint-here"
ELASTICSEARCH_API_KEY="your-key-here"
OPENAI_API_KEY="your-key-here"<p>Eso concluye la configuración básica. Desde aquí, ya puedes empezar a construir y orquestar agentes. Vamos un paso más allá y agregaremos Elasticsearch como la capa de almacenamiento y búsqueda vectorial.</p><h2>Agregar Elasticsearch como almacenamiento vectorial</h2><p>Crea una nueva carpeta llamada <code>stores</code> y dentro, agrega este <a href="https://github.com/jdarmada/getting-started-mastra-elastic/blob/main/src/mastra/stores/elastic-store.ts">archivo</a>. Antes de que Mastra y Elastic lanzaran una integración oficial de almacenamiento vectorial de Elasticsearch, <a href="https://github.com/abhiaiyer91">Abhi Aiyer</a>(CTO de Mestra) compartió esta clase prototipo temprana llamada <code>ElasticVector</code>. Simplemente, conecta la abstracción de memoria de Mamra con las densas capacidades vectoriales de Elasticsearch, para que los desarrolladores puedan incluir Elasticsearch como base de datos vectorial para sus agentes.</p><p>Echemos un vistazo más profundo a las partes importantes de la integración:</p><h3>Ingestión del cliente Elasticsearch</h3><p>Esta sección define la clase <code>ElasticVector</code> y configura la conexión cliente de Elasticsearch con soporte tanto para despliegues estándar como serverless.</p>export interface ElasticVectorConfig extends ClientOptions {
    /**
     * Explicitly specify if connecting to Elasticsearch Serverless.
     * If not provided, will be auto-detected on first use.
     */
    isServerless?: boolean;
    
    /**
     * Maximum documents to count accurately when describing indices.
     * Higher values provide accurate counts but may impact performance on large indices.
     * 
     * @default 10000
     */
    maxCountAccuracy?: number;
}

export class ElasticVector extends MastraVector {
    private client: Client;
    private isServerless: boolean | undefined;
    private deploymentChecked: boolean = false;
    private readonly maxCountAccuracy: number;

    constructor(config: ElasticVectorConfig) {
        super();
        this.client = new Client(config);
        this.isServerless = config.isServerless;
        this.maxCountAccuracy = config.maxCountAccuracy ?? 10000;
    }
}<ul><li><p><code>ElasticVectorConfig extends ClientOptions</code>: Esto crea una nueva interfaz de configuración que hereda todas las opciones del cliente de Elasticsearch (como <code>node</code>, <code>auth</code>, <code>requestTimeout</code>) y agrega nuestras propiedades personalizadas. Esto significa que los usuarios pueden pasar cualquier configuración válida de Elasticsearch junto con nuestras opciones específicas para serverless.</p></li><li><p><code>extends MastraVector</code>: Esto permite <code>ElasticVector</code> heredar de la clase base de <code>MastraVector</code> de Mastra, que es una interfaz común a la que se ajustan todas las integraciones de almacenamiento vectorial. Esto garantiza que Elasticsearch se comporte como cualquier otro backend de vectores Mastra desde la perspectiva del agente.</p></li><li><p><code>private client: Client</code>: Esta es una propiedad privada que contiene una instancia del cliente JavaScript Elasticsearch. Esto permite que la clase hable directamente con tu grupo.</p></li><li><p><code>isServerless</code> y <code>deploymentChecked</code>: Estas propiedades trabajan juntas para detectar y almacenar en caché si estamos conectados a un despliegue serverless o estándar de Elasticsearch. Esta detección ocurre automáticamente en el primer uso, o puede configurar explícitamente.</p></li><li><p><code>constructor(config: ClientOptions)</code>: Este constructor toma un objeto de configuración (que contiene tus credenciales de Elasticsearch y configuraciones opcionales de serverless) y lo emplea para inicializar el cliente en la línea <code>this.client = new Client(config)</code>.</p></li><li><p><code>super()</code>: Esto llama constructor base de Mastra, por lo que hereda el registro, los asistentes de validación y otros ganchos internos.</p></li></ul><p>En este punto, Mastra sabe que hay un nuevo almacén vectorial llamado <code>ElasticVector</code></p><h3>Detección del tipo de despliegue</h3><p>Antes de crear índices, el adaptador detecta automáticamente si estás usando Elasticsearch estándar o Elasticsearch Serverless. Esto es importante porque los despliegues serverless no permiten la configuración manual de shards.</p>private async detectServerless(): Promise&lt;boolean&gt; {
    // Return cached result if already detected
    if (this.deploymentChecked) {
        return this.isServerless ?? false;
    }

    // Use explicit configuration if provided
    if (this.isServerless !== undefined) {
        this.deploymentChecked = true;
        this.logger?.info(
            `Using explicit deployment type: ${this.isServerless ? 'Serverless' : 'Standard'}`
        );
        return this.isServerless;
    }

    try {
        const info = await this.client.info();
        
        // Primary detection: build flavor (most reliable)
        const isBuildFlavorServerless = info.version?.build_flavor === 'serverless';
        
        // Secondary detection: tagline (fallback)
        const isTaglineServerless = info.tagline?.toLowerCase().includes('serverless') ?? false;
        
        this.isServerless = isBuildFlavorServerless || isTaglineServerless;
        this.deploymentChecked = true;
        
        this.logger?.info(
            `Auto-detected ${this.isServerless ? 'Serverless' : 'Standard'} Elasticsearch deployment`,
            { 
                buildFlavor: info.version?.build_flavor, 
                version: info.version?.number,
                detectionMethod: isBuildFlavorServerless ? 'build_flavor' : 'tagline'
            }
        );
        
        return this.isServerless;
    } catch (error) {
        this.logger?.warn(
            'Could not auto-detect deployment type, assuming Standard Elasticsearch. ' +
            'Set isServerless: true explicitly in config if using Serverless.',
            { error: error instanceof Error ? error.message : String(error) }
        );
        this.isServerless = false;
        this.deploymentChecked = true;
        return false;
    }
}<p>Qué pasa:</p><ul><li><p>Primero comprueba si pusiste explícitamente <code>isServerless</code> en la configuración (se salta la auto-detección).</p></li><li><p>Llama a la API <code>info()</code> de Elasticsearch para obtener información del clúster</p></li><li><p>Comprueba el <code>build_flavor field</code> (los despliegues serverless devuelven <code>serverless</code>)</p></li><li><p>Vuelve a revisar el lema si no hay variedad de build disponible</p></li><li><p>Almacena en caché el resultado para evitar llamadas repetidas a la API</p></li><li><p>Por defecto se aplica al despliegue estándar si falla la detección</p></li></ul><p> Ejemplo de uso:</p>// Option 1: Auto-detect (recommended)
const vector = new ElasticVector({
    node: 'https://your-cluster.es.cloud',
    auth: { apiKey: 'your-api-key' }
});
// Detection happens automatically on first index operation

// Option 2: Explicit configuration (faster startup)
const vector = new ElasticVector({
    node: 'https://your-serverless.es.cloud',
    auth: { apiKey: 'your-api-key' },
    isServerless: true  // Skips auto-detection
});<h3>Creación del almacén de "memoria" en Elasticsearch</h3><p>La función siguiente establece un índice Elasticsearch para almacenar incrustaciones. Comprueba si el índice ya existe. Si no, crea uno con el mapeo que aparece abajo y contiene un campo <code>dense_vector</code> para almacenar incrustaciones y métricas de similitud personalizadas.</p><p>Algunas cosas a tener en cuenta:</p><ul><li><p>El parámetro <code>dimension</code> es la longitud de cada vector de incrustación, que depende del modelo de incrustación que estés usando. En nuestro caso, generaremos incrustaciones usando el modelo <code>text-embedding-3-small</code> de OpenAI, que genera vectores de tamaño <code>1536</code>. Usaremos esto como nuestro valor por defecto.</p></li><li><p>La variable <code>similarity</code> empleada en el mapeo a continuación se define a partir de la función auxiliar c<code>onst similarity = this.mapMetricToSimilarity(metric)</code>, que toma el valor del parámetro <code>metric</code> y lo convierte en una palabra clave compatible con Elasticsearch para la métrica de distancia elegida.</p><ul><li><p>Por ejemplo: Mastra emplea términos generales para similitud vectorial como <code>cosine</code>, <code>euclidean</code>, y <code>dotproduct</code>. Si pasáramos la métrica <code>euclidean</code> directamente al mapeo de Elasticsearch, generaría un error porque Elasticsearch espera que la palabra clave <code>l2_norm</code> represente la distancia euclidiana.</p></li></ul></li><li><p>Compatibilidad sin servidor: El código omite automáticamente los ajustes de shard y réplica para despliegues sin servidor, ya que estos son gestionados automáticamente por Elasticsearch Serverless.</p></li></ul>async createIndex(params: CreateIndexParams): Promise&lt;void&gt; {
    const { indexName, dimension = 1536, metric = 'cosine' } = params;

    try {
        const exists = await this.client.indices.exists({ index: indexName });

        if (exists) {
            try {
                await this.validateExistingIndex(indexName, dimension, metric);
                this.logger?.info(`Index "${indexName}" already exists and is valid`);
                return;
            } catch (validationError) {
                throw new Error(
                    `Index "${indexName}" exists but does not match the required configuration: ${
                        validationError instanceof Error ? validationError.message : String(validationError)
                    }`
                );
            }
        }

        const isServerless = await this.detectServerless();
        const similarity = this.mapMetricToSimilarity(metric);

        const indexConfig: any = {
            index: indexName,
            mappings: {
                properties: {
                    vector: {
                        type: 'dense_vector',
                        dims: dimension,
                        index: true,
                        similarity: similarity,
                    },
                    metadata: {
                        type: 'object',
                        enabled: true,
                        dynamic: true, // Allows flexible metadata structures
                    },
                },
            },
        };

        // Only configure shards/replicas for non-serverless deployments
        // Serverless manages infrastructure automatically
        if (!isServerless) {
            indexConfig.settings = {
                number_of_shards: 1,
                number_of_replicas: 0, // Increase for production HA deployments
            };
        }

        await this.client.indices.create(indexConfig);

        this.logger?.info(
            `Created ${isServerless ? 'Serverless' : 'Standard'} Elasticsearch index "${indexName}"`,
            { dimension, metric, similarity }
        );
    } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        this.logger?.error(`Failed to create index "${indexName}": ${errorMessage}`);
        throw new Error(`Failed to create index "${indexName}": ${errorMessage}`);
    }
}<h3>Almacenar una nueva recordación o nota tras una interacción</h3><p>Esta función toma nuevas incrustaciones generadas tras cada interacción, junto con los metadatos, y luego las inserta o actualiza en el índice usando la API <code>bulk</code> de Elastic. La API <code>bulk</code> agrupa múltiples operaciones de escritura en una sola solicitud; Esta mejora en nuestro rendimiento de indexación garantiza que las actualizaciones se mantengan eficientes a medida que la memoria de nuestro agente sigue creciendo.</p>async upsert(params: UpsertVectorParams): Promise&lt;string[]&gt; {
    const { indexName, vectors, metadata = [], ids } = params;

    try {
        // Generate unique IDs if not provided
        const vectorIds = ids || vectors.map((_, i) =&gt; 
            `vec_${Date.now()}_${i}_${Math.random().toString(36).substr(2, 9)}`
        );

        const operations = vectors.flatMap((vec, index) =&gt; [
            { index: { _index: indexName, _id: vectorIds[index] } },
            {
                vector: vec,
                metadata: metadata[index] || {},
            },
        ]);

        const response = await this.client.bulk({
            refresh: true,
            operations,
        });

        if (response.errors) {
            const erroredItems = response.items.filter((item: any) =&gt; item.index?.error);
            const erroredIds = erroredItems.map((item: any) =&gt; item.index?._id);
            const errorDetails = erroredItems.slice(0, 3).map((item: any) =&gt; ({
                id: item.index?._id,
                error: item.index?.error?.reason || item.index?.error,
                type: item.index?.error?.type
            }));
            
            const errorMessage = `Failed to upsert ${erroredIds.length}/${vectors.length} vectors`;
            console.error(`${errorMessage}. Sample errors:`, JSON.stringify(errorDetails, null, 2));
            this.logger?.error(errorMessage, { 
                failedCount: erroredIds.length, 
                totalCount: vectors.length,
                sampleErrors: errorDetails 
            });
            
            // Still return successfully inserted IDs
            const successfulIds = vectorIds.filter((id, idx) =&gt; 
                !erroredIds.includes(id)
            );
            
            if (successfulIds.length === 0) {
                throw new Error(`${errorMessage}. All operations failed. See logs for details.`);
            }
            
            return successfulIds;
        }

        this.logger?.info(`Successfully upserted ${vectors.length} vectors to "${indexName}"`);
        return vectorIds;
    } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        this.logger?.error(`Failed to upsert vectors to "${indexName}": ${errorMessage}`);
        throw new Error(`Failed to upsert vectors to "${indexName}": ${errorMessage}`);
    }
}<h3>Consulta de vectores similares para la recuperación semántica</h3><p>Esta función es el núcleo de la característica de recuperación semántica. El agente emplea búsqueda vectorial para encontrar incrustaciones almacenadas similares dentro de nuestro índice.</p>async query(params: QueryVectorParams&lt;any&gt;): Promise&lt;QueryResult[]&gt; {
    const { indexName, queryVector, topK = 10, filter, includeVector = false } = params;

    try {
        const knnQuery: any = {
            field: 'vector',
            query_vector: queryVector,
            k: topK,
            num_candidates: Math.max(topK * 10, 100), // Search more candidates for better recall
        };

        // Apply metadata filters if provided
        if (filter) {
            knnQuery.filter = this.buildElasticFilter(filter);
        }

        const sourceFields = ['metadata'];
        if (includeVector) {
            sourceFields.push('vector');
        }

        const response = await this.client.search({
            index: indexName,
            knn: knnQuery,
            size: topK,
            _source: sourceFields,
        });

        const results = response.hits.hits.map((hit: any) =&gt; ({
            id: hit._id,
            score: hit._score || 0,
            metadata: hit._source?.metadata || {},
            vector: includeVector ? hit._source?.vector : undefined,
        }));

        this.logger?.debug(`Query returned ${results.length} results from "${indexName}"`);
        return results;
    } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        this.logger?.error(`Failed to query vectors from "${indexName}": ${errorMessage}`);
        throw new Error(`Failed to query vectors from "${indexName}": ${errorMessage}`);
    }
}<p>Bajo el capó:</p><ul><li><p>Ejecuta una consulta <a href="https://www.elastic.co/docs/solutions/search/vector/knn">kNN</a> (k-vecinos más cercanos) usando la API <code>knn</code> en Elasticsearch.</p></li><li><p>Recupera los vectores top-K similares al vector de consulta de entrada.</p></li><li><p>Opcionalmente, aplica filtros de metadatos para reducir resultados (por ejemplo, buscar solo dentro de una categoría o rango de tiempo específico)</p></li><li><p>Devuelve resultados estructurados que incluyen el ID del documento, el puntaje de similitud y los metadatos almacenados.</p></li></ul><h2>Creación del agente del conocimiento</h2><p>Ahora que vimos la conexión entre Mastra y Elasticsearch a través de la integración <code>ElasticVector</code> , creemos el propio Knowledge Agent.</p><p>Dentro de la carpeta <code>agents</code>, crea un archivo llamado <code>knowledge-agent.ts</code>. Podemos empezar conectando nuestras variables de entorno e inicializando el cliente Elasticsearch.</p>import { Agent } from '@mastra/core/agent';
import { Memory } from '@mastra/memory';
import { openai } from '@ai-sdk/openai';
import { Client } from '@elastic/elasticsearch';
import { ElasticVector } from '../stores/elastic-store';
import dotenv from "dotenv";

dotenv.config();

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

//Error check for undefined credentials
if (!ELASTICSEARCH_ENDPOINT || !ELASTICSEARCH_API_KEY) {
  throw new Error('Missing Elasticsearch credentials');
}

//Check to see if a connection can be established
const testClient = new Client({
  node: ELASTICSEARCH_ENDPOINT,
  auth: { 
    apiKey: ELASTICSEARCH_API_KEY 
  },
});

try {
  await testClient.ping();
  console.log('Connected to Elasticsearch successfully');
} catch (error: unknown) {
  if (error instanceof Error) {
    console.error('Failed to connect to Elasticsearch:', error.message);
  } else {
    console.error('Failed to connect to Elasticsearch:', error);
  }
  process.exit(1);
}
//Initialize the Elasticsearch vector store
const vectorStore = new ElasticVector({
  node: ELASTICSEARCH_ENDPOINT,
  auth: {
    apiKey: ELASTICSEARCH_API_KEY,
  },
//Optional: Explicitly set to true if using Elasticsearch Serverless to skip auto-detection and improve startup time
//isServerless: true,
});<p>Aquí, nosotros:</p><ul><li><p>Usa <code>dotenv</code> para cargar nuestras variables desde nuestro archivo <code>.env</code> .</p></li><li><p>Comprueba si las credenciales de Elasticsearch se están inyectando correctamente y podemos establecer una conexión exitosa con el cliente.</p></li><li><p>Pasa el endpoint de Elasticsearch y la clave API al constructor <code>ElasticVector</code> para crear una instancia de nuestro almacén vectorial que definimos antes.</p></li><li><p>Opcionalmente, especifica <code>isServerless: true</code> si usas Elasticsearch Serverless. Esto omite el paso de detección automática y mejora el tiempo de arranque. Si se omite, el adaptador detectará automáticamente el tipo de despliegue en el primer uso.</p></li></ul><p>A continuación, podemos definir el agente usando la clase <code>Agent</code> de Mastra.</p>export const knowledgeAgent = new Agent({
    name: 'KnowledgeAgent',
    instructions: 'You are a helpful knowledge assistant.',
    model: openai('gpt-4o'),
    memory: new Memory({

        vector: vectorStore,

        //embedder used to create embeddings for each message
        embedder: 'openai/text-embedding-3-small',

        //set semantic recall options
        options: {
            semanticRecall: {
                topK: 3, // retrieve 3 similar messages
                messageRange: 2, // include 2 messages before/after each match
                scope: 'resource',
            },
        },
    }),
});<p>Los campos que podemos definir son:</p><ul><li><p><code>name</code> y <code>instructions</code>: Darle una identidad y función primaria.</p></li><li><p><code>model</code>: Estamos usando la <code>gpt-4o</code> de OpenAI a través del paquete <code>@ai-sdk/openai</code> .</p></li><li><p><code>memory</code>:</p><ul><li><p><code>vector</code>: Apunta a nuestra tienda Elasticsearch, así que los embeddings se almacenan y recuperan desde allí.</p></li><li><p><code>embedder</code>: Qué modelo usar para generar incrustaciones</p></li><li><p><code>semanticRecall</code> Las opciones deciden cómo funciona la retirada:</p><ul><li><p><code>topK</code>: Cuántos mensajes semánticamente similares recuperar.</p></li><li><p><code>messageRange</code>: Cuánto de la conversación incluir en cada partido.</p></li><li><p><code>scope</code>: Define el límite de la memoria.</p></li></ul></li></ul></li></ul><p>Casi termino. Solo tenemos que agregar este agente recién creado a nuestra configuración de Mestra. En el archivo llamado <a href="http://index.ts/"><code>index.ts</code></a>, importa el agente de conocimiento e insértalo en el campo <code>agents</code> .</p>export const mastra = new Mastra({
  agents: { knowledgeAgent },
  storage: new LibSQLStore({
    // stores observability, scores, ... into memory storage, if it needs to persist, change to file:../mastra.db
    url: ":memory:",
  }),
  logger: new PinoLogger({
    name: 'Mastra',
    level: 'info',
  }),
  telemetry: {
    // Telemetry is deprecated and will be removed in the Nov 4th release
    enabled: false, 
  },
  observability: {
    // Enables DefaultExporter and CloudExporter for AI tracing
    default: { enabled: true }, 
  },
});<p>Los otros campos incluyen:</p><ul><li><p><code>storage</code>: Este es el almacén interno de datos de Mamra para historial de ejecuciones, métricas de observabilidad, puntajes y cachés. Para más información sobre el almacenamiento de mastras, visita <a href="https://mastra.ai/docs/server-db/storage">aquí</a>.</p></li><li><p><code>logger</code>: Mastra emplea <a href="https://github.com/pinojs/pino">Pino</a>, que es un registrador JSON estructurado y ligero. Captura eventos como inicios y atajada de agentes, llamadas y resultados de herramientas, errores y tiempos de respuesta de los LLM.</p></li><li><p><code>observability</code>: Controla el rastreo de IA y la visibilidad de ejecución de los agentes. Sigue lo siguiente:</p><ul><li><p>Inicio/final de cada paso de razonamiento.</p></li><li><p>Qué modelo o herramienta se empleó.</p></li><li><p>Entradas y salidas.</p></li><li><p>Puntajes y evaluaciones</p></li></ul></li></ul><h3>Probando al agente con Mastra Studio</h3><p>¡Felicidades! Si llegaste hasta aquí, estás listo para ejecutar este agente y probar sus capacidades semánticas de recuperación. Por suerte, Mastra ofrece una interfaz de chat integrada para que no tengamos que crear la nuestra.</p><p>Para iniciar el servidor de desarrollo de Mestra, abre un terminal y ejecuta el siguiente comando:</p>npm run dev<p>Tras el empaquetado y el arranque inicial del servidor, debería proporcionarte una dirección del Playground.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte5f857fddc74ffc9/6a16f7b6a6c2b995d5e794c0/8b045f70008d26aec4d2e6b59d61085555b9c5b2-686x116.png" alt="Dirección del servidor para Playground" /><p>Pega esta dirección en tu navegador y te recibirás con Mastra Studio.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc7fdda6ce46ce068/6a16f7b7b0367d4f7672bacf/69bc80fe8486edd9e0cf91d87b39f465aeb23111-1600x438.png" alt="Pegar la dirección de Playground para acceder a Mastra Studio" /><p>Selecciona la opción de <code>knowledgeAgent</code> y charla sin parar.</p><p>Para una prueba rápida y ver si todo está correctamente cableado, dale información como: "El equipo anunció que el rendimiento de ventas en octubre subió un 12%, impulsado principalmente por renovaciones empresariales. El siguiente paso es ampliar su alcance a clientes de gama media." Después, inicia un nuevo chat y haz una pregunta como: "¿En qué segmento de clientes dijimos que debemos centrarnos a continuación?" El agente de conocimiento debería ser capaz de recordar la información que le diste en el primer chat. Deberías ver una respuesta como:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltfec3266e81a7213b/6a16f7b92b835f6f70f4afe2/da8ebddad89874023ed440a8f1ad2cb04ed043f4-1070x288.png" alt="Al charlar con un agente de conocimiento en Mastra Studio: el agente puede recordar información" /><p>Ver una respuesta así significa que el agente almacenó con éxito nuestro mensaje anterior como incrustaciones en Elasticsearch y lo recuperó después usando búsqueda vectorial.</p><h3>Inspección del almacenamiento de memoria a largo plazo del agente</h3><p>Ve a la pestaña <code>memory</code> en la configuración de tu agente en Mastra Studio. Esto te permite ver lo que tu agente aprendió con el tiempo. Cada mensaje, respuesta e interacción que se incrusta y almacena en Elasticsearch pasa a formar parte de esta memoria a largo plazo. Puedes buscar semánticamente en interacciones pasadas para encontrar rápidamente información o contexto recordado que el agente aprendió antes. Este es esencialmente el mismo mecanismo que emplea el agente durante la recuperación semántica, pero aquí puedes inspeccionarlo directamente. En nuestro ejemplo a continuación, buscamos el término "ventas" y recibimos cada interacción que incluyera algo relacionado con las ventas.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte428134d7bf2a43a/6a16f7bbb0367d185872bad3/3decaa0c332d288c5ae0b11c25f592c7d50c2f0f-1104x1320.png" alt="Cómo inspeccionar el almacenamiento de memoria a largo plazo de los agentes de conocimiento" /><h2>Conclusión</h2><p>Al conectar Mastra y Elasticsearch, podemos dar memoria a nuestros agentes, que es una capa clave en la ingeniería de contexto. Con la memoria semántica, los agentes pueden construir contexto con el tiempo, basando sus respuestas en lo que aprendieron. Eso significa interacciones más precisas, fiables y naturales.</p><p>Esta integración temprana es solo el punto de partida. El mismo patrón aquí puede permitir que los agentes de soporte recuerden tiquetes anteriores, bots internos que recuperen la documentación relevante o asistentes de IA que puedan recuperar detalles de los clientes en medio de una conversación. También estamos trabajando en una integración oficial de Mestra, haciendo que esta pareja sea aún más fluida en un futuro próximo.</p><p>Estamos deseando ver qué construyes a continuación. Pruébalo, explora <a href="https://mastra.ai/">Mastra</a> y sus funciones de memoria, y siéntete libre de compartir lo que descubras con la comunidad.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/knowledge-agent-semantic-recall-mastra-elasticsearch</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/knowledge-agent-semantic-recall-mastra-elasticsearch</guid>
    <category><![CDATA[AI agéntica]]></category>
    <category><![CDATA[Experiencia del desarrollador]]></category>
    <category><![CDATA[Integraciones]]></category>
    <dc:creator><![CDATA[JD Armada]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt09afdbff05603865/6a16f7bd839dfabbf2dcfcb5/b8d51c2726d5573385c9246a7821d12ade4f1b0e-720x420.jpg" length="0" type="image/jpeg"/>
    <pubDate>Thu, 06 Nov 2025 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>