Go e Elasticsearch: Busca Full-Text Avançada
Elasticsearch é o motor de busca mais popular do mundo, usado desde pequenas aplicações até sistemas como Netflix e Uber. Neste guia, você aprenderá a integrar Elasticsearch com Go para criar buscas rápidas, relevantes e escaláveis.
Índice
- O que é Elasticsearch?
- Configuração do Cliente Go
- Indexando Documentos
- Realizando Buscas
- Queries Avançadas
- Agregações
- Mapeamentos e Análise
- Exemplo Prático: Catálogo de Produtos
- Performance e Otimização
O que é Elasticsearch?
Elasticsearch é um motor de busca e análise distribuído baseado no Apache Lucene. Ele proporciona:
- Busca Full-Text: Busca em texto natural com relevância
- Performance: Milissegundos em bilhões de documentos
- Escalabilidade: Distribuição automática entre nós
- Agregações: Analytics em tempo real
- REST API: Interface HTTP JSON simples
Casos de Uso
| Caso de Uso | Exemplo |
|---|---|
| Busca de produtos | Amazon, Mercado Livre |
| Log aggregation | ELK Stack (Elasticsearch, Logstash, Kibana) |
| Análise de dados | Dashboards em tempo real |
| Autocomplete | Sugestões de pesquisa |
| Geolocalização | Restaurantes próximos |
Configuração do Cliente Go
Instalação
# Cliente oficial Elasticsearch
go get github.com/elastic/go-elasticsearch/v8
# Ou para versões mais antigas
go get github.com/elastic/go-elasticsearch/v7
Cliente Básico
package main
import (
"log"
"github.com/elastic/go-elasticsearch/v8"
)
func main() {
// Configuração básica
cfg := elasticsearch.Config{
Addresses: []string{
"http://localhost:9200",
},
Username: "elastic",
Password: "your-password",
}
client, err := elasticsearch.NewClient(cfg)
if err != nil {
log.Fatalf("Erro criando cliente: %s", err)
}
// Verificar conexão
res, err := client.Info()
if err != nil {
log.Fatalf("Erro no ping: %s", err)
}
defer res.Body.Close()
log.Printf("Conectado: %s", res)
}
Configuração Avançada
import (
"crypto/tls"
"net/http"
"time"
)
cfg := elasticsearch.Config{
// Múltiplos nós para alta disponibilidade
Addresses: []string{
"https://es1.example.com:9200",
"https://es2.example.com:9200",
"https://es3.example.com:9200",
},
// Autenticação
Username: "elastic",
Password: "senha-segura",
// API Key (alternativa à senha)
// APIKey: "base64-api-key",
// Configurações de retry
MaxRetries: 3,
RetryBackoff: func(i int) time.Duration {
return time.Duration(i) * 100 * time.Millisecond
},
// Timeout
RequestTimeout: 10 * time.Second,
// Transport personalizado (TLS, etc.)
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, // ⚠️ Apenas para desenvolvimento
},
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
},
// Logger para debug
Logger: &estransport.ColorLogger{
Output: os.Stdout,
EnableRequestBody: true,
EnableResponseBody: true,
},
}
client, err := elasticsearch.NewClient(cfg)
Indexando Documentos
Criando um Índice
package main
import (
"bytes"
"context"
"encoding/json"
"log"
"strings"
"github.com/elastic/go-elasticsearch/v8"
"github.com/elastic/go-elasticsearch/v8/esapi"
)
func criarIndice(client *elasticsearch.Client, nome string) error {
// Verificar se índice existe
exists, err := client.Indices.Exists([]string{nome})
if err != nil {
return err
}
if exists.StatusCode == 200 {
log.Printf("Índice %s já existe", nome)
return nil
}
// Criar índice com mapeamento
mapping := `{
"mappings": {
"properties": {
"titulo": {
"type": "text",
"analyzer": "portuguese"
},
"descricao": {
"type": "text",
"analyzer": "portuguese"
},
"preco": {
"type": "float"
},
"categoria": {
"type": "keyword"
},
"data_criacao": {
"type": "date"
}
}
},
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0
}
}`
req := esapi.IndicesCreateRequest{
Index: nome,
Body: strings.NewReader(mapping),
}
res, err := req.Do(context.Background(), client)
if err != nil {
return err
}
defer res.Body.Close()
if res.IsError() {
log.Printf("Erro criando índice: %s", res.String())
} else {
log.Printf("Índice %s criado com sucesso", nome)
}
return nil
}
Indexando Documentos
type Produto struct {
ID string `json:"id"`
Titulo string `json:"titulo"`
Descricao string `json:"descricao"`
Preco float64 `json:"preco"`
Categoria string `json:"categoria"`
Tags []string `json:"tags"`
DataCriacao time.Time `json:"data_criacao"`
}
func indexarProduto(client *elasticsearch.Client, produto Produto) error {
// Serializar para JSON
data, err := json.Marshal(produto)
if err != nil {
return err
}
// Indexar documento
req := esapi.IndexRequest{
Index: "produtos",
DocumentID: produto.ID,
Body: bytes.NewReader(data),
Refresh: "true", // Torna o documento imediatamente disponível
}
res, err := req.Do(context.Background(), client)
if err != nil {
return err
}
defer res.Body.Close()
if res.IsError() {
return fmt.Errorf("erro indexando: %s", res.String())
}
return nil
}
// Indexar em bulk (mais eficiente para muitos documentos)
func indexarEmBulk(client *elasticsearch.Client, produtos []Produto) error {
var buf bytes.Buffer
for _, p := range produtos {
// Ação de index
meta := []byte(fmt.Sprintf(`{"index":{"_index":"produtos","_id":"%s"}}%s`, p.ID, "\n"))
data, _ := json.Marshal(p)
data = append(data, "\n"...)
buf.Write(meta)
buf.Write(data)
}
req := esapi.BulkRequest{
Body: &buf,
Refresh: "true",
}
res, err := req.Do(context.Background(), client)
if err != nil {
return err
}
defer res.Body.Close()
if res.IsError() {
return fmt.Errorf("erro bulk: %s", res.String())
}
return nil
}
Realizando Buscas
Busca Simples (Match Query)
func buscarProdutos(client *elasticsearch.Client, termo string) ([]Produto, error) {
// Construir query
query := map[string]interface{}{
"query": map[string]interface{}{
"multi_match": map[string]interface{}{
"query": termo,
"fields": []string{"titulo^3", "descricao", "tags"},
},
},
}
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(query); err != nil {
return nil, err
}
// Executar busca
res, err := client.Search(
client.Search.WithContext(context.Background()),
client.Search.WithIndex("produtos"),
client.Search.WithBody(&buf),
client.Search.WithTrackTotalHits(true),
client.Search.WithPretty(),
)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.IsError() {
return nil, fmt.Errorf("erro na busca: %s", res.String())
}
// Decodificar resultado
var r map[string]interface{}
if err := json.NewDecoder(res.Body).Decode(&r); err != nil {
return nil, err
}
// Extrair produtos
var produtos []Produto
hits := r["hits"].(map[string]interface{})["hits"].([]interface{})
for _, hit := range hits {
source := hit.(map[string]interface{})["_source"]
sourceJSON, _ := json.Marshal(source)
var p Produto
json.Unmarshal(sourceJSON, &p)
produtos = append(produtos, p)
}
return produtos, nil
}
Busca com Filtros
func buscarComFiltros(client *elasticsearch.Client, params BuscaParams) ([]Produto, error) {
// Bool query com must, filter, should, must_not
query := map[string]interface{}{
"query": map[string]interface{}{
"bool": map[string]interface{}{
"must": []map[string]interface{}{
{"multi_match": map[string]interface{}{
"query": params.Termo,
"fields": []string{"titulo^3", "descricao"},
}},
},
"filter": []map[string]interface{}{
// Categoria exata
{"term": map[string]interface{}{"categoria": params.Categoria}},
// Range de preço
{"range": map[string]interface{}{
"preco": map[string]interface{}{
"gte": params.PrecoMin,
"lte": params.PrecoMax,
},
}},
// Data
{"range": map[string]interface{}{
"data_criacao": map[string]interface{}{
"gte": params.DataInicio.Format(time.RFC3339),
"lte": params.DataFim.Format(time.RFC3339),
},
}},
},
},
},
"sort": []map[string]interface{}{
{params.Ordenacao: map[string]interface{}{"order": params.Ordem}},
},
"from": params.Offset,
"size": params.Limite,
}
// ... executar busca
}
Queries Avançadas
Match Phrase (Busca de Frase)
// Busca exata da frase
query := map[string]interface{}{
"query": map[string]interface{}{
"match_phrase": map[string]interface{}{
"descricao": "notebook gamer",
},
},
}
Fuzzy Search (Tolerância a Erros)
// Tolerância a erros de digitação
query := map[string]interface{}{
"query": map[string]interface{}{
"fuzzy": map[string]interface{}{
"titulo": map[string]interface{}{
"value": "iphone", // Busca "iphon", "iphonne", etc.
"fuzziness": "AUTO", // AUTO determina baseado no tamanho
},
},
},
}
Wildcard e Regexp
// Wildcard
query := map[string]interface{}{
"query": map[string]interface{}{
"wildcard": map[string]interface{}{
"titulo": "*iphone*", // Contém "iphone"
},
},
}
// Regexp
query := map[string]interface{}{
"query": map[string]interface{}{
"regexp": map[string]interface{}{
"titulo": "noteboo.*", // Começa com "noteboo"
},
},
}
Autocomplete (Prefix + Edge N-gram)
// Mapeamento para autocomplete
mappingAutoComplete := `{
"mappings": {
"properties": {
"titulo": {
"type": "text",
"analyzer": "autocomplete",
"search_analyzer": "standard"
}
}
},
"settings": {
"analysis": {
"analyzer": {
"autocomplete": {
"tokenizer": "autocomplete_tokenizer",
"filter": ["lowercase"]
}
},
"tokenizer": {
"autocomplete_tokenizer": {
"type": "edge_ngram",
"min_gram": 2,
"max_gram": 20,
"token_chars": ["letter", "digit"]
}
}
}
}
}`
// Query de autocomplete
query := map[string]interface{}{
"query": map[string]interface{}{
"match": map[string]interface{}{
"titulo": {
"query": termo,
"operator": "and",
},
},
},
"size": 10,
}
Busca por Proximidade (Geo)
type Localizacao struct {
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
}
// Mapeamento geopoint
geoMapping := `{
"mappings": {
"properties": {
"localizacao": {
"type": "geo_point"
}
}
}
}`
// Buscar próximos
query := map[string]interface{}{
"query": map[string]interface{}{
"geo_distance": map[string]interface{}{
"distance": "10km",
"localizacao": map[string]interface{}{
"lat": -23.5505, // São Paulo
"lon": -46.6333,
},
},
},
"sort": []map[string]interface{}{
{
"_geo_distance": map[string]interface{}{
"localizacao": map[string]interface{}{
"lat": -23.5505,
"lon": -46.6333,
},
"order": "asc",
"unit": "km",
"distance_type": "plane",
},
},
},
}
Agregações
Agregações Básicas
// Contagem por categoria
agg := map[string]interface{}{
"aggs": map[string]interface{}{
"por_categoria": map[string]interface{}{
"terms": map[string]interface{}{
"field": "categoria",
"size": 20,
},
},
"preco_stats": map[string]interface{}{
"stats": map[string]interface{}{
"field": "preco",
},
},
},
"size": 0, // Não retornar documentos, só agregações
}
Histograma de Preços
agg := map[string]interface{}{
"aggs": map[string]interface{}{
"histograma_precos": map[string]interface{}{
"histogram": map[string]interface{}{
"field": "preco",
"interval": 100,
},
},
},
}
Agregações Aninhadas (Sub-aggs)
agg := map[string]interface{}{
"aggs": map[string]interface{}{
"por_categoria": map[string]interface{}{
"terms": map[string]interface{}{
"field": "categoria",
},
"aggs": map[string]interface{}{
"preco_medio": map[string]interface{}{
"avg": map[string]interface{}{
"field": "preco",
},
},
"preco_max": map[string]interface{}{
"max": map[string]interface{}{
"field": "preco",
},
},
},
},
},
}
Exemplo Prático: Catálogo de Produtos
Modelo Completo
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"time"
"github.com/elastic/go-elasticsearch/v8"
"github.com/go-chi/chi/v5"
)
type Produto struct {
ID string `json:"id"`
Titulo string `json:"titulo"`
Descricao string `json:"descricao"`
Preco float64 `json:"preco"`
PrecoPromo float64 `json:"preco_promo,omitempty"`
Categoria string `json:"categoria"`
SubCategoria string `json:"sub_categoria"`
Marca string `json:"marca"`
Tags []string `json:"tags"`
Estoque int `json:"estoque"`
Rating float64 `json:"rating"`
Reviews int `json:"reviews"`
Imagens []string `json:"imagens"`
DataCriacao time.Time `json:"data_criacao"`
}
type CatalogoService struct {
es *elasticsearch.Client
}
func NewCatalogoService(es *elasticsearch.Client) *CatalogoService {
return &CatalogoService{es: es}
}
// Busca com sugestões de autocomplete
func (s *CatalogoService) Buscar(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Parâmetros da query
query := r.URL.Query().Get("q")
categoria := r.URL.Query().Get("categoria")
precoMin := parseFloat(r.URL.Query().Get("preco_min"), 0)
precoMax := parseFloat(r.URL.Query().Get("preco_max"), 100000)
ordenar := r.URL.Query().Get("ordenar") // preco, rating, relevancia
page := parseInt(r.URL.Query().Get("page"), 1)
size := parseInt(r.URL.Query().Get("size"), 20)
// Construir query
var esQuery map[string]interface{}
if query == "" {
// Match all com filtros
esQuery = map[string]interface{}{
"query": map[string]interface{}{
"bool": map[string]interface{}{
"must": []map[string]interface{}{{
"match_all": map[string]interface{}{},
}},
},
},
}
} else {
// Multi-match com boost
esQuery = map[string]interface{}{
"query": map[string]interface{}{
"bool": map[string]interface{}{
"must": []map[string]interface{}{{
"multi_match": map[string]interface{}{
"query": query,
"fields": []string{
"titulo^4", // Titulo tem maior peso
"marca^3", // Marca também é importante
"tags^2", // Tags médio
"descricao", // Descrição padrão
},
"type": "best_fields",
"tie_breaker": 0.3,
},
}},
},
},
}
}
// Adicionar filtros
filters := []map[string]interface{}{}
if categoria != "" {
filters = append(filters, map[string]interface{}{
"term": map[string]interface{}{"categoria": categoria},
})
}
filters = append(filters, map[string]interface{}{
"range": map[string]interface{}{
"preco": map[string]interface{}{
"gte": precoMin,
"lte": precoMax,
},
},
})
// Estoque disponível
filters = append(filters, map[string]interface{}{
"range": map[string]interface{}{
"estoque": map[string]interface{}{
"gt": 0,
},
},
})
if len(filters) > 0 {
esQuery["query"].(map[string]interface{})["bool"].(map[string]interface{})["filter"] = filters
}
// Ordenação
switch ordenar {
case "preco_asc":
esQuery["sort"] = []map[string]interface{}{
{"preco": "asc"},
}
case "preco_desc":
esQuery["sort"] = []map[string]interface{}{
{"preco": "desc"},
}
case "rating":
esQuery["sort"] = []map[string]interface{}{
{"rating": "desc"},
}
default:
// Relevância (score) é padrão
}
// Paginação
esQuery["from"] = (page - 1) * size
esQuery["size"] = size
// Agregações (facets)
esQuery["aggs"] = map[string]interface{}{
"categorias": map[string]interface{}{
"terms": map[string]interface{}{
"field": "categoria",
"size": 10,
},
},
"marcas": map[string]interface{}{
"terms": map[string]interface{}{
"field": "marca",
"size": 20,
},
},
"faixas_preco": map[string]interface{}{
"histogram": map[string]interface{}{
"field": "preco",
"interval": 500,
},
},
}
// Highlight (destacar termos encontrados)
if query != "" {
esQuery["highlight"] = map[string]interface{}{
"fields": map[string]interface{}{
"titulo": map[string]interface{}{},
"descricao": map[string]interface{}{
"fragment_size": 150,
},
},
}
}
// Executar busca
var buf bytes.Buffer
json.NewEncoder(&buf).Encode(esQuery)
res, err := s.es.Search(
s.es.Search.WithContext(ctx),
s.es.Search.WithIndex("produtos"),
s.es.Search.WithBody(&buf),
s.es.Search.WithTrackTotalHits(true),
)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer res.Body.Close()
if res.IsError() {
http.Error(w, res.String(), http.StatusInternalServerError)
return
}
// Decodificar resposta
var result map[string]interface{}
if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Formatar resposta
response := s.formatarResposta(result, page, size)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
func (s *CatalogoService) formatarResposta(result map[string]interface{}, page, size int) map[string]interface{} {
hits := result["hits"].(map[string]interface{})
total := int(hits["total"].(map[string]interface{})["value"].(float64))
docs := hits["hits"].([]interface{})
produtos := []map[string]interface{}{}
for _, doc := range docs {
d := doc.(map[string]interface{})
source := d["_source"]
score := d["_score"]
item := map[string]interface{}{
"data": source,
"score": score,
}
// Adicionar highlights se existirem
if highlight, ok := d["highlight"]; ok {
item["highlights"] = highlight
}
produtos = append(produtos, item)
}
// Extrair agregações
aggs := result["aggregations"].(map[string]interface{})
return map[string]interface{}{
"produtos": produtos,
"paginacao": map[string]interface{}{
"total": total,
"pagina": page,
"por_pagina": size,
"total_paginas": (total + size - 1) / size,
},
"facets": map[string]interface{}{
"categorias": extractBuckets(aggs, "categorias"),
"marcas": extractBuckets(aggs, "marcas"),
"faixas_preco": extractBuckets(aggs, "faixas_preco"),
},
}
}
func extractBuckets(aggs map[string]interface{}, key string) []map[string]interface{} {
if agg, ok := aggs[key]; ok {
buckets := agg.(map[string]interface{})["buckets"].([]interface{})
result := []map[string]interface{}{}
for _, b := range buckets {
result = append(result, b.(map[string]interface{}))
}
return result
}
return []map[string]interface{}{}
}
func main() {
// Inicializar Elasticsearch
es, err := elasticsearch.NewDefaultClient()
if err != nil {
log.Fatal(err)
}
catalogo := NewCatalogoService(es)
// Setup rotas
r := chi.NewRouter()
r.Get("/api/buscar", catalogo.Buscar)
log.Println("Servidor rodando em :8080")
http.ListenAndServe(":8080", r)
}
Performance e Otimização
1. Bulk Indexing
// Indexar em lotes de 1000 documentos
const batchSize = 1000
for i := 0; i < len(produtos); i += batchSize {
end := i + batchSize
if end > len(produtos) {
end = len(produtos)
}
if err := indexarEmBulk(client, produtos[i:end]); err != nil {
log.Printf("Erro no batch %d: %v", i/batchSize, err)
}
}
2. Connection Pooling
// Reutilizar conexões HTTP
cfg := elasticsearch.Config{
Addresses: []string{"http://localhost:9200"},
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 30 * time.Second,
},
}
3. Scroll para Grandes Volumes
// Buscar grandes volumes sem sobrecarregar memória
func scrollSearch(client *elasticsearch.Client, query map[string]interface{}) error {
ctx := context.Background()
// Iniciar scroll
res, err := client.Search(
client.Search.WithContext(ctx),
client.Search.WithIndex("produtos"),
client.Search.WithBody(encodeQuery(query)),
client.Search.WithSize(1000),
client.Search.WithScroll(5*time.Minute),
)
if err != nil {
return err
}
defer res.Body.Close()
var result map[string]interface{}
json.NewDecoder(res.Body).Decode(&result)
scrollID := result["_scroll_id"].(string)
// Processar primeira página
processHits(result["hits"].(map[string]interface{})["hits"].([]interface{}))
// Continuar scroll
for {
scrollRes, err := client.Scroll(
client.Scroll.WithContext(ctx),
client.Scroll.WithScrollID(scrollID),
client.Scroll.WithScroll(5*time.Minute),
)
if err != nil {
return err
}
defer scrollRes.Body.Close()
var scrollResult map[string]interface{}
json.NewDecoder(scrollRes.Body).Decode(&scrollResult)
hits := scrollResult["hits"].(map[string]interface{})["hits"].([]interface{})
if len(hits) == 0 {
break
}
processHits(hits)
scrollID = scrollResult["_scroll_id"].(string)
}
// Limpar scroll
client.ClearScroll(
client.ClearScroll.WithContext(ctx),
client.ClearScroll.WithScrollID(scrollID),
)
return nil
}
4. Index Templates
// Criar template para índices de logs
indexTemplate := `{
"index_patterns": ["logs-*"],
"template": {
"settings": {
"number_of_shards": 2,
"number_of_replicas": 1,
"index.lifecycle.rollover_alias": "logs"
},
"mappings": {
"properties": {
"@timestamp": {"type": "date"},
"level": {"type": "keyword"},
"message": {"type": "text"},
"service": {"type": "keyword"}
}
}
}
}`
req := esapi.IndicesPutIndexTemplateRequest{
Name: "logs-template",
Body: strings.NewReader(indexTemplate),
}
res, err := req.Do(context.Background(), client)
Próximos Passos
Conclusão
Elasticsearch é uma ferramenta poderosa para busca e analytics em Go. Este guia cobriu:
- Indexação: Bulk, templates, mapeamentos
- Busca: Full-text, filtros, fuzzy, autocomplete
- Agregações: Analytics em tempo real
- Performance: Otimizações e boas práticas
Para aplicações que precisam de busca robusta, Elasticsearch + Go é uma combinação excelente.
FAQ
Q: Quando usar Elasticsearch vs PostgreSQL Full-Text? R: Use PostgreSQL para busca simples em poucos campos. Use Elasticsearch para busca complexa, relevância, agregações e alta escala.
Q: É necessário usar Logstash? R: Não. O cliente Go pode indexar diretamente. Logstash é útil para processamento complexo de logs.
Q: Como faço backup dos dados? R: Use snapshots para S3 ou filesystem. Configure políticas de snapshot automático.
Q: Posso usar Elasticsearch como banco primário? R: Não recomendado. Use como índice secundário para busca, mantendo dados primários em SQL/NoSQL.
Q: Como escalo Elasticsearch? R: Adicione nós ao cluster. Shards são distribuídos automaticamente. Monitore com Kibana.