Busca Lexical na Prática com BM25
No artigo anterior vimos como funciona a busca semântica, que tenta encontrar resultados com base no significado dos textos.
Agora vamos entender a busca lexical.
A busca lexical trabalha de uma maneira mais direta:
Ela procura correspondências entre os termos da pesquisa e os termos presentes nos documentos.
Imagine que temos:
Documento 1:
"Como configurar PostgreSQL utilizando Docker"
Documento 2:
"Como criar uma API utilizando Node.js"
Documento 3:
"Como configurar Redis utilizando Docker"
O usuário pesquisa:
PostgreSQL Docker
A busca lexical identifica que o Documento 1 possui os termos:
PostgreSQL
Docker
Portanto, ele provavelmente será o resultado mais relevante.
1. O conceito de índice invertido
Um dos principais conceitos por trás de mecanismos de busca lexicais é o:
Índice invertido (Inverted Index)
Imagine que temos:
Documento 1
"PostgreSQL Docker"
Documento 2
"Node.js API"
Documento 3
"PostgreSQL Redis"
Podemos criar um índice:
PostgreSQL
↓
Documento 1
Documento 3
Docker
↓
Documento 1
Node.js
↓
Documento 2
API
↓
Documento 2
Redis
↓
Documento 3
Quando o usuário pesquisa:
PostgreSQL Docker
o sistema consulta o índice:
PostgreSQL
↓
1, 3
Docker
↓
1
O Documento 1 possui os dois termos.
Portanto:
Documento 1
é provavelmente o melhor resultado.
Esse conceito permite realizar buscas muito mais rapidamente do que analisar todos os documentos do zero.
2. Tokenização
Antes de criar o índice, precisamos transformar o texto em termos.
Por exemplo:
"Como configurar PostgreSQL utilizando Docker"
Pode se tornar:
[
"como",
"configurar",
"postgresql",
"utilizando",
"docker"
]
Esse processo é chamado de:
Tokenização
Dependendo do mecanismo de busca, podemos realizar outras operações, como:
- Converter para lowercase
- Remover pontuação
- Remover stopwords
- Stemming
- Normalização
Por exemplo:
"PostgreSQL, PostgreSQL!"
pode ser normalizado para:
postgresql
postgresql
3. Stopwords
Algumas palavras aparecem com tanta frequência que normalmente não ajudam muito na busca.
Por exemplo:
o
a
de
do
da
para
com
em
Essas palavras podem ser chamadas de:
Stopwords
Uma frase:
"Como configurar o PostgreSQL no Docker"
pode ser simplificada para:
configurar
postgresql
docker
Isso pode reduzir o tamanho do índice e melhorar a eficiência.
Porém, stopwords devem ser utilizadas com cuidado.
Em algumas aplicações, palavras aparentemente comuns podem ser importantes para o significado da consulta.
4. Term Frequency
Uma das ideias fundamentais da busca lexical é:
Term Frequency (TF)
Ela representa a frequência com que um termo aparece em um documento.
Imagine:
Documento A:
PostgreSQL é um banco de dados.
PostgreSQL pode ser executado com Docker.
PostgreSQL possui muitas extensões.
O termo:
PostgreSQL
aparece várias vezes.
Isso indica que o documento provavelmente está relacionado ao termo.
Porém, existe um problema.
Imagine:
PostgreSQL PostgreSQL PostgreSQL PostgreSQL
Só porque uma palavra aparece muitas vezes não significa que o documento seja melhor.
Por isso, mecanismos modernos utilizam algoritmos de ranking mais sofisticados.
Um dos mais conhecidos é o:
BM25
5. O que é BM25?
O BM25 é um algoritmo utilizado para calcular a relevância de um documento em relação a uma consulta.
De maneira simplificada, ele considera fatores como:
1. Frequência do termo no documento
2. Frequência do termo em toda a coleção
3. Tamanho do documento
A ideia é:
Query
↓
Termos
↓
Documentos que possuem os termos
↓
BM25 calcula relevância
↓
Ranking
O resultado pode ser:
Documento A
Score: 12.4
Documento B
Score: 8.7
Documento C
Score: 1.2
Os documentos são então retornados nessa ordem.
6. Um exemplo simples
Imagine os seguintes documentos:
const documents = [
{
id: 1,
text: "Como configurar PostgreSQL utilizando Docker",
},
{
id: 2,
text: "Como criar uma API utilizando Node.js",
},
{
id: 3,
text: "Como configurar Redis utilizando Docker",
},
];
O usuário pesquisa:
PostgreSQL Docker
O resultado ideal seria:
Documento 1
porque contém os dois termos.
O Documento 3 contém:
Docker
mas não contém:
PostgreSQL
Portanto, ele pode aparecer depois.
7. Uma implementação simples
Antes de implementar BM25, podemos começar com uma busca lexical extremamente simples.
function tokenize(text: string): string[] {
return text
.toLowerCase()
.replace(/[^\w\s]/g, "")
.split(/\s+/);
}
Agora podemos calcular quantos termos da pesquisa aparecem em cada documento:
function lexicalScore(
query: string,
document: string
): number {
const queryTerms = tokenize(query);
const documentTerms = tokenize(document);
let score = 0;
for (const term of queryTerms) {
if (documentTerms.includes(term)) {
score++;
}
}
return score;
}
Podemos executar:
const query = "PostgreSQL Docker";
for (const document of documents) {
const score = lexicalScore(
query,
document.text
);
console.log(
document.id,
score
);
}
Teríamos algo semelhante a:
Documento 1 → 2
Documento 2 → 0
Documento 3 → 1
Então podemos ordenar:
1. Documento 1
2. Documento 3
3. Documento 2
Isso já é uma forma extremamente simples de busca lexical.
Porém, mecanismos reais utilizam algoritmos mais sofisticados.
8. Implementando uma versão simplificada de BM25
Uma implementação simplificada pode ser feita assim:
function bm25(
query: string,
document: string,
allDocuments: string[],
k1 = 1.5,
b = 0.75
): number {
const queryTerms = tokenize(query);
const documentTerms = tokenize(document);
const documentLength =
documentTerms.length;
const averageDocumentLength =
allDocuments.reduce(
(sum, doc) =>
sum + tokenize(doc).length,
0
) / allDocuments.length;
let score = 0;
for (const term of queryTerms) {
const termFrequency =
documentTerms.filter(
(item) => item === term
).length;
if (termFrequency === 0) {
continue;
}
const documentsContainingTerm =
allDocuments.filter((doc) =>
tokenize(doc).includes(term)
).length;
const idf = Math.log(
1 +
(allDocuments.length -
documentsContainingTerm +
0.5) /
(documentsContainingTerm + 0.5)
);
const numerator =
termFrequency * (k1 + 1);
const denominator =
termFrequency +
k1 *
(1 -
b +
b *
(documentLength /
averageDocumentLength));
score +=
idf *
(numerator / denominator);
}
return score;
}
Agora podemos criar nossa busca:
function search(
query: string,
documents: {
id: number;
text: string;
}[]
) {
const allTexts =
documents.map(
(document) => document.text
);
return documents
.map((document) => ({
...document,
score: bm25(
query,
document.text,
allTexts
),
}))
.sort(
(a, b) =>
b.score - a.score
);
}
Podemos executar:
const results = search(
"PostgreSQL Docker",
documents
);
console.log(results);
O resultado seria aproximadamente:
[
{
id: 1,
text: "Como configurar PostgreSQL utilizando Docker",
score: 1.8
},
{
id: 3,
text: "Como configurar Redis utilizando Docker",
score: 0.7
},
{
id: 2,
text: "Como criar uma API utilizando Node.js",
score: 0
}
]
O Documento 1 ficou em primeiro porque possui:
PostgreSQL
+
Docker
Enquanto o Documento 3 possui apenas:
Docker
9. O problema da busca simples
Nossa implementação funciona para demonstrar o conceito, mas existem vários problemas.
Por exemplo, o usuário pesquisa:
PC lento
O documento contém:
Computador com baixo desempenho
A busca lexical provavelmente não encontrará uma boa correspondência.
Isso acontece porque:
PC ≠ computador
lento ≠ baixo desempenho
Mesmo que os significados sejam próximos.
Esse é justamente um dos principais problemas que a busca semântica tenta resolver.
10. Busca Lexical vs Semântica
Podemos comparar:
Busca Lexical
Query:
"PostgreSQL Docker"
Procura:
PostgreSQL
Docker
Busca Semântica
Query:
"Como executar um banco em containers"
Pode encontrar:
"Como configurar PostgreSQL utilizando Docker"
mesmo que as palavras sejam diferentes.
11. Onde a busca lexical é melhor?
A busca lexical é especialmente útil quando a correspondência exata é importante.
Por exemplo:
"RTX 5090"
"iPhone 17 Pro"
"ERR_CONNECTION_REFUSED"
"PostgreSQL 18"
Nesses casos, você provavelmente não quer que o sistema retorne apenas algo "semanticamente parecido".
Se o usuário pesquisou:
RTX 5090
um resultado sobre:
RTX 4090
pode não ser relevante, mesmo que os produtos sejam semanticamente relacionados.
12. Busca Lexical em aplicações reais
Um sistema de busca real normalmente não implementa BM25 manualmente.
É comum utilizar mecanismos especializados.
Algumas opções populares são:
Elasticsearch
OpenSearch
Solr
Lucene
PostgreSQL Full Text Search
O fluxo seria:
Documentos
↓
Indexação
↓
Índice Invertido
↓
BM25
Quando o usuário pesquisa:
"PostgreSQL Docker"
temos:
Query
↓
Tokenização
↓
Índice Invertido
↓
Documentos candidatos
↓
BM25
↓
Ranking
↓
Resultados
13. Exemplo com Elasticsearch
Em um sistema real, poderíamos ter um índice:
{
"mappings": {
"properties": {
"title": {
"type": "text"
},
"content": {
"type": "text"
}
}
}
}
Podemos então pesquisar:
{
"query": {
"multi_match": {
"query": "PostgreSQL Docker",
"fields": [
"title",
"content"
]
}
}
}
O Elasticsearch pode utilizar BM25 para calcular a relevância.
O resultado pode ser:
Documento A
Score: 8.2
Documento B
Score: 4.7
Documento C
Score: 1.1
Os resultados são retornados já ordenados pela relevância.
14. O conceito de Ranking
Um dos pontos mais importantes da busca lexical é:
Encontrar documentos é apenas uma parte do problema.
Também precisamos decidir:
Qual documento deve aparecer primeiro?
Imagine:
Query:
"PostgreSQL Docker"
Temos:
Documento A
Título: PostgreSQL com Docker
Conteúdo: Guia completo...
Documento B
Título: Docker
Conteúdo: PostgreSQL é mencionado uma vez...
Mesmo que ambos possuam os termos, o Documento A provavelmente deve aparecer primeiro.
O ranking considera fatores como:
Frequência dos termos
Relevância dos termos
Tamanho do documento
Raridade dos termos
Campo onde o termo aparece
Por isso, mecanismos de busca são muito mais sofisticados do que:
if (document.includes(query))
15. Busca Lexical e Busca Híbrida
Em sistemas modernos, a busca lexical frequentemente é combinada com a busca semântica.
Podemos ter:
Query
↓
┌────────┴────────┐
↓ ↓
Busca Lexical Busca Semântica
↓ ↓
BM25 Vector Search
↓ ↓
└────────┬────────┘
↓
Combinação dos Rankings
↓
Reranking
↓
Resultado
Por exemplo:
Busca Lexical
→ Excelente para termos exatos
Busca Semântica
→ Excelente para significado
Busca Híbrida
→ Combina os dois
Isso é especialmente útil em:
- RAG
- E-commerce
- Documentação
- Sistemas de busca internos
- Bases de conhecimento
- Sistemas de recomendação
Conclusão
A busca lexical é uma das bases dos sistemas modernos de recuperação de informação.
Seu funcionamento pode ser resumido em:
Documento
↓
Tokenização
↓
Indexação
↓
Índice Invertido
↓
Query
↓
Busca pelos termos
↓
BM25
↓
Ranking
↓
Resultados
O BM25 é especialmente importante porque permite calcular a relevância de cada documento levando em consideração não apenas se um termo aparece, mas também fatores como sua frequência, sua raridade e o tamanho do documento.
A principal vantagem da busca lexical é a precisão em termos específicos.
Ela é excelente para encontrar:
Códigos
IDs
SKUs
Nomes
Versões
Termos técnicos
Por outro lado, ela possui dificuldade para compreender que:
PC
≈
Computador
ou:
carro econômico
≈
veículo com baixo consumo
É por isso que sistemas modernos frequentemente combinam:
Busca Lexical
+
Busca Semântica
↓
Busca Híbrida
A busca lexical responde "quais documentos possuem termos relacionados à minha pesquisa?", enquanto a busca semântica tenta responder "quais documentos possuem significado relacionado à minha pesquisa?".