Inicio / Artículos / Pruebas de rendimiento de bases de datos vectoriales para búsquedas semánticas de alto rendimiento.

Pruebas de rendimiento de bases de datos vectoriales para búsquedas semánticas de alto rendimiento.

Aprenda una metodología práctica de Node.js y Python para realizar pruebas de rendimiento de bases de datos vectoriales bajo cargas realistas, a fin de tomar decisiones arquitectónicas y de escalado adecuadas.

3124 palabras

Desarrollar una búsqueda semántica de alto rendimiento implica someter a prueba las bases de datos vectoriales antes de decidirse por una. Esta guía orienta a ingenieros senior y arquitectos mediante una metodología práctica de pruebas de rendimiento que combina Node.js y Python para evaluar el rendimiento y tomar decisiones arquitectónicas acertadas.

Introducción y contexto industrial

A partir de 2026, las aplicaciones impulsadas por la IA —en particular aquellas desarrolladas con pipelines de generación mejorada mediante recuperación de información (RAG) y sistemas avanzados de recomendaciones— han convertido a las bases de datos vectoriales en un componente esencial de la infraestructura de cualquier plataforma de datos seria. Estos almacenes diseñados específicamente indexan y consultan embeddings de alta dimensión de manera eficiente, lo que permite una búsqueda semántica que va mucho más allá de las simples búsquedas por palabras clave. Ya sea que esté desarrollando un chatbot consciente del contexto, una herramienta inteligente de búsqueda de documentos o un recomendador de productos personalizado, la velocidad con la que puede mostrar elementos semánticamente relacionados influye directamente en la experiencia del usuario y en los resultados empresariales.

El problema es que el panorama de las bases de datos vectoriales está muy competitivo y evoluciona rápidamente. Puedes elegir entre plataformas diseñadas específicamente para este fin, como Pinecone, Weaviate y Qdrant, o entre funciones vectoriales integradas en bases de datos ya existentes como PostgreSQL (a través de pgvector) y Redis (a través de RediSearch). Esta abundancia de opciones plantea un verdadero desafío para los arquitectos: la decisión no se trata solo de determinar qué producto cuenta con la lista de funciones más impresionante, sino también de cómo se comporta cada opción bajo cargas reales, cuál es su costo para funcionar a gran escala y qué tan bien se adapta a un aumento en la demanda. A medida que aumentan el tráfico y el volumen de datos de tu aplicación, mantener la búsqueda semántica rápida y económica se convierte en un requisito esencial para cualquier sistema a gran escala. A continuación, se presenta una metodología para crear un entorno de pruebas riguroso en Node.js y Python, de modo que puedas tomar decisiones basadas en evidencias concretas y no en conjeturas.

El problema principal y su impacto en los negocios y la tecnología

La mayoría de los equipos se enfrentan al mismo problema al evaluar la infraestructura de bases de datos vectoriales: carecen de cifras objetivas de rendimiento específicas para sus cargas de trabajo. Confiar en pruebas de rendimiento publicadas por los proveedores o en comparaciones superficiales de características conduce a errores costosos. La provisión insuficiente se manifiesta en picos de latencia que empeoran la experiencia del usuario, aumentan las tasas de rebote y pueden traducirse directamente en pérdidas de ingresos para los productos orientados al cliente. Las herramientas internas también se ven afectadas: una búsqueda semántica lenta ralentiza a los ingenieros y retrasa la obtención de información importante en plazos ajustados. Por otro lado, la provisión excesiva por precaución agota el presupuesto destinado a la infraestructura, que podría haberse utilizado para otras prioridades.

La dificultad técnica aquí es considerable. Las bases de datos vectoriales realizan cálculos intensivos desde el punto de vista computacional para determinar similitudes —similitud coseno, producto escalar y métricas similares— en colecciones que pueden contener millones o miles de millones de vectores de alta dimensión. La eficiencia con la que se ejecutan estas operaciones depende de una serie de factores: la estrategia de indexación utilizada (HNSW, IVF, etc.), cómo están distribuidos los datos, la dimensionalidad de los vectores y el equilibrio entre inserciones intensivas y consultas frecuentes. Sin datos empíricos sobre el rendimiento de una base de datos específica en función de estos variables bajo condiciones similares a su tráfico real, básicamente se está haciendo conjeturas sobre el comportamiento en producción. Esas conjeturas conllevan riesgos reales para el negocio: clientes insatisfechos, incumplimiento de acuerdos de nivel de servicio, aumentos en los costos operativos y proyectos de inteligencia artificial estancados que no pueden escalar más allá de una fase de prueba. El objetivo de una base de datos adecuada

