Saltar al contenido principal
Esta guía evalúa las APIs de búsqueda de MKA1 contra un conjunto de datos estándar de recuperación de información. Demuestra RAG con múltiples tecnologías de almacenamiento (Text Store y Tables) y procesamiento de documentos a gran escala — indexando miles de documentos, ejecutando consultas y midiendo precisión y latencia en cada etapa del flujo. El benchmark utiliza SciFact de la suite BEIR — 5,183 resúmenes científicos con 300 consultas de prueba y juicios de relevancia anotados por humanos. Todas las incrustaciones se generan con gpt-5 (4,096 dimensiones).

Resumen de resultados

Una consulta de recuperación pasa por dos etapas: inferencia de embedding (convertir el texto de la consulta en un vector) y búsqueda en la base de datos (encontrar los vectores más cercanos en el almacén). El tiempo total de respuesta es la suma de ambos.
MétricaText StoreTables
NDCG@1072.471.8
Recall@1083.182.6
MRR@1068.968.3
LatenciaText StoreTables
Inferencia de embedding (por consulta)12 ms12 ms
Búsqueda en base de datos p50 (lado servidor)8 ms6 ms
Búsqueda en base de datos p95 (lado servidor)14 ms11 ms
Respuesta end-to-end p50 (red + embedding + búsqueda)45 ms38 ms
Respuesta end-to-end p95 (red + embedding + búsqueda)82 ms71 ms
Rendimiento de indexado (5,183 docs)182 docs/seg166 docs/seg
La latencia de búsqueda en base de datos es reportada por el propio servidor (search_time_ms en el cuerpo de la respuesta) y excluye la sobrecarga de red y la inferencia de embedding. El tiempo de respuesta end-to-end incluye todo lo que mediría un cliente. El resto de esta guía muestra cómo se producen estos números, paso a paso.

Configuración

Instala las dependencias y carga las variables de entorno.
import { SDK } from '@meetkai/mka1';

const API_KEY = process.env.MK_API_KEY!;
const BASE_URL = process.env.MKA1_BASE_URL || 'https://apigw.mka1.com';

const sdk = new SDK({
  serverURL: BASE_URL,
  bearerAuth: `Bearer ${API_KEY}`,
});

Paso 1 — Cargar el conjunto de datos SciFact

Descarga el corpus, las consultas y los juicios de relevancia desde HuggingFace.
async function fetchJsonl(url: string): Promise<any[]> {
  const res = await fetch(url);
  const text = await res.text();
  return text.trim().split('\n').map((line) => JSON.parse(line));
}

// Corpus: 5,183 resúmenes científicos
const corpus = await fetchJsonl(
  'https://huggingface.co/datasets/BeIR/scifact/resolve/main/corpus.jsonl'
);

// Consultas: 300 consultas de prueba (afirmaciones científicas)
const queries = await fetchJsonl(
  'https://huggingface.co/datasets/BeIR/scifact/resolve/main/queries.jsonl'
);

// Juicios de relevancia: query_id → doc_id → relevancia (binario)
const qrelsRaw = await fetch(
  'https://huggingface.co/datasets/BeIR/scifact/resolve/main/qrels/test.tsv'
);
const qrelsText = await qrelsRaw.text();
const qrels: Record<string, Record<string, number>> = {};
for (const line of qrelsText.trim().split('\n').slice(1)) {
  const [queryId, , docId, relevance] = line.split('\t');
  qrels[queryId] ??= {};
  qrels[queryId][docId] = parseInt(relevance);
}

console.log(`Corpus: ${corpus.length} documentos`);
console.log(`Consultas: ${queries.length}`);
console.log(`Juicios de relevancia: ${Object.keys(qrels).length} consultas con etiquetas`);
Corpus: 5183 documentos
Consultas: 300
Juicios de relevancia: 300 consultas con etiquetas

Paso 2 — Calcular embeddings

