EN ES PT

Motor de Busca
Sub-Milissegundo em Rust

Como construimos um motor de busca hibrido BM25 + semantico que processa 27K queries/seg com latencia p99 sub-milissegundo em um unico nodo. Arquitetura, benchmarks reais e licoes aprendidas.

<1ms
Latencia p99
27K
Queries/seg
21K
Docs/seg Indexacao
766B
Por Documento

O Problema

A maioria das equipes recorre ao Elasticsearch ou Solr quando precisa de busca full-text. Sao ferramentas solidas, mas tem um custo: overhead da JVM, pausas de GC, complexidade de cluster e latencias de consulta que tipicamente ficam na faixa de 5–50ms para queries simples. Para muitas aplicacoes, isso e aceitavel.

Nos precisavamos de algo diferente. Nossos requisitos: latencia sub-milissegundo no p99, ranking hibrido combinando relevancia BM25 com similaridade semantica, e deploy como um unico binario — sem JVM, sem coordenador de cluster, sem YAML.

A resposta foi Rust.

Arquitetura Geral

O motor e estruturado como um conjunto de crates Rust modulares, cada um responsavel por uma unica preocupacao. A camada de API e Axum + Tokio. A camada de indexacao e recuperacao e construida sobre Tantivy. A busca semantica passa pelo Qdrant, e os resultados de ambos os caminhos sao fundidos via Reciprocal Rank Fusion (RRF).

API
Axum HTTP Query Parser Auth Middleware
Busca
BM25 (Tantivy) Semantica (Qdrant) Fusao RRF
Indice
Indice Invertido Vector Store Merge de Segmentos
Storage
Arquivos mmap Redis Cache Commit Log

Por que Tantivy

Tantivy e uma biblioteca de busca full-text escrita em Rust, inspirada no Apache Lucene. Diferente do Elasticsearch (que envolve o Lucene em uma camada JVM), Tantivy compila para codigo nativo com zero garbage collection. Nos da controle direto sobre o layout de memoria, o merge de segmentos e os pipelines de tokenizacao.

A vantagem chave: o IndexReader do Tantivy usa segmentos memory-mapped com compartilhamento baseado em Arc. Multiplas threads de busca leem da mesma memoria mapeada sem copiar ou travar. Por isso o throughput concorrente escala quase linearmente.

Ranking Hibrido com RRF

Combinamos dois sinais de ranking:

Estes sao fundidos usando Reciprocal Rank Fusion, que combina listas ranqueadas sem exigir normalizacao de scores. O algoritmo e O(n) no numero de resultados e adiciona overhead desprezivel — menos de 1ms mesmo para 1.000 resultados.

// Fusao RRF — merge O(n) de listas ranqueadas
fn rrf_fuse(bm25: &[DocScore], semantic: &[DocScore], k: f32) -> Vec<DocScore> {
    let mut scores: HashMap<DocId, f32> = HashMap::new();

    for (rank, doc) in bm25.iter().enumerate() {
        *scores.entry(doc.id).or_default() += 1.0 / (k + rank as f32 + 1.0);
    }
    for (rank, doc) in semantic.iter().enumerate() {
        *scores.entry(doc.id).or_default() += 1.0 / (k + rank as f32 + 1.0);
    }

    let mut fused: Vec<_> = scores.into_iter().collect();
    fused.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
    fused
}

Resultados dos Benchmarks

Todos os benchmarks foram executados em macOS x86_64, build release, com Criterion para validade estatistica. O corpus consiste em documentos sinteticos (conteudo educacional, metadados de musicas, registros SCADA) que refletem nosso perfil de carga em producao.

Throughput de Indexacao

Com corpus pequenos, o custo de setup (criacao de diretorio, alocacao do writer de 50MB) domina. Conforme o corpus cresce, o throughput se estabiliza em 21K docs/seg. Os modos serial e batch tem desempenho identico — a tokenizacao e I/O interna do Tantivy e o verdadeiro gargalo, nao o overhead de locks.

Tamanho do CorpusMetodoTempoDocs/seg
1Kserial2.41s416
1Kbatch2.37s422
10Kserial2.43s4.117
10Kbatch2.52s3.963
50Kserial2.51s19.881
50Kbatch2.64s18.913
100Kserial4.76s21.006
100Kbatch5.08s19.690

Latencia de Consultas

O numero chave: a latencia p99 fica abaixo de 1ms mesmo com 50K documentos. A latencia mediana com 50K docs e 234 microssegundos. Para contexto, uma query simples tipica do Elasticsearch em um corpus comparavel retorna em 5–15ms — aproximadamente 20–50x mais lento.

Corpusp50p95p99Media
1K119us296us335us136us
10K131us294us537us160us
50K234us712us984us290us
Por que microssegundos importam