El objetivo de los ejercicios de benchmarking es reemplazar esa especulación por decisiones arquitectónicas basadas en datos.

Concepto arquitectónico y plano de solución

Para obtener resultados fiables de benchmark en llamadas de búsqueda semántica de alto rendimiento, se necesita una configuración estructurada que reproduzca cargas de trabajo realistas y capture una imagen completa del rendimiento. El plano que se muestra a continuación describe un sistema de benchmarking distribuido creado con herramientas conocidas de Node.js y Python. Está compuesto por las siguientes partes:

  1. Generador de datos: Este componente produce datos vectoriales sintéticos o carga un conjunto de datos existente. Por lo general, esto implica generar texto representativo, pasarlo por un modelo de incrustación de su elección (un modelo moderno de transformación de oraciones, text-embedding-3-large de OpenAI, o un modelo alojado localmente como Gemma), y formatear los vectores resultantes para su uso.
  2. Banco de datos en prueba (DUT): Se trata de la instancia real del banco de datos vectorial que está evaluando; puede ser un servicio gestionado como Pinecone o Weaviate Cloud, o una implementación autoalojada como Qdrant o Milvus que funciona en Kubernetes.
  • Cliente de ingestión (Node.js): Un servicio de Node.js que escribe los vectores generados y su metadatos en el DUT de la manera más eficiente posible. Debe gestionar el agrupamiento de operaciones, la lógica de intentos repetidos y, cuando sea relevante, las operaciones de escritura concurrentes.
  • Generador de carga (Python/Locust): Una herramienta de pruebas de carga basada en Python — Locust es una opción adecuada — que simula a muchos usuarios o servicios concurrentes realizando consultas de búsqueda semántica. Esta capa es responsable de reproducir patrones de consulta realistas, incluyendo variaciones en la complejidad y concurrencia de las consultas.
  • Cliente de consultas (Node.js/Python): El código que realmente se comunica con el DUT, enviando consultas y leyendo los resultados. Por lo general, se utiliza Node.js aquí para replicar un servicio web en producción y Python para trabajos de ciencia de datos o flujos de trabajo por lotes. Este cliente también se encarga de convertir el texto de las consultas en embeddings antes de enviar la solicitud de búsqueda.
  • Recopilador de métricas y monitoreo: Herramientas como Prometheus y Grafana, o el monitoreo integrado de un proveedor en la nube, para capturar los indicadores clave de rendimiento que son importantes: consultas por segundo (QPS), latencia promedio, latencias P90 y P99, tasas de error y consumo de recursos (CPU, memoria, E/S de disco) en el DUT.
  • Juntos, estos componentes le permiten identificar dónde se producen los cuellos de botella, comparar configuraciones entre bases de datos vectoriales y ver cómo cada una escala a medida que aumenta la carga. Dado que usted controla los datos de entrada, los patrones de consulta y el nivel de concurrencia, los resultados se convierten en orientaciones concretas para implementaciones en producción. Es importante medir tanto el rendimiento de ingestión como el desempeño de las consultas simultáneamente, ya que la mayoría de los sistemas en producción no solo sirven índices estáticos: constantemente ingieren nuevos vectores mientras gestionan el tráfico de búsquedas en tiempo real.

    Implementación paso a paso

    Para hacerlo más concreto, considere una configuración simplificada en la que Node.js se encarga de la ingestión de datos y un script en Python realiza las pruebas de carga contra el endpoint de consultas. Una abstracción genérica VectorDBClient permite que el ejemplo sea portable entre diferentes proveedores de bases de datos vectoriales.

    Comience por estructurar el proyecto Node.js:

    npm init -y
    npm install @xenova/transformers dotenv @pinecone-database/pinecone@2.2.0 # Or your chosen vector DB client
    mkdir src
    ​
    

    A continuación, cree un cliente de ingestión para Node.js encargado de generar embeddings y escribirlos en la base de datos. El ejemplo utiliza Xenova/transformers para calcular embeddings localmente, aunque también podría recurrir a una API de embeddings alojada como OpenAI o Cohere.

    // src/ingestionClient.js
    import { pipeline } from '@xenova/transformers';
    import { Pinecone } from '@pinecone-database/pinecone'; // Example client, replace with your DB client
    import 'dotenv/config'; // Loads .env file
    ​
    // Initialize embedding pipeline (using a local model)
    const embedder = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
    ​
    // --- Mock or Actual Vector DB Client Configuration ---
    // In a real scenario, you'd configure your specific vector DB client here.
    // For demonstration, let's assume a generic interface.
    class GenericVectorDBClient {
      constructor(config) {
        // Initialize your actual DB client (e.g., Pinecone, Weaviate, Qdrant)
        // For Pinecone:
        // this.pinecone = new Pinecone({ apiKey: config.apiKey, environment: config.environment });
        // this.index = this.pinecone.index(config.indexName);
        console.log(`Initialized generic vector DB client for index: ${config.indexName}`);
      }
    ​
      async upsert(vectors) {
        // Simulate upserting vectors to the database
        // In Pinecone: await this.index.upsert({ vectors });
        // console.log(`Upserted ${vectors.length} vectors.`);
        await new Promise(resolve => setTimeout(resolve, 10)); // Simulate network latency
        return { upsertedCount: vectors.length };
      }
    ​
      async query(queryVector, topK = 5) {
        // Simulate querying the database
        // In Pinecone: await this.index.query({ vector: queryVector, topK });
        await new Promise(resolve => setTimeout(resolve, 5)); // Simulate network latency
        return Array.from({ length: topK }, (_, i) => ({ id: `result-${i}`, score: Math.random() }));
      }
    }
    ​
    // --- Main Ingestion Logic ---
    async function runIngestion(numVectors = 1000, batchSize = 100) {
      const pineconeConfig = {
        apiKey: process.env.PINECONE_API_KEY || 'YOUR_API_KEY',
        environment: process.env.PINECONE_ENVIRONMENT || 'YOUR_ENVIRONMENT',
        indexName: process.env.PINECONE_INDEX_NAME || 'my-test-index',
      };
    ​
      const dbClient = new GenericVectorDBClient(pineconeConfig); // Use actual Pinecone client if needed
      console.log(`Starting ingestion of ${numVectors} vectors...`);
    ​
      let ingestedCount = 0;
      for (let i = 0; i < numVectors; i += batchSize) {
        const batch = [];
        for (let j = 0; j < batchSize && (i + j) < numVectors; j++) {
          const text = `This is a sample document for semantic search, item number ${i + j}.`;
          const embedding = await embedder(text, { pooling: 'mean', normalize: true });
          batch.push({
            id: `doc-${i + j}`,
            values: embedding.data, // Extract float32Array data
            metadata: { text: text, source: 'benchmark-data' }
          });
        }
    ​
        try {
          const result = await dbClient.upsert(batch);
          ingestedCount += result.upsertedCount; // Adjust based on your DB client's response
          console.log(`Batch ${i / batchSize + 1} ingested. Total: ${ingestedCount}`);
        } catch (error) {
          console.error(`Error during batch ingestion:`, error);
          // Implement robust retry logic in a production scenario
        }
      }
      console.log(`Ingestion complete. Total vectors: ${ingestedCount}`);
    }
    ​
    // Run the ingestion if this script is executed directly
    if (process.argv[1] === new URL(import.meta.url).pathname) {
      const count = parseInt(process.argv[2] || '10000', 10);
      const batch = parseInt(process.argv[3] || '100', 10);
      runIngestion(count, batch).catch(console.error);
    }
    ​
    

    Active la ejecución de ingestión de esta manera:

    node src/ingestionClient.js 10000 50 # Ingests 10,000 vectors in batches of 50
    ​
    

    Una vez cubierta la ingestión, configure la parte en Python para las pruebas de carga:

    pip install locust transformers sentence-transformers
    ​
    

    Finalmente, defina un archivo Locust que simule usuarios concurrentes realizando consultas contra el almacén de vectores. Este archivo imita la lógica de generación de embeddings del cliente de Node.js, pero está reimplementada en Python para que el generador de carga pueda crear vectores de consulta realistas por sí mismo.

    # locustfile.py
    import os
    import time
    import random
    from locust import HttpUser, task, between
    from sentence_transformers import SentenceTransformer # For generating query embeddings
    ​
    # --- Mock or Actual Vector DB Client Configuration ---
    # Replace with your actual vector database client and API calls
    class GenericVectorDBClient:
        def __init__(self, host, index_name, api_key):
            self.host = host
            self.index_name = index_name
            self.api_key = api_key
            # Initialize actual client here, e.g., Pinecone, Weaviate, Qdrant
            # For Pinecone:
            # from pinecone import Pinecone
            # self.pinecone = Pinecone(api_key=api_key, environment='YOUR_ENVIRONMENT')
            # self.index = self.pinecone.Index(index_name)
            print(f"Initialized generic vector DB client for {index_name} at {host}")
    ​
        def query(self, query_vector, top_k=5):
            # Simulate query to the database
            # In Pinecone: return self.index.query(vector=query_vector, top_k=top_k, include_metadata=False)
            time.sleep(0.005) # Simulate network and DB latency (5ms)
            return [{"id": f"sim-result-{random.randint(0, 10000)}", "score": random.random()} for _ in range(top_k)]
    ​
    # --- Embedding Model (load once for performance) ---
    # Using a local sentence transformer model
    # Make sure to run `python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('all-MiniLM-L6-v2')"` once to download
    MODEL_NAME = 'all-MiniLM-L6-v2'
    EMBEDDING_MODEL = SentenceTransformer(MODEL_NAME)
    ​
    def generate_embedding(text):
        return EMBEDDING_MODEL.encode(text, normalize_embeddings=True).tolist()
    ​
    # --- Locust User Definition ---
    class VectorSearchUser(HttpUser):
        wait_time = between(0.5, 2) # Simulate user think time
    ​
        host = "http://localhost:8000" # Or your API gateway if you have one
        # In a real scenario, this would be the actual vector DB endpoint
        # or a service endpoint that wraps the vector DB client.
    ​
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            # Using a direct client for demonstration. In production, this might be via an API.
            self.db_client = GenericVectorDBClient(
                host=os.getenv("VECTOR_DB_HOST", "localhost"),
                index_name=os.getenv("VECTOR_DB_INDEX", "my-test-index"),
                api_key=os.getenv("VECTOR_DB_API_KEY", "YOUR_API_KEY")
            )
            self.sample_queries = [
                "What are the latest AI advancements?",
                "How to optimize database queries?",
                "Best practices for cloud security in 2026?",
                "Explain quantum computing simply.",
                "Future of software development."
            ]
    ​
        @task(1)
        def search_vector_db(self):
            query_text = random.choice(self.sample_queries)
            query_vector = generate_embedding(query_text)
    ​
            start_time = time.time()
            try:
                results = self.db_client.query(query_vector, top_k=5)
                self.environment.events.request.fire(
                    request_type="VectorSearch",
                    name="/query_semantic",
                    response_time=(time.time() - start_time) * 1000, # in ms
                    response_length=len(str(results)),
                    exception=None,
                )
            except Exception as e:
                self.environment.events.request.fire(
                    request_type="VectorSearch",
                    name="/query_semantic",
                    response_time=(time.time() - start_time) * 1000,
                    response_length=0,
                    exception=e,
                )
                print(f"Error during query: {e}")
    ​
    

    Para ejecutar Locust, ejecute:

    locust -f locustfile.py --web-host localhost
    ​
    

    Luego dirija su navegador a http://localhost:8089 (o a la dirección que indique Locust) para iniciar la prueba de carga y observar los resultados en tiempo real. Antes de hacerlo, asegúrese de actualizar los valores de VECTOR_DB_HOST, VECTOR_DB_INDEX y VECTOR_DB_API_KEY; ya sea como variables de entorno o codificados directamente en el script, de modo que apunten a su verdadera implementación de la base de datos vectorial.

    Optimización del rendimiento y buenas prácticas

    Alcanzar una búsqueda semántica de alto rendimiento rara vez se trata solo de elegir la “mejor” base de datos vectorial y dar por finalizado el proceso. Se requiere prestar atención a varios niveles de la arquitectura al mismo tiempo. Las siguientes prácticas suelen ser las más importantes:

    1. Algoritmos de indexación y sus parámetros: La mayoría de las bases de datos vectoriales admiten múltiples estrategias de Vecino Más Cercano Aproximado (ANN); HNSW, IVF_FLAT y ScaNN son ejemplos comunes. Cada uno equilibra de manera diferente la velocidad de consulta, el recuerdo y el uso de memoria. Vale la pena experimentar con ajustes como M y efConstruction para HNSW, o nlist para IVF_FLAT, a fin de adaptarse a la estructura de sus datos y a sus requisitos de precisión. Aumentar el parámetro ef relacionado con el tiempo de búsqueda suele mejorar el recuerdo, pero a costa de una mayor latencia.
  • Dimensión de los embeddings: El tamaño de tus vectores afecta directamente tanto el espacio de almacenamiento como el costo computacional. Los embeddings más grandes pueden codificar detalles semánticos más ricos, pero también enfrentan la maldición de la dimensionalidad, lo que hace que la búsqueda de similitudes sea menos eficiente. Elige un modelo de embeddings que ofrezca el equilibrio adecuado para tu aplicación específica en lugar de optar por el más grande disponible.
  • Sharding y replicación: Cuando los conjuntos de datos crecen mucho, es necesario dividir el índice en varios shards para poder escalar horizontalmente. Las réplicas aumentan la tolerancia a fallos y te permiten manejar más tráfico de consultas. Define su tamaño según el número de consultas por segundo que esperes y cuánto tiempo de inactividad puedas soportar. Los servicios gestionados de bases de datos vectoriales suelen ocultar esta complejidad, pero conocer qué ocurre en el fondo sigue siendo útil al elegir un nivel de servicio.
  • Batching: Tanto la escritura de datos como su consulta se benefician al agrupar las operaciones en lugar de realizarlas una por una. El batching reduce los viajes de ida y vuelta por la red y permite que la base de datos procese las solicitudes de manera más eficiente. Esto es exactamente lo que hacía la lógica de batching en el ejemplo anterior de ingestión con Node.js.
  • Reutilización de conexiones: Abrir y cerrar repetidamente las conexiones con la base de datos genera un sobrecargo real. Utilice el pooling de conexiones en el lado del cliente, tanto en su código Node.js como en el de Python, para que las conexiones ya establecidas se reutilicen en lugar de crearse nuevamente, lo que mejora el rendimiento.
  • Hacer rápida la generación de embeddings: Calcular un embedding para una consulta recibida puede consumir una gran parte del tiempo total de respuesta. Para mantener esta velocidad a escala, considere almacenar en caché los embeddings de las consultas que se repiten con frecuencia, elegir un modelo de embedding más pequeño y rápido cuando los requisitos de precisión lo permitan, o trasladar la generación de embeddings a un servicio separado con escalabilidad horizontal, como una función sin servidor o un endpoint respaldado por GPU.
  • Vigilar el sistema de forma continua: Configure la supervisión de su base de datos vectorial utilizando herramientas como Prometheus, Grafana o cualquier otro instrumento de observabilidad que ofrezca su proveedor en la nube. Esté atento al número de solicitudes por segundo, a la latencia en los percentiles P90 y P99, a las tasas de error, al consumo de CPU y memoria, así como al crecimiento del tamaño del índice. Configure alertas para que cualquier desviación del comportamiento normal sea detectada antes de convertirse en un problema mayor.
  • Elegir el hardware o tamaño de instancia adecuado: Si lo aloja usted mismo, asigne suficiente CPU y memoria, prestando especial atención al almacenamiento; las SSD NVMe suelen marcar una gran diferencia. En el caso de servicios gestionados, elegir un nivel que se ajuste realmente a su carga de trabajo es lo que mantiene en equilibrio el costo y el rendimiento.
  • ROI empresarial y perspectivas futuras

    Una configuración de base de datos vectorial cuidadosamente probada y ajustada rinde frutos de varias maneras concretas. El primero es una mejor experiencia para los usuarios finales. Una búsqueda que devuelve resultados relevantes rápidamente se traduce en un mayor compromiso, una tasa de conversión más alta y clientes más satisfechos. En el comercio electrónico, esto se manifiesta en una mejor descubierta de productos; en las plataformas de contenido, significa recomendaciones que realmente se ajustan a las necesidades; en las herramientas de soporte, implica resoluciones más rápidas.

    La segunda ventaja es un gasto más eficiente. Una vez que se sabe con exactitud cómo se comporta una base de datos vectorial determinada bajo una carga de trabajo similar a tu tráfico real, se puede dimensionar la infraestructura con precisión en lugar de sobreprovisionar por precaución. Esa precisión a menudo se traduce directamente en costos en la nube más bajos, lo que deja más presupuesto disponible para otras prioridades.

    El tercero es una entrega más rápida de nuevas capacidades de IA. Un layer de búsqueda vectorial fiable permite a los equipos de ingeniería crear e implementar nuevas funciones con la certeza de que la infraestructura resistirá, en lugar de perder tiempo solucionando problemas de rendimiento a medida que aumenta el uso. Esto es aún más importante al acercarnos a 2026, ya que los modelos de acción a gran escala y los agentes de IA autónomos exigen que las arquitecturas RAG gestionen patrones de recuperación cada vez más exigentes y sofisticados.

    De cara al futuro, es probable que el ámbito de las bases de datos vectoriales siga convergiendo: las bases de datos de uso general seguirán incorporando funciones avanzadas de búsqueda vectorial, mientras que las bases de datos vectoriales especializadas adquirirán más capacidades relacionales y de tipo documento. Se espera un enfoque continuo en enfoques híbridos de indexación, búsquedas multimodales que combinen embeddings de texto, imagen y audio, y algoritmos ANN lo suficientemente eficientes como para manejar colecciones a escala de petabytes con tiempos de respuesta de submilisegundos. Los equipos que establezcan ahora una sólida disciplina en pruebas de rendimiento estarán en una buena posición para adoptar estos avances a medida que lleguen.

    Conclusión y puntos clave

    Elegir y ajustar una base de datos vectorial para cargas de trabajo de búsqueda semántica exigentes no es algo que se pueda hacer correctamente por intuición o confiando únicamente en el marketing del proveedor. Como ha demostrado esta guía, omitir pruebas rigurosas tiende a generar costosos problemas de escalabilidad y rendimiento con el tiempo. Crear su propio entorno de pruebas de rendimiento —Node.js para ingresar datos, Python con Locust para generar cargas realistas— proporciona a ingenieros y arquitectos evidencias concretas y específicas para cada tipo de carga de trabajo sobre cómo se comportará realmente una base de datos candidata en producción.

    Algunos puntos merecen ser tenidos en cuenta:

    • No hay nada que sustituya a las pruebas con su propia carga de trabajo. Las pruebas de rendimiento listas para usar son un punto de partida razonable, pero solo un entorno desarrollado a partir de sus datos reales, patrones de consulta y concurrencia le permitirá obtener la información que realmente necesita.
  • La optimización abarca todo el proceso, no solo la base de datos. La velocidad de generación integrada, el procesamiento por lotes en el cliente, la reutilización de conexiones y una supervisión adecuada contribuyen todos al rendimiento general.
  • Sopesa deliberadamente el costo frente al rendimiento. El objetivo no es alcanzar la mayor cantidad posible de consultas por segundo, sino comprender cómo se equilibran la latencia, el recuerdo y los gastos en infraestructura para tomar la decisión correcta según tu situación.
  • Sigue revisando tus elecciones. El panorama de las bases de datos vectoriales está en constante cambio, por lo que vale la pena ejecutar periódicamente tus pruebas de rendimiento y reevaluar el enfoque elegido a medida que evolucionan tu aplicación y las herramientas disponibles.
  • Aplicar estos principios de manera consistente coloca a su equipo en una posición sólida para desarrollar aplicaciones impulsadas por IA que sean escalables, económicas y realmente eficientes a medida que las demandas siguen creciendo.

    Lecturas relacionadas

  • Cómo realmente funcionan las bases de datos vectoriales: desde embeddings hasta la búsqueda híbrida — Explica cómo los embeddings codifican el significado, cómo escalan la búsqueda por similitud y el indexado, y cuándo la búsqueda híbrida y las bases de datos vectoriales son adecuadas para los sistemas de IA empresariales.
  • Ajustando índices HNSW y escalando la búsqueda vectorial para RAG en producción — Aprenda cómo los parámetros M y ef de HNSW equilibran la recuperación, la latencia y la memoria, cómo configurarlos en Chroma, y cuándo escalar una base de datos vectorial verticalmente o mediante sharding.