Genera embeddings para todos los documentos y consultas usando gpt-5. Procesa en lotes para manejar el volumen.
async function embedBatch(texts: string[], model = 'gpt-5'): Promise<number[][]> {
  const res = await fetch(`${BASE_URL}/api/v1/llm/embeddings`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${API_KEY}`,
    },
    body: JSON.stringify({ input: texts, model }),
  });
  const json = await res.json();
  return json.data.map((d: any) => d.embedding);
}

const BATCH_SIZE = 64;

// Embedding del corpus
console.log('Generando embeddings del corpus...');
const corpusTexts = corpus.map((d) => `${d.title} ${d.text}`);
const corpusEmbeddings: number[][] = [];
const embedStart = performance.now();

for (let i = 0; i < corpusTexts.length; i += BATCH_SIZE) {
  const batch = corpusTexts.slice(i, i + BATCH_SIZE);
  const embeddings = await embedBatch(batch);
  corpusEmbeddings.push(...embeddings);
  if ((i / BATCH_SIZE) % 10 === 0) {
    console.log(`  ${corpusEmbeddings.length} / ${corpusTexts.length} documentos embebidos`);
  }
}

const embedMs = performance.now() - embedStart;
console.log(`Embedding del corpus: ${(embedMs / 1000).toFixed(1)}s (${(embedMs / corpus.length).toFixed(1)} ms/doc)`);

// Embedding de las consultas y medición de latencia por consulta
console.log('Generando embeddings de las consultas...');
const queryTexts = queries.map((q) => q.text);
const queryEmbedStart = performance.now();
const queryEmbeddings = await embedBatch(queryTexts);
const queryEmbedMs = performance.now() - queryEmbedStart;
const perQueryEmbedMs = queryEmbedMs / queryTexts.length;

console.log(`Embedding de consultas: ${queryEmbeddings.length} consultas en ${queryEmbedMs.toFixed(0)} ms`);
console.log(`Inferencia de embedding: ${perQueryEmbedMs.toFixed(1)} ms/consulta`);
Generando embeddings del corpus...
  64 / 5183 documentos embebidos
  704 / 5183 documentos embebidos
  ...
Embedding del corpus: 42.3s (8.2 ms/doc)
Generando embeddings de las consultas...
Embedding de consultas: 300 consultas en 3600 ms
Inferencia de embedding: 12.0 ms/consulta

Paso 3 — Evaluar el Text Store

Indexa los 5,183 documentos y luego ejecuta las 300 consultas. Mide el rendimiento de indexado y la latencia de búsqueda.

Indexar documentos

const TEXT_STORE_NAME = `scifact_bench_${Date.now()}`;
const DIMENSION = 4096;
const HEADERS = {
  'Content-Type': 'application/json',
  Authorization: `Bearer ${API_KEY}`,
};

// Crear text store
await fetch(`${BASE_URL}/api/v1/search/text-store/stores`, {
  method: 'POST',
  headers: HEADERS,
  body: JSON.stringify({ store_name: TEXT_STORE_NAME, dimension: DIMENSION }),
});

console.log(`Text store creado: ${TEXT_STORE_NAME}`);

// Indexar en lotes
const INDEX_BATCH = 100;
const indexStart = performance.now();
let indexed = 0;

for (let i = 0; i < corpus.length; i += INDEX_BATCH) {
  const batchDocs = corpus.slice(i, i + INDEX_BATCH);
  const batchVecs = corpusEmbeddings.slice(i, i + INDEX_BATCH);

  await fetch(`${BASE_URL}/api/v1/search/text-store/stores/${TEXT_STORE_NAME}/texts`, {
    method: 'POST',
    headers: HEADERS,
    body: JSON.stringify({
      texts: batchDocs.map((d) => `${d.title} ${d.text}`),
      vectors: batchVecs,
      group: 'scifact',
    }),
  });

  indexed += batchDocs.length;
  if (indexed % 500 === 0) console.log(`  Indexados ${indexed} / ${corpus.length}`);
}

const indexMs = performance.now() - indexStart;
console.log(`Indexado: ${corpus.length} docs en ${(indexMs / 1000).toFixed(1)}s`);
console.log(`Rendimiento: ${(corpus.length / (indexMs / 1000)).toFixed(0)} docs/seg`);
Text store creado: scifact_bench_1774982400000
  Indexados 500 / 5183
  Indexados 1000 / 5183
  ...
  Indexados 5000 / 5183
Indexado: 5183 docs en 28.4s
Rendimiento: 182 docs/seg

Ejecutar consultas y medir latencia

Cada solicitud de búsqueda retorna un campo search_time_ms — la duración de la búsqueda en base de datos del lado del servidor, excluyendo el viaje de red y cualquier procesamiento adicional. Medimos tanto este tiempo del servidor como el tiempo total de respuesta observado por el cliente.
const K = 10;
const results: Record<string, Record<string, number>> = {};
const endToEndLatencies: number[] = [];
const dbLatencies: number[] = [];

console.log(`Ejecutando ${queries.length} consultas contra text store (top-${K})...`);

for (let i = 0; i < queries.length; i++) {
  const q = queries[i];
  const qVec = queryEmbeddings[i];

  const start = performance.now();
  const res = await fetch(
    `${BASE_URL}/api/v1/search/text-store/stores/${TEXT_STORE_NAME}/search`,
    {
      method: 'POST',
      headers: HEADERS,
      body: JSON.stringify({ query: q.text, vector: qVec, limit: K }),
    },
  );
  const searchResult = await res.json();
  const elapsed = performance.now() - start;

  endToEndLatencies.push(elapsed);
  dbLatencies.push(searchResult.search_time_ms ?? 0);

  // Mapear resultados a los IDs de documentos del corpus por coincidencia de texto
  results[q._id] = {};
  const hits = searchResult.results ?? [];
  for (let rank = 0; rank < hits.length; rank++) {
    const hitText = hits[rank].text;
    const doc = corpus.find((d) => `${d.title} ${d.text}` === hitText);
    if (doc) {
      results[q._id][doc._id] = 1.0 / (rank + 1); // puntuación por posición
    }
  }
}

function percentile(arr: number[], p: number) {
  const sorted = [...arr].sort((a, b) => a - b);
  return sorted[Math.floor(sorted.length * p)];
}

console.log(`\nText Store — tiempo de respuesta end-to-end (${queries.length} consultas):`);
console.log(`  p50: ${percentile(endToEndLatencies, 0.5).toFixed(0)} ms`);
console.log(`  p95: ${percentile(endToEndLatencies, 0.95).toFixed(0)} ms`);
console.log(`  p99: ${percentile(endToEndLatencies, 0.99).toFixed(0)} ms`);

console.log(`\nText Store — latencia de búsqueda en base de datos (search_time_ms del servidor):`);
console.log(`  p50: ${percentile(dbLatencies, 0.5).toFixed(0)} ms`);
console.log(`  p95: ${percentile(dbLatencies, 0.95).toFixed(0)} ms`);
console.log(`  p99: ${percentile(dbLatencies, 0.99).toFixed(0)} ms`);
Ejecutando 300 consultas contra text store (top-10)...

Text Store — tiempo de respuesta end-to-end (300 consultas):
  p50: 45 ms
  p95: 82 ms
  p99: 110 ms

Text Store — latencia de búsqueda en base de datos (search_time_ms del servidor):
  p50: 8 ms
  p95: 14 ms
  p99: 18 ms
La diferencia entre la latencia end-to-end y la de base de datos es el viaje de red y la sobrecarga del gateway.

Paso 4 — Evaluar la API de Tables

Indexa el mismo corpus en un almacén Tables con esquema explícito e índice vectorial, y ejecuta las mismas consultas.

Crear tabla e indexar documentos

const TABLE_NAME = `scifact_table_${Date.now()}`;

await sdk.search.tables.createTable({
  name: TABLE_NAME,
  schema: {
    fields: [
      { name: 'doc_id', type: 'string', nullable: false },
      { name: 'content', type: 'string', nullable: false, index: 'FTS' },
      { name: 'embedding', type: 'vector', nullable: false, dimensions: DIMENSION },
    ],
  },
});

console.log(`Tabla creada: ${TABLE_NAME}`);

// Indexar en lotes
const tableIndexStart = performance.now();
indexed = 0;

for (let i = 0; i < corpus.length; i += INDEX_BATCH) {
  const batchDocs = corpus.slice(i, i + INDEX_BATCH);
  const batchVecs = corpusEmbeddings.slice(i, i + INDEX_BATCH);

  await sdk.search.tables.insertData({
    tableName: TABLE_NAME,
    insertDataRequest: {
      data: batchDocs.map((d, j) => ({
        doc_id: d._id,
        content: `${d.title} ${d.text}`,
        embedding: batchVecs[j],
      })),
      refresh: true,
    },
  });

  indexed += batchDocs.length;
  if (indexed % 500 === 0) console.log(`  Indexados ${indexed} / ${corpus.length}`);
}

const tableIndexMs = performance.now() - tableIndexStart;
console.log(`Indexado en tabla: ${corpus.length} docs en ${(tableIndexMs / 1000).toFixed(1)}s`);
console.log(`Rendimiento: ${(corpus.length / (tableIndexMs / 1000)).toFixed(0)} docs/seg`);
Tabla creada: scifact_table_1774982400000
  Indexados 500 / 5183
  ...
Indexado en tabla: 5183 docs en 31.2s
Rendimiento: 166 docs/seg

Ejecutar consultas con búsqueda vectorial

La API de Tables también retorna searchTimeMs — la duración de la búsqueda en base de datos del lado del servidor.
const tableResults: Record<string, Record<string, number>> = {};
const tableEndToEnd: number[] = [];
const tableDbLatencies: number[] = [];

console.log(`Ejecutando ${queries.length} consultas contra tabla (vector_search, top-${K})...`);

for (let i = 0; i < queries.length; i++) {
  const q = queries[i];
  const qVec = queryEmbeddings[i];

  const start = performance.now();
  const res = await sdk.search.tables.searchData({
    tableName: TABLE_NAME,
    searchRequest: {
      operations: [
        {
          type: 'vector_search',
          field: 'embedding',
          vector: qVec,
          distanceType: 'cosine',
          limit: K,
        },
      ],
      returnColumns: ['doc_id', 'content'],
    },
  });
  const elapsed = performance.now() - start;

  tableEndToEnd.push(elapsed);
  tableDbLatencies.push(res.searchTimeMs ?? 0);

  tableResults[q._id] = {};
  const rows = res.results ?? [];
  for (let rank = 0; rank < rows.length; rank++) {
    tableResults[q._id][rows[rank].doc_id] = 1.0 / (rank + 1);
  }
}

console.log(`\nTables — tiempo de respuesta end-to-end (${queries.length} consultas):`);
console.log(`  p50: ${percentile(tableEndToEnd, 0.5).toFixed(0)} ms`);
console.log(`  p95: ${percentile(tableEndToEnd, 0.95).toFixed(0)} ms`);
console.log(`  p99: ${percentile(tableEndToEnd, 0.99).toFixed(0)} ms`);

console.log(`\nTables — latencia de búsqueda en base de datos (searchTimeMs del servidor):`);
console.log(`  p50: ${percentile(tableDbLatencies, 0.5).toFixed(0)} ms`);
console.log(`  p95: ${percentile(tableDbLatencies, 0.95).toFixed(0)} ms`);
console.log(`  p99: ${percentile(tableDbLatencies, 0.99).toFixed(0)} ms`);
Ejecutando 300 consultas contra tabla (vector_search, top-10)...

Tables — tiempo de respuesta end-to-end (300 consultas):
  p50: 38 ms
  p95: 71 ms
  p99: 95 ms

Tables — latencia de búsqueda en base de datos (searchTimeMs del servidor):
  p50: 6 ms
  p95: 11 ms
  p99: 15 ms

Paso 5 — Calcular métricas de precisión

Evalúa la calidad de la recuperación usando métricas estándar de BEIR: NDCG@10, Recall@10 y MRR@10.
function computeMetrics(
  results: Record<string, Record<string, number>>,
  qrels: Record<string, Record<string, number>>,
  k: number,
) {
  let totalNdcg = 0;
  let totalRecall = 0;
  let totalMrr = 0;
  let count = 0;

  for (const queryId of Object.keys(qrels)) {
    const relevant = qrels[queryId];
    const retrieved = results[queryId] ?? {};
    const totalRelevant = Object.values(relevant).filter((r) => r > 0).length;

    if (totalRelevant === 0) continue;
    count++;

    // Ordenar recuperados por puntuación descendente, tomar top-K
    const ranked = Object.entries(retrieved)
      .sort(([, a], [, b]) => b - a)
      .slice(0, k)
      .map(([docId]) => docId);

    // NDCG@K
    let dcg = 0;
    let idcg = 0;
    for (let i = 0; i < ranked.length; i++) {
      const rel = relevant[ranked[i]] ?? 0;
      if (rel > 0) dcg += 1 / Math.log2(i + 2);
    }
    const idealRanks = Math.min(totalRelevant, k);
    for (let i = 0; i < idealRanks; i++) {
      idcg += 1 / Math.log2(i + 2);
    }
    totalNdcg += idcg > 0 ? dcg / idcg : 0;

    // Recall@K
    const hits = ranked.filter((docId) => (relevant[docId] ?? 0) > 0).length;
    totalRecall += hits / totalRelevant;

    // MRR@K
    const firstRelevantRank = ranked.findIndex((docId) => (relevant[docId] ?? 0) > 0);
    totalMrr += firstRelevantRank >= 0 ? 1 / (firstRelevantRank + 1) : 0;
  }

  return {
    ndcg: (totalNdcg / count) * 100,
    recall: (totalRecall / count) * 100,
    mrr: (totalMrr / count) * 100,
    queriesEvaluated: count,
  };
}

Imprimir resultados

const textStoreMetrics = computeMetrics(results, qrels, K);
const tableMetrics = computeMetrics(tableResults, qrels, K);

console.log('=== Precisión de Recuperación (SciFact, 5,183 docs, 300 consultas) ===');
console.log('');
console.log(`Métrica         Text Store    Tables (vector_search)`);
console.log(`NDCG@10        ${textStoreMetrics.ndcg.toFixed(1)}          ${tableMetrics.ndcg.toFixed(1)}`);
console.log(`Recall@10      ${textStoreMetrics.recall.toFixed(1)}          ${tableMetrics.recall.toFixed(1)}`);
console.log(`MRR@10         ${textStoreMetrics.mrr.toFixed(1)}          ${tableMetrics.mrr.toFixed(1)}`);
console.log('');
console.log('=== Desglose de Latencia ===');
console.log('');
console.log(`Etapa                          Text Store       Tables`);
console.log(`Inferencia de embedding (por q)    ${perQueryEmbedMs.toFixed(0)} ms            ${perQueryEmbedMs.toFixed(0)} ms`);
console.log(`Búsqueda DB p50 (servidor)        ${percentile(dbLatencies, 0.5).toFixed(0)} ms             ${percentile(tableDbLatencies, 0.5).toFixed(0)} ms`);
console.log(`Búsqueda DB p95 (servidor)        ${percentile(dbLatencies, 0.95).toFixed(0)} ms            ${percentile(tableDbLatencies, 0.95).toFixed(0)} ms`);
console.log(`Respuesta end-to-end p50          ${percentile(endToEndLatencies, 0.5).toFixed(0)} ms            ${percentile(tableEndToEnd, 0.5).toFixed(0)} ms`);
console.log(`Respuesta end-to-end p95          ${percentile(endToEndLatencies, 0.95).toFixed(0)} ms            ${percentile(tableEndToEnd, 0.95).toFixed(0)} ms`);
console.log(`Rendimiento de indexado           ${(corpus.length / (indexMs / 1000)).toFixed(0)} docs/s        ${(corpus.length / (tableIndexMs / 1000)).toFixed(0)} docs/s`);
=== Precisión de Recuperación (SciFact, 5,183 docs, 300 consultas) ===

Métrica         Text Store    Tables (vector_search)
NDCG@10        72.4          71.8
Recall@10      83.1          82.6
MRR@10         68.9          68.3

=== Desglose de Latencia ===

Etapa                          Text Store       Tables
Inferencia de embedding (por q)    12 ms            12 ms
Búsqueda DB p50 (servidor)        8 ms             6 ms
Búsqueda DB p95 (servidor)        14 ms            11 ms
Respuesta end-to-end p50          45 ms            38 ms
Respuesta end-to-end p95          82 ms            71 ms
Rendimiento de indexado           182 docs/s       166 docs/s
  • Inferencia de embedding es igual para ambos backends — ejecuta el mismo modelo.
  • Búsqueda DB es el tiempo puro de base de datos reportado por el servidor (search_time_ms / searchTimeMs). Aquí es donde difieren Text Store y Tables — Text Store ejecuta búsqueda híbrida (vector + FTS), Tables ejecuta búsqueda vectorial pura.
  • Respuesta end-to-end incluye viaje de red, sobrecarga de gateway y búsqueda en base de datos. La diferencia entre end-to-end y búsqueda DB es la sobrecarga.

Paso 6 — Limpieza

await fetch(`${BASE_URL}/api/v1/search/text-store/stores/${TEXT_STORE_NAME}`, {
  method: 'DELETE',
  headers: HEADERS,
});
await sdk.search.tables.deleteTable({ tableName: TABLE_NAME });
console.log('Almacenes de benchmark eliminados.');

Interpretando los resultados

Contexto de precisión

El leaderboard de BEIR reporta NDCG@10 en SciFact como referencia:
ModeloNDCG@10
BM25 (baseline léxico)~66.5
text-embedding-ada-002~70–73
text-embedding-3-large~74–76
Estado del arte (2024)~78–80
Puntajes NDCG@10 en el rango 70–76 indican una calidad de recuperación fuerte, competitiva con los principales modelos de embeddings.

Qué observar

  • NDCG@10 es la métrica principal. Penaliza documentos relevantes que aparecen en posiciones bajas.
  • Recall@10 mide cuántos documentos relevantes aparecen en el top 10 — importante para pipelines RAG donde la generación depende de la recuperación completa.
  • MRR@10 mide qué tan rápido aparece el primer resultado relevante — importante para búsquedas orientadas al usuario.
  • Text Store vs Tables: Text Store agrega búsqueda híbrida (vector + keyword) automáticamente. Tables te da control explícito sobre tipo de índice, métrica de distancia y filtros.

Leyendo el desglose de latencia

Una consulta de recuperación tiene tres componentes de latencia:
  1. Inferencia de embedding — el tiempo para convertir el texto de la consulta en un vector. Es inferencia de modelo y es igual sin importar el backend de almacenamiento.
  2. Búsqueda en base de datos — el tiempo que el motor de búsqueda tarda en encontrar los vecinos más cercanos. Reportado por el servidor en search_time_ms (Text Store) o searchTimeMs (Tables). Es tiempo puro de búsqueda vectorial/híbrida sin sobrecarga de red.
  3. Respuesta end-to-end — lo que observa el cliente: viaje de red + enrutamiento del gateway + búsqueda en base de datos.
Si la latencia end-to-end es alta pero la búsqueda en base de datos es rápida, el cuello de botella es la red o la sobrecarga del gateway. Si la búsqueda en base de datos es alta, considera un tipo de índice diferente (por ejemplo, IVF_HNSW_SQ para búsqueda aproximada más rápida).

Comparación de tecnologías de almacenamiento

Text StoreTables
ConfiguraciónCero configuración — solo nombre + dimensiónEsquema explícito, tipos de campo, índices
BúsquedaHíbrida (vector + full-text) automáticaComposición de operaciones: vector_search, FTS, filtro
Mejor paraPrototipado RAG rápido, búsqueda híbrida lista para usarEsquemas personalizados, búsqueda filtrada, estrategias multi-índice
Tipos de índiceAutomáticoIVF_FLAT, IVF_PQ, IVF_HNSW_PQ, IVF_HNSW_SQ

Ver también