stamatios
← Voltar ao feed
Máquina de busca antiga e pesada sendo substituída por cilindro de banco de dados, metáfora da migração para PostgreSQL
Dev & Engenharia

Cortex troca Lucene por PostgreSQL e corta latência da API em 88%

resumo de ~3 min

Problema com a arquitetura antiga

O catálogo de software do Cortex era pesquisado com um sistema baseado em Lucene. Os índices eram gerados por cron, armazenados e, na consulta, desserializados na memória da aplicação. Com o crescimento dos dados, chegaram a existir objetos de cerca de 1 GiB, o que causava exceções de falta de memória e latência alta em consultas de cold start. Como os índices eram reconstruídos apenas periodicamente, uma entidade recém-criada podia demorar a aparecer na busca, e uma edição podia levar pelo menos cinco minutos para ser refletida. Segundo o autor, isso explicava boa parte dos relatórios de bug.

As restrições eram claras: a solução precisava funcionar em ambientes com apenas um PostgreSQL, sem depender de extensões arbitrárias, já que nem todos os provedores gerenciados permitem instalá-las. Extensões do contrib, como unaccent, estão disponíveis em qualquer provedor. Elasticsearch e ParadeDB foram descartados pela carga operacional e porque serviços como RDS e Cloud SQL não permitem instalá-los.

Interpretação das consultas

O PostgreSQL oferece websearch_to_tsquery, mas essa função não entende campos (owner:), verificação de presença (owner:*) nem parênteses. Além disso, a equipe queria controle total sobre a gramática, para estendê-la conforme o produto crescesse. Por isso, criou um tokenizador e um parser com a precedência usual de NOT, AND e OR. A árvore resultante tem limites de profundidade e de número de termos, para que consultas patológicas não gerem planos que o Postgres demore a executar. Cada nó da árvore vira um trecho da cláusula WHERE, montado com jOOQ.

Índice, ranking e atualização

O índice usa to_tsvector sem stemming, de propósito, porque o catálogo é cheio de nomes, tags e identificadores que não devem ser reduzidos. Um índice GIN sobre a coluna search_vector localiza candidatos rapidamente; quando a busca é por um campo específico, a condição é verificada novamente apenas nessas linhas. Para o ranking, a equipe combina níveis: correspondência exata, substring, ts_rank_cd e similaridade trigram. Os pesos foram escolhidos para que qualquer correspondência exata supere qualquer substring, e o ts_rank só desempata dentro de cada nível.

A atualização não depende mais de um cronograma. O Debezium lê o write-ahead log, publica as alterações no Kafka, e um consumidor reprojeta a entidade afetada a partir do estado atual das tabelas de origem, sem aplicar o conteúdo do evento. Assim, a ordem dos eventos pouco importa, e reexecuções são inofensivas. Um hash de conteúdo evita reescrever linhas que não mudaram. Alterações de grande alcance, como renomear um grupo com milhares de serviços, ficam para uma reconstrução noturna completa. O autor reconhece que essa escolha troca um pouco de consistência por menos impacto nas atualizações comuns, além de servir como rede de segurança para falhas no CDC.

Superfícies de busca

As caixas de busca de scorecards, workflows, relatórios e outras listas foram extraídas em um componente chamado SearchSurface. Ele declara apenas a tabela, o identificador, as colunas e os pesos. As superfícies usam o mesmo parser, mas não aceitam operadores de campo. Por padrão, termos adjacentes são tratados como OR e ordenados pelo número de termos encontrados, enquanto um AND digitado pelo usuário continua sendo obrigatório. Como essas tabelas costumam ter centenas ou milhares de linhas por tenant, o tsvector é calculado na consulta. Se uma tabela crescer, o autor diz que basta adicionar um índice GIN de expressão, sem mudanças no código.

Resultados

A comparação usa um mês de produção no mecanismo antigo e um mês após a migração do último tenant. Na busca interna, o p50 foi de 0,16 s para 0,14 s, o p95 de 3,33 s para 1,68 s e o p99 de 17,8 s para 5,0 s. Na API pública, o p50 foi de 0,63 s para 0,30 s, o p95 de 13,3 s para 1,54 s (cerca de 8,6 vezes mais rápido) e o p99 de 21,2 s para 3,35 s. A maior melhoria ocorreu na cauda da distribuição, onde antes os cold starts esperavam o carregamento do índice.

Nos aplicativos que atendem a busca, o heap residual após GC caiu cerca de metade, e o uso total de memória, cerca de 40%. Quanto à atualidade, a maior parte das edições aparece na busca em poucos segundos: no caso de atualizações, o p50 foi de 1,5 s e o p99 de 9,7 s; em inserções, 1,4 s e 3,3 s. Atrasos maiores ocorrem em backlogs curtos, quando grandes jobs gravam muitos dados de uma vez.

Conclusão

O autor descreve meses de planejamento, desenvolvimento e implantação que, para a maioria dos clientes, se resumem ao fato de que a busca funciona. O texto não traz custos financeiros da migração e não discute limitações além das ressalvas já citadas, como os atrasos em lotes grandes.