Pular para o conteúdo principal

Documentation Index

Fetch the complete documentation index at: https://docs.mka1.com/llms.txt

Use this file to discover all available pages before exploring further.

Este guia avalia as APIs de busca do MKA1 em relação a um conjunto de dados padrão de recuperação de informações. Ele demonstra RAG com múltiplas tecnologias de armazenamento (Text Store e Tables) e processamento de documentos em alto volume — indexando milhares de documentos, executando consultas e medindo precisão e latência em cada etapa do pipeline. O benchmark utiliza o SciFact da suíte BEIR — 5.183 resumos científicos com 300 consultas de teste e julgamentos de relevância anotados por humanos. Todas as embeddings são geradas com meetkai:functionary-pt (4.096 dimensões).

Resumo dos resultados

Uma consulta de recuperação passa por duas etapas: inferência de embedding (convertendo o texto da consulta em um vetor) e busca no banco de dados (encontrando os vetores mais próximos no repositório). O tempo total de resposta é a soma de ambos.
MétricaText StoreTables
NDCG@1072.471.8
Recall@1083.182.6
MRR@1068.968.3
LatênciaText StoreTables
Inferência de embedding (por consulta)12 ms12 ms
Busca no banco p50 (lado do servidor)8 ms6 ms
Busca no banco p95 (lado do servidor)14 ms11 ms
Resposta ponta a ponta p50 (rede + embed + busca)45 ms38 ms
Resposta ponta a ponta p95 (rede + embed + busca)82 ms71 ms
Vazão de indexação (5.183 docs)182 docs/seg166 docs/seg
A latência de busca no banco de dados é reportada pelo próprio servidor (search_time_ms no corpo da resposta) e exclui a sobrecarga de rede e a inferência de embedding. O tempo de resposta ponta a ponta inclui tudo o que um cliente mediria. O restante deste guia mostra como esses números são produzidos, passo a passo.

Configuração

Instale as dependências e carregue as variáveis de ambiente.
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}`,
});

Passo 1 — Carregar o conjunto de dados SciFact

Baixe o corpus, as consultas e os julgamentos de relevância do 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 resumos científicos
const corpus = await fetchJsonl(
  'https://huggingface.co/datasets/BeIR/scifact/resolve/main/corpus.jsonl'
);

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

// Julgamentos de relevância: query_id → doc_id → relevância (binária)
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(`Julgamentos de relevância: ${Object.keys(qrels).length} consultas com rótulos`);
Corpus: 5183 documentos
Consultas: 300
Julgamentos de relevância: 300 consultas com rótulos

Passo 2 — Calcular embeddings

Gere embeddings para todos os documentos e consultas usando meetkai:functionary-pt. Processe em lotes para lidar com o volume.
async function embedBatch(texts: string[], model = 'meetkai:functionary-pt'): 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;

// Gerar embeddings do corpus
console.log('Gerando embeddings do 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 processados`);
  }
}

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

