Elasticsearch: implementando buscas full-text poderosas

1. Fundamentos do Elasticsearch e Índices Invertidos

Elasticsearch é um mecanismo de busca distribuído baseado em Apache Lucene, projetado especificamente para full-text search em larga escala. Diferentemente de bancos relacionais que realizam buscas lineares com operadores LIKE, o Elasticsearch utiliza uma estrutura chamada índice invertido (inverted index).

Um índice invertido funciona como um glossário: para cada termo único encontrado nos documentos, armazena uma lista de referências (documentos e posições) onde aquele termo aparece. Quando você faz uma busca por "machine learning", o Elasticsearch consulta rapidamente o índice invertido, localiza os documentos que contêm esses termos e calcula a relevância.

O processo de tokenização divide o texto em tokens (palavras individuais), aplica normalizações como lowercasing e remove pontuações. Enquanto um banco SQL precisa escanear linha por linha com WHERE texto LIKE '%termo%', o Elasticsearch localiza os resultados em milissegundos, mesmo em coleções com bilhões de documentos.

2. Mapeamento e Análise de Texto

Para que o Elasticsearch entenda como tratar seus dados, você define mappings. Campos do tipo text são analisados e indexados para full-text, enquanto campos keyword são armazenados como valores exatos para agregações e filtros.

PUT /artigos
{
  "mappings": {
    "properties": {
      "titulo": { "type": "text", "analyzer": "standard" },
      "conteudo": { "type": "text", "analyzer": "brazilian" },
      "autor": { "type": "keyword" },
      "data_publicacao": { "type": "date" }
    }
  }
}

Analisadores customizados permitem controle granular. Você pode combinar tokenizers (como standard ou whitespace) com filtros como lowercase, asciifolding, stemmer (para reduzir palavras à raiz) e stop (para remover palavras comuns como "de", "para", "com").

PUT /artigos/_settings
{
  "analysis": {
    "analyzer": {
      "meu_analisador": {
        "tokenizer": "standard",
        "filter": ["lowercase", "asciifolding", "portuguese_stemmer"]
      }
    },
    "filter": {
      "portuguese_stemmer": { "type": "stemmer", "language": "portuguese" }
    }
  }
}

O uso de sinônimos melhora a recuperação: uma busca por "carro" pode retornar documentos com "automóvel" ou "veículo" se configurado no filtro synonym.

3. Consultas Full-text: Match, Query String e Multi-match

A query match é a mais simples e poderosa para full-text search. Ela analisa o texto de busca e encontra documentos relevantes:

GET /artigos/_search
{
  "query": {
    "match": {
      "conteudo": "inteligência artificial aplicada"
    }
  }
}

Para buscar frases exatas, use match_phrase:

GET /artigos/_search
{
  "query": {
    "match_phrase": {
      "conteudo": "aprendizado supervisionado"
    }
  }
}

A query multi_match permite buscar em vários campos simultaneamente, atribuindo pesos diferentes:

GET /artigos/_search
{
  "query": {
    "multi_match": {
      "query": "redes neurais",
      "fields": ["titulo^3", "conteudo", "resumo^2"]
    }
  }
}

No exemplo acima, o campo titulo tem peso 3 (três vezes mais importante), resumo tem peso 2, e conteudo peso padrão 1.

4. Relevância e Scoring (TF-IDF e BM25)

O Elasticsearch utiliza o algoritmo BM25 (Okapi BM25) como padrão para calcular a relevância desde a versão 5.0. BM25 é uma evolução do TF-IDF que considera:

  • TF (Term Frequency): quantas vezes o termo aparece no documento
  • IDF (Inverse Document Frequency): raridade do termo na coleção
  • Normalização por comprimento: documentos mais curtos tendem a ser mais relevantes

O boosting permite priorizar resultados. Você pode aplicar boost em nível de campo (como visto no multi_match) ou em nível de query:

GET /artigos/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "conteudo": "machine learning" } }
      ],
      "should": [
        { "match": { "titulo": "machine learning" } }
      ]
    }
  }
}