Com 984us no p99, sua query mais lenta (1 em cada 100) ainda fica abaixo de um milissegundo. Isso significa que voce pode adicionar processamento extra — re-ranking, personalizacao, logica de teste A/B — e ainda retornar resultados em menos de 5ms no total. Esse e o orcamento que o Elasticsearch usa apenas para a query de busca.

Throughput Concorrente

O throughput escala quase linearmente com workers concorrentes gracas a arquitetura lock-free do IndexReader do Tantivy. Com 100 workers concorrentes, sustentamos 27.287 queries por segundo em um unico nodo.

WorkersTotal QueriesTempo TotalQueries/seg
110020.9ms4.782
101.00051.1ms19.566
505.000211.9ms23.600
10010.000366.5ms27.287

Performance do Query Parser

O query parser processa termos simples, queries de frase, negacao e expressoes booleanas complexas — tudo abaixo de 103 microssegundos. Esse e overhead pre-busca; executa antes de tocar o indice.

Tipo de QueryExemploLatencia Media
Simplesfracciones matematicas76.7us
Frase"tabla periodica" quimica74.0us
Negacaovolcanes -tectonica ciencias102.6us
Complexa"sistema solar" -pluton type:educational85.2us

Fusao de Ranking RRF

A etapa de ranking hibrido adiciona overhead desprezivel. Mesmo fundindo 1.000 resultados de ambos os pipelines BM25 e semantico leva menos de 650 microssegundos.

Resultados para FundirLatencia Media
106.0us
10058.3us
500317.6us
1.000647.6us

Eficiencia de Memoria

O indice usa aproximadamente 766 bytes por documento em RSS. Um corpus de 50K documentos adiciona apenas 36MB ao footprint de memoria do processo. Compare com o Elasticsearch, onde um indice comparavel tipicamente requer 2–5KB por documento mais overhead do heap da JVM.

MetricaValor
RSS antes da indexacao56 MB
RSS apos 50K docs93 MB
Delta36 MB
Bytes por documento~766 bytes

Analise Profunda

Por que o Overhead de Locks e Zero

Um resultado surpreendente: a indexacao serial e batch tem desempenho identico. A intuicao diz que o modo batch deveria evitar contencao de locks, mas na pratica o gargalo e a tokenizacao e I/O interna do Tantivy — nao o write lock.

Tantivy usa um unico IndexWriter com um buffer interno de 50MB. Os documentos sao tokenizados, analisados e escritos em segmentos em memoria antes de serem descarregados para disco. O write lock e mantido apenas durante a breve chamada a commit(), que dispara um flush de segmento. Durante a indexacao normal, o lock nao tem contencao.

Como as Leituras Concorrentes Escalam

O IndexReader do Tantivy cria instancias leves de Searcher que compartilham os mesmos arquivos de segmento memory-mapped via Arc. Nenhum dado e copiado. Cada thread de busca obtem seu proprio Searcher, mas todos leem das mesmas paginas de memoria fisica. O page cache do SO faz o resto.

Isso e fundamentalmente diferente dos motores baseados em JVM, onde cada query aloca objetos no heap, aumentando a pressao de GC sob carga concorrente.

O Query Parser

Nosso query parser suporta operadores booleanos, queries de frase com "aspas", queries por campo com sintaxe campo:valor, negacao com -termo, e matching com wildcards. Compila para uma arvore Box<dyn Query> do Tantivy em menos de 100 microssegundos independente da complexidade.

O parser usa uma abordagem de passagem unica: tokeniza o input, classifica cada token (limite de frase, prefixo de negacao, prefixo de campo, termo simples), e constroi a arvore de consulta de baixo para cima. Sem backtracking, sem ambiguidade. O(n) no comprimento do input.

RRF: O Simples Vence o Complexo

Consideramos modelos de learned-to-rank para combinar scores de BM25 e semanticos. O problema: requerem dados de treinamento, adicionam latencia de inferencia, e precisam de re-treinamento quando o corpus muda. RRF nao tem nenhum desses problemas.

A formula e trivial: para cada documento que aparece em qualquer lista ranqueada, somar 1 / (k + rank) atraves de todas as listas. Ordenar por score. Pronto. Com k=60 (a constante padrao), isso produz resultados competitivos com modelos treinados em benchmarks padrao, a uma fracao da complexidade.

Conclusoes Chave

Nota metodologica

Todos os benchmarks foram gerados usando um binario Rust dedicado (benchmark_report.rs) que cria documentos sinteticos que correspondem ao nosso schema de producao, mede tempo wall-clock com std::time::Instant, e calcula percentis de 100–10.000 amostras por medicao. A suite de benchmarks Criterion (core_benchmarks.rs) fornece validacao estatistica com intervalos de confianca. O codigo-fonte esta disponivel para auditoria.

Precisa desse nivel de performance
no seu stack de busca?

Auditamos e projetamos sistemas Rust. Entrega em 48 horas. Achados reais, nao checklists.

Iniciar Auditoria audit@newcool.io