- Implementa uma arquitetura de mecanismo de busca que funciona sem serviços externos usando o banco de dados existente, com foco em tokenização, pesos e scoring
- A ideia central é tokenizar e armazenar todo o texto e, na busca, combinar os tokens da mesma forma para calcular a relevância
- Combina tokenizadores Word, Prefix e N-Gram para lidar com correspondência exata, correspondência parcial e tolerância a erros de digitação, com pesos próprios para cada tokenizador
- Por meio de um sistema de pesos e algoritmo de scoring baseado em SQL, avalia em conjunto comprimento do documento, diversidade de tokens e qualidade média
- Tem alta escalabilidade e transparência, permitindo adicionar novos tokenizadores ou tipos de documento, ajustar pesos e modificar o scoring com liberdade
Por que criar seu próprio mecanismo de busca
- Serviços externos como Elasticsearch e Algolia são poderosos, mas trazem a carga de aprender APIs complexas e gerenciar infraestrutura
- Quando o que se precisa é apenas de uma função de busca integrada ao banco de dados existente e fácil de depurar, construir internamente pode ser útil
- O objetivo é um mecanismo de busca simples que retorne resultados relevantes sem dependências externas
Conceito central: tokenização e correspondência
- O princípio básico é tokenizar (tokenize) e armazenar todo o texto e, na busca, gerar tokens da mesma forma para fazer a correspondência
- Na etapa de indexação, o conteúdo é separado em unidades de token e armazenado com seus pesos
- Na etapa de busca, a consulta é tokenizada da mesma maneira para encontrar tokens correspondentes e calcular a pontuação
- Na etapa de scoring, os pesos armazenados são usados para calcular a relevância
Projeto do esquema do banco de dados
- Usa duas tabelas:
index_tokens e index_entries
index_tokens: armazena tokens únicos e pesos por tokenizador
index_entries: conecta tokens e documentos, armazenando a pontuação final que reflete os pesos de campo, documento e tokenizador
- Fórmula de cálculo do peso final:
field_weight × tokenizer_weight × ceil(sqrt(token_length))
- Os índices são configurados para consulta de documentos, consulta de tokens, queries por campo e filtragem por peso
Sistema de tokenização
- WordTokenizer: separação por palavra, remoção de palavras curtas, para correspondência exata (peso 20)
- PrefixTokenizer: gera prefixos de palavras, para autocomplete e correspondência parcial (peso 5)
- NGramsTokenizer: gera combinações de caracteres de tamanho fixo, para lidar com erros de digitação e correspondência parcial (peso 1)
- Todos os tokenizadores executam em comum conversão para minúsculas, remoção de caracteres especiais e normalização de espaços
Sistema de pesos
- Peso de campo: reflete a importância de título, corpo, palavras-chave etc.
- Peso do tokenizador: na ordem Word > Prefix > N-Gram
- Peso do documento: calculado combinando os dois fatores acima com o comprimento do token
- A função
ceil(sqrt()) é usada para reduzir a influência de tokens longos, podendo ser ajustada para uma função logarítmica ou linear se necessário
Serviço de indexação
- Só documentos que implementam
IndexableDocumentInterface podem ser indexados
- Na criação ou modificação do documento, a indexação é executada por listeners de eventos ou comandos (
app:index-document, app:reindex-documents)
- Procedimento:
- Remove o índice existente e gera novos tokens
- Executa todos os tokenizadores para cada campo
- Verifica a existência do token e o cria se necessário (
findOrCreateToken)
- Faz inserção em lote (batch insert) em
index_entries com os pesos calculados
- Estrutura voltada para evitar duplicação, melhorar desempenho e lidar com atualizações
Serviço de busca
- A query é processada com os mesmos tokenizadores para obter o mesmo conjunto de tokens da indexação
- Após remover tokens duplicados, eles são ordenados por comprimento em ordem decrescente (tokens mais longos primeiro), com limite de 300
- Uma query SQL faz o join entre tokens e documentos e calcula e ordena a pontuação de relevância
- O resultado é retornado no formato
SearchResult(documentId, score)
Algoritmo de scoring
- Pontuação base:
SUM(sd.weight)
- Correção por diversidade de tokens:
LOG(1 + COUNT(DISTINCT token_id))
- Correção por peso médio:
LOG(1 + AVG(weight))
- Penalidade por tamanho do documento:
1 / (1 + LOG(1 + token_count))
- Normalização (normalization): divide pelo valor máximo para ajustar ao intervalo de 0 a 1
- Remove correspondências irrelevantes de baixo peso com o filtro de peso mínimo de token (
st2.weight >= ?)
Tratamento dos resultados
- Os resultados da busca são retornados como ID do documento e pontuação, e convertidos em documentos reais via repositório (repository)
- Usa a função
FIELD() para manter a ordem dos resultados da busca durante a consulta dos documentos
Escalabilidade do sistema
- Novos tokenizadores podem ser adicionados implementando
TokenizerInterface
- Novos tipos de documento podem ser registrados implementando
IndexableDocumentInterface
- A lógica de pesos e scoring pode ser ajustada apenas com modificações no SQL
Conclusão
- Essa estrutura é um mecanismo de busca simples, mas que realmente funciona, oferecendo desempenho suficiente sem infraestrutura externa
- As vantagens são lógica clara, controle total e depuração fácil
- Reforça a ideia de que, mais do que sistemas complexos, código que você mesmo entende e controla pode ter mais valor
1 comentários
Comentários no Hacker News
A ideia básica de busca é simples e um campo de problemas interessante
Mas o realmente difícil é lidar com grandes volumes de dados e com consultas ambíguas
Uma abordagem baseada em DBMS funciona bem no nível de sites pequenos, mas bate no limite rapidamente na escala da Wikipédia em inglês
Como material introdutório, o e-book SeIRP é um bom recurso gratuito
O fato de não existir uma resposta claramente correta torna isso especialmente complicado
O Google às vezes mostra anúncios como “os resultados mais relevantes”, então o Marginalia Search é um bom caso de contraste
Fico curioso se você já chegou a consultar os artigos do TREC
Motores de busca precisam lutar sem parar contra agentes adversariais tentando ganhar dinheiro com anúncios
Isso vira um jogo infinito de gato e rato, mudando constantemente as métricas de qualidade para impedir que sejam exploradas
Queria saber que tamanho de corpus daria para pesquisar assumindo algo como 5 segundos por consulta e cerca de 12 consultas por minuto
Por exemplo, é difícil julgar se o artigo da wiki de Gilligan’s Island ou um blog de fã é o resultado “melhor”
Se ainda somar manipulação de ranking ou keyword stuffing, o desafio fica muito mais complexo do que um mero problema de escalabilidade
Busca é realmente difícil
Até empresas com muitos recursos e capacidade técnica, como Apple, Microsoft e OpenAI, têm baixa qualidade nas funcionalidades de busca
Isso não é só um problema técnico
Para melhorar a qualidade da busca, é preciso ajustar finamente os parâmetros de ranking, e esse tipo de trabalho é difícil de planejar em estruturas de gestão como sprint ou Jira
No fim, é uma área que exige confiança e autonomia para os desenvolvedores
Investem bilhões em modelos de IA, mas webapps ou busca ficam em segundo plano, e o resultado é esse
Há uns 10 anos trabalhei com um colega de doutorado em design de motores de busca
Ele falava com muito entusiasmo sobre a integração entre busca e banco de dados, e aprendi bastante por causa disso
Um dia ainda quero explorar a fundo o funcionamento interno de Solr e Lucene da Apache
Antigamente não existiam soluções open source de busca, então era preciso fazer tudo na mão
A lição tirada dessa experiência foi: não construa seu próprio motor de busca
Muita gente passou anos em cima desse problema, e fazer por conta própria leva a um inferno interminável de manutenção
Quando começam pedidos como “adiciona correção de typos” ou “ano que vem vamos incluir também uma taxonomia”, acabou
Eu gostava muito de uma aula de construção de motores de busca dada no passado pelo professor David Evans, da Virginia University
Construir um “motor de busca clássico” com as próprias mãos foi um projeto muito divertido
Dá para consultar o link da disciplina e a playlist no YouTube
O que me incomoda nos motores de busca que uso com frequência é que eles ignoram siglas ou palavras de 2 a 3 letras
É muito ruim quando removem termos curtos como “mp3” ou “PHP” ao pesquisar
Li Programming Collective Intelligence, do Toby Segaran, e me inspirei em várias ideias sobre busca, recomendação e classificadores
Foi um texto interessante
Fiquei curioso sobre o nível de otimização dos tokenizadores usado pelos mecanismos de busca populares
Fico me perguntando o quanto esse sistema conseguiria escalar bem
O Elasticsearch mostra um desempenho bem impressionante mesmo além da escala recomendada
Não é tão difícil fazer um motor simples de busca textual
Mas criar um bom motor de busca é uma história completamente diferente
Não basta simplesmente implementar um algoritmo como BM25
A maioria das empresas para as quais fiz consultoria usava soluções próprias e acabou migrando para Elasticsearch ou OpenSearch
A implementação própria começa simples, mas com o tempo fica complexa por causa de problemas de ranking e queda de desempenho
Sintomas como “está lento” ou “está trazendo resultados nada a ver” passam a se repetir
O Elasticsearch já resolve esses problemas há 10 anos, e hoje está muito mais evoluído
Dizem que “é difícil de configurar”, mas hoje em dia quase tudo é configurado automaticamente, e também há muitos serviços gerenciados
É até mais fácil de lidar do que o Postgres
No fim, o que importa mesmo é otimizar o mapeamento do índice
Algumas pessoas dizem “não precisamos dessas funções avançadas”, mas na prática a qualidade da busca está diretamente ligada à sobrevivência do negócio
Se você quer uma busca realmente boa, no fim vai ter de aceitar essa complexidade
Alternativas em ascensão como o SeekStorm, que tem aparecido bastante no HN ultimamente, também parecem interessantes, mas ainda não vi casos reais em produção
Especialmente a dica de desativar o dynamic mapping e impedir a indexação de campos desnecessários foi útil
Pelo que sei, é um projeto mais antigo que o Lucene