Para controle avançado, use function_score:

GET /artigos/_search
{
  "query": {
    "function_score": {
      "query": { "match": { "conteudo": "deep learning" } },
      "functions": [
        { "filter": { "term": { "categoria": "tutorial" } }, "weight": 2 },
        { "gauss": { "data_publicacao": { "origin": "now", "scale": "30d" } } }
      ]
    }
  }
}

5. Filtros, Agregações e Facetas

Filtros no Elasticsearch são executados em contexto filter dentro de uma query bool. Eles não afetam o score, mas eliminam documentos:

GET /artigos/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "conteudo": "processamento linguagem natural" } }
      ],
      "filter": [
        { "term": { "categoria": "pesquisa" } },
        { "range": { "data_publicacao": { "gte": "2024-01-01" } } }
      ]
    }
  }
}

Agregações permitem criar facetas de navegação em tempo real:

GET /artigos/_search
{
  "size": 0,
  "aggs": {
    "por_categoria": {
      "terms": { "field": "categoria" }
    },
    "por_ano": {
      "date_histogram": {
        "field": "data_publicacao",
        "calendar_interval": "year"
      }
    }
  }
}

Combine buscas full-text com agregações para construir dashboards interativos onde o usuário filtra por texto e vê as distribuições atualizadas instantaneamente.

6. Sugestões, Autocomplete e Correção Ortográfica

O completion suggester implementa autocomplete eficiente baseado em prefixos:

PUT /artigos/_mapping
{
  "properties": {
    "titulo_suggest": {
      "type": "completion"
    }
  }
}

GET /artigos/_search
{
  "suggest": {
    "titulo_sugestao": {
      "prefix": "ma",
      "completion": { "field": "titulo_suggest" }
    }
  }
}

Para correção ortográfica estilo "did you mean?", use o phrase suggester:

GET /artigos/_search
{
  "suggest": {
    "correcao": {
      "text": "inteligencia artificial",
      "phrase": {
        "field": "conteudo",
        "size": 3,
        "gram_size": 3,
        "direct_generator": [{
          "field": "conteudo",
          "suggest_mode": "always"
        }]
      }
    }
  }
}

7. Otimização de Performance e Escalabilidade

Um cluster Elasticsearch distribui dados em shards (partições) e cria réplicas para alta disponibilidade. A configuração ideal depende do volume de dados e padrão de consultas:

PUT /artigos
{
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 2,
    "refresh_interval": "30s"
  }
}

O refresh_interval controla o quão rápido novos documentos ficam visíveis para busca. Valores maiores (30s-60s) melhoram a performance de indexação, enquanto valores menores (1s) reduzem latência de visibilidade.

Cache de consultas e filtros acelera buscas repetitivas. Ative o cache para filtros frequentes:

GET /artigos/_search
{
  "query": {
    "bool": {
      "filter": [
        { "term": { "categoria": "tecnologia" } }
      ]
    }
  },
  "request_cache": true
}

8. Casos de Uso Avançados e Integração

Busca geoespacial combinada com full-text permite encontrar "restaurantes italianos próximos" ou "artigos sobre sustentabilidade em São Paulo":

GET /estabelecimentos/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "descricao": "comida japonesa" } }
      ],
      "filter": [
        { "geo_distance": { "distance": "5km", "localizacao": { "lat": -23.55, "lon": -46.63 } } }
      ]
    }
  }
}

Para documentos com arrays de objetos complexos, use nested queries:

PUT /cursos
{
  "mappings": {
    "properties": {
      "modulos": { "type": "nested" }
    }
  }
}

GET /cursos/_search
{
  "query": {
    "nested": {
      "path": "modulos",
      "query": {
        "bool": {
          "must": [
            { "match": { "modulos.titulo": "introdução" } },
            { "range": { "modulos.duracao": { "gte": 60 } } }
          ]
        }
      }
    }
  }
}

A integração com Logstash permite ingerir logs e dados estruturados, enquanto Kibana oferece dashboards visuais para explorar resultados de busca, agregações e monitorar performance do cluster.


Referências