// Gerar embeddings das consultas e medir latência por consulta
console.log('Gerando embeddings das 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 das consultas: ${queryEmbeddings.length} consultas em ${queryEmbedMs.toFixed(0)} ms`);
console.log(`Inferência de embedding: ${perQueryEmbedMs.toFixed(1)} ms/consulta`);
Gerando embeddings do corpus...
  64 / 5183 documentos processados
  704 / 5183 documentos processados
  ...
Embedding do corpus: 42.3s (8.2 ms/doc)
Gerando embeddings das consultas...
Embedding das consultas: 300 consultas em 3600 ms
Inferência de embedding: 12.0 ms/consulta

Passo 3 — Avaliar o Text Store

Indexe todos os 5.183 documentos e execute todas as 300 consultas. Meça a vazão de indexação e a latência de busca.

Indexar documentos

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

// Criar 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 criado: ${TEXT_STORE_NAME}`);

// Indexar em 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(`Indexação: ${corpus.length} docs em ${(indexMs / 1000).toFixed(1)}s`);
console.log(`Vazão: ${(corpus.length / (indexMs / 1000)).toFixed(0)} docs/seg`);
Text store criado: scifact_bench_1774982400000
  Indexados 500 / 5183
  Indexados 1000 / 5183
  ...
  Indexados 5000 / 5183
Indexação: 5183 docs em 28.4s
Vazão: 182 docs/seg

Executar consultas e medir latência

Cada requisição de busca retorna um campo search_time_ms — a duração da busca no banco do lado do servidor, excluindo o tempo de ida e volta da rede e qualquer processamento upstream. Medimos tanto esse tempo do lado do servidor quanto o tempo total ponta a ponta observado pelo cliente.
const K = 10;
const results: Record<string, Record<string, number>> = {};
const endToEndLatencies: number[] = [];
const dbLatencies: number[] = [];

console.log(`Executando ${queries.length} consultas no 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 de volta para os IDs dos documentos do corpus comparando o 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); // pontuação por posição
    }
  }
}

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 — tempo de resposta ponta a ponta (${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 — latência de busca no banco (search_time_ms do 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`);
Executando 300 consultas no text store (top-10)...

Text Store — tempo de resposta ponta a ponta (300 consultas):
  p50: 45 ms
  p95: 82 ms
  p99: 110 ms

Text Store — latência de busca no banco (search_time_ms do servidor):
  p50: 8 ms
  p95: 14 ms
  p99: 18 ms
A diferença entre a latência ponta a ponta e a latência do banco de dados é o tempo de ida e volta da rede e a sobrecarga do gateway.

Passo 4 — Avaliar a API Tables

Indexe o mesmo corpus em um repositório Tables com esquema explícito e índice vetorial, depois execute as mesmas consultas.

Criar tabela 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(`Tabela criada: ${TABLE_NAME}`);

// Indexar em 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(`Indexação da tabela: ${corpus.length} docs em ${(tableIndexMs / 1000).toFixed(1)}s`);
console.log(`Vazão: ${(corpus.length / (tableIndexMs / 1000)).toFixed(0)} docs/seg`);
Tabela criada: scifact_table_1774982400000
  Indexados 500 / 5183
  ...
Indexação da tabela: 5183 docs em 31.2s
Vazão: 166 docs/seg

Executar consultas com busca vetorial

A API Tables também retorna searchTimeMs — a duração da busca no banco do lado do servidor.
const tableResults: Record<string, Record<string, number>> = {};
const tableEndToEnd: number[] = [];
const tableDbLatencies: number[] = [];

console.log(`Executando ${queries.length} consultas na tabela (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 — tempo de resposta ponta a ponta (${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 — latência de busca no banco (searchTimeMs do 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`);
Executando 300 consultas na tabela (vector_search, top-10)...

Tables — tempo de resposta ponta a ponta (300 consultas):
  p50: 38 ms
  p95: 71 ms
  p99: 95 ms

Tables — latência de busca no banco (searchTimeMs do servidor):
  p50: 6 ms
  p95: 11 ms
  p99: 15 ms

Passo 5 — Calcular métricas de precisão

Avalie a qualidade da recuperação usando métricas padrão do BEIR: NDCG@10, Recall@10 e 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 score decrescente, pegar 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('=== Precisão da Recuperação (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('=== Quebra de Latência ===');
console.log('');
console.log(`Etapa                          Text Store       Tables`);
console.log(`Inferência de embedding (por q)    ${perQueryEmbedMs.toFixed(0)} ms            ${perQueryEmbedMs.toFixed(0)} ms`);
console.log(`Busca no BD p50 (servidor)    ${percentile(dbLatencies, 0.5).toFixed(0)} ms             ${percentile(tableDbLatencies, 0.5).toFixed(0)} ms`);
console.log(`Busca no BD p95 (servidor)    ${percentile(dbLatencies, 0.95).toFixed(0)} ms            ${percentile(tableDbLatencies, 0.95).toFixed(0)} ms`);
console.log(`Resposta ponta a ponta p50        ${percentile(endToEndLatencies, 0.5).toFixed(0)} ms            ${percentile(tableEndToEnd, 0.5).toFixed(0)} ms`);
console.log(`Resposta ponta a ponta p95        ${percentile(endToEndLatencies, 0.95).toFixed(0)} ms            ${percentile(tableEndToEnd, 0.95).toFixed(0)} ms`);
console.log(`Vazão de indexação               ${(corpus.length / (indexMs / 1000)).toFixed(0)} docs/s        ${(corpus.length / (tableIndexMs / 1000)).toFixed(0)} docs/s`);
=== Precisão da Recuperação (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

=== Quebra de Latência ===

Etapa                          Text Store       Tables
Inferência de embedding (por q)    12 ms            12 ms
Busca no BD p50 (servidor)    8 ms             6 ms
Busca no BD p95 (servidor)    14 ms            11 ms
Resposta ponta a ponta p50        45 ms            38 ms
Resposta ponta a ponta p95        82 ms            71 ms
Vazão de indexação               182 docs/s       166 docs/s
  • Inferência de embedding é igual para ambos os backends — utiliza o mesmo modelo.
  • Busca no BD é o tempo puro de banco de dados reportado pelo servidor (search_time_ms / searchTimeMs). Aqui é onde Text Store e Tables diferem — Text Store executa busca híbrida (vetor + FTS), Tables executa busca vetorial pura.
  • Resposta ponta a ponta inclui ida e volta de rede, sobrecarga do gateway e busca no banco. A diferença entre ponta a ponta e busca no BD é a sobrecarga.

Passo 6 — Limpeza

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('Repositórios de benchmark limpos.');

Interpretando os resultados

Contexto de precisão

O leaderboard do BEIR reporta NDCG@10 no SciFact como referência:
ModeloNDCG@10
BM25 (baseline lexical)~66.5
text-embedding-ada-002~70–73
text-embedding-3-large~74–76
Estado da arte (2024)~78–80
Pontuações NDCG@10 na faixa de 70–76 indicam alta qualidade de recuperação, competitiva com os principais modelos de embedding.

O que observar

  • NDCG@10 é a principal métrica. Penaliza documentos relevantes que aparecem em posições inferiores.
  • Recall@10 mede quantos documentos relevantes aparecem entre os 10 primeiros — importante para pipelines RAG onde a geração depende da completude da recuperação.
  • MRR@10 mede quão rapidamente o primeiro resultado relevante aparece — importante para buscas voltadas ao usuário.
  • Text Store vs Tables: Text Store adiciona busca híbrida (vetor + palavra-chave) automaticamente. Tables oferece controle explícito sobre tipo de índice, métrica de distância e filtros.

Lendo a quebra de latência

Uma consulta de recuperação tem três componentes de latência:
  1. Inferência de embedding — tempo para converter o texto da consulta em um vetor. Isso é inferência de modelo e é igual independentemente do backend de armazenamento.
  2. Busca no banco de dados — tempo que o mecanismo de busca leva para encontrar os vizinhos mais próximos. Reportado pelo servidor em search_time_ms (Text Store) ou searchTimeMs (Tables). É o tempo puro de busca vetorial/híbrida sem sobrecarga de rede.
  3. Resposta ponta a ponta — o que o cliente observa: ida e volta de rede + roteamento do gateway + busca no banco.
Se a latência ponta a ponta for alta mas a busca no banco for rápida, o gargalo está na rede ou no gateway. Se a busca no banco for alta, considere um tipo de índice diferente (por exemplo, IVF_HNSW_SQ para busca aproximada mais rápida).

Comparação das tecnologias de armazenamento

Text StoreTables
ConfiguraçãoZero config — apenas nome + dimensãoEsquema explícito, tipos de campo, índices
BuscaHíbrida (vetor + texto livre) automáticaCompor operações: vector_search, FTS, filtro
Melhor paraPrototipagem rápida de RAG, busca híbrida prontaEsquemas customizados, busca filtrada, estratégias multi-índice
Tipos de índiceAutomáticoIVF_FLAT, IVF_PQ, IVF_HNSW_PQ, IVF_HNSW_SQ

Veja também