Lógica de busca com cache em Python usando Redis

Rocketseat

Rocketseat

5 min de leitura
https://prod-files-secure.s3.us-west-2.amazonaws.com/08f749ff-d06d-49a8-a488-9846e081b224/d764d489-2345-44be-abda-26d193088d1a/logica-busca-cache-redis-python.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466SIJRIU4I%2F20260624%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20260624T115922Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEGsaCXVzLXdlc3QtMiJHMEUCIFUqJz8jNObvqd%2B%2BRNvsygnqckjcb3IxKCvsKtes2IDeAiEA3gkw8XXFScdxpmHx9qlTRmBfkGvY4Zh%2BrrxqeR5g6Jkq%2FwMINBAAGgw2Mzc0MjMxODM4MDUiDNNJbhoaCbjky3P1XCrcA7KdRHoYChA%2FpEqehA4gEoKbR3eoraF%2FFcZ7HREXlQArMfVeVzIa%2BbV3L9nA9BQKKOTrJ3JQCTCurxJiQ19KOVkNUJtTvHEl%2BIgRyB%2FjkQfYeaHIaJf8eXYFA02IzQUO0fnGSKzYu%2BY4ERqMRMrqS5uX50u2Vijvt71pf9JXyxk2nFslGpIaSGYeoP54wW5QX9IQmgpc6Ausf%2FEsFt29BmyY2HnSszKf4ydq4%2BqtX5aFrbqlEXuAnOmTPdVUOrY84Hx%2BrXAH2q2iVWNiD7T2qRcPfYXAZNzC3c4OrNFV5PKOMaUJCBq%2BfxcUjgHlpJEAl%2F42cFv2YGps6gfPzrl9lEkDPtRdt9A%2Fq9R6cmN3Sp0BUIecD3q8XMPuAgjHtIRiBbtpPw22%2Ffgz3ZO8q76oRx00wu%2BnY1rHfGzQ17fFcFWYlnbNmB7VPnfbtZpinuahdyjrkYD%2FJ0tI%2B5OnNyU8RjWDKswGDQhG7%2BvPYhTrjLQhPRr8NX4qZ%2FDNUaiu8sJt3MszYUrPj8sq%2FCT1yFT8ROxkm7sLPJqpd8RwLMdox9JSTFTs%2BzuqxnPcQNbPBeiSiOKHzvi74zGBWwSdVE%2BLi6M1%2FdozMR1lCrCzdH4DI%2BAtvoBmUj1p%2B4IV0OpwMNfl7tEGOqUBawWhib0idBjR6jML1wuDE6XhzO7fLqTt2Cf9lc8fygJvBnIxsbmPVaCm6T1KV%2FrRAIKIRJWGdVVoYlBVnSuLtHMAQvswNGrKXRucnlqLMt5v5B7Xg3aavArTlvb9ygxEM6c7387jFov11cSxLnnHBHaEobJb3CJMf2xfCsCunM%2FENGrSl8oKVkw3CzhIP6GcnygGV652yu%2B%2BfV3auTyFusKCjtZk&X-Amz-Signature=baa210bf7feff994d07acb017d00aef8d75e1b09cfe4c193960e90394d7c8516&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject
Quando milhões de usuários fazem buscas simultâneas, a velocidade deixa de ser um diferencial e passa a ser uma exigência. Imagine seu servidor de banco de dados sobrecarregado enquanto a mesma consulta é executada 500 vezes por segundo. É exatamente nesse cenário que o Redis se torna indispensável, transformando buscas repetidas em operações quase instantâneas.
A ideia desse guia é você entender como construir um sistema de busca inteligente que aproveita o cache distribuído do Redis para servir resultados em milissegundos, mantendo os dados sempre atualizados. Vamos explorar padrões avançados, estratégias de invalidação e implementações prontas para produção.

Entendendo o problema: por que cache importa em buscas

Toda busca tradicional segue o mesmo fluxo: recebe a query, executa SQL complexo, processa os dados e retorna ao cliente. Quando a mesma busca ocorre novamente nos próximos minutos, todo esse trabalho é repetido desnecessariamente.
O Redis resolve esse problema mantendo os resultados mais populares na memória RAM. Em vez de consultar o banco de dados, você obtém a resposta do cache em menos de 1 milissegundo. A lógica é direta: se 80% das suas buscas são repetições de 20% das queries, você elimina uma quantidade expressiva de processamento redundante.
A vantagem vai além da velocidade. Você também reduz a carga no banco de dados, diminui custos de infraestrutura e entrega uma experiência de usuário notavelmente melhor. É como substituir uma busca em um arquivo gigante por um índice já memorizado.

Implementando busca com cache básico

O ponto de partida é o padrão mais direto: armazenar resultados de busca como strings JSON no Redis. Cada query única recebe uma chave no formato busca:{query}:{parametros}, garantindo que diferentes combinações de parâmetros não colidam.
import redis import json from typing import List, Dict, Any class BasicSearchCache: def __init__(self): self.redis = redis.Redis( host='localhost', port=6379, decode_responses=True ) def buscar(self, query: str, limite: int = 10) -> List[Dict]: # Constrói chave de cache única chave_cache = f"busca:{query.lower()}:{limite}" # Tenta recuperar resultado armazenado resultado_cache = self.redis.get(chave_cache) if resultado_cache: return json.loads(resultado_cache) # Se não existe, consulta o banco de dados resultados = self._consultar_banco(query, limite) # Armazena por 1 hora self.redis.setex( chave_cache, 3600, json.dumps(resultados) ) return resultados def _consultar_banco(self, query: str, limite: int) -> List[Dict]: # Sua lógica de busca real aqui pass
Um detalhe importante: normalize a query antes de gerar a chave. Tratar "Python" e "python" como entradas distintas gera duplicação desnecessária no cache e reduz a taxa de acerto.

Padrões avançados: cache com invalidação inteligente

Armazenar resultados é a parte simples. O verdadeiro desafio é manter o cache consistente quando os dados mudam. As estratégias a seguir são testadas em produção.

Invalidação baseada em tempo (TTL)

O padrão mais simples é definir um tempo de vida para cada entrada. Dados que mudam raramente podem usar TTL de 1 hora; informações mais voláteis exigem janelas de 5 minutos.
class SearchWithTTL: def __init__(self): self.redis = redis.Redis(decode_responses=True) self.ttl_por_tipo = { 'usuarios': 300, # 5 minutos 'produtos': 1800, # 30 minutos 'artigos': 3600 # 1 hora } def buscar(self, tipo: str, query: str) -> List[Dict]: chave = f"busca:{tipo}:{query.lower()}" resultado = self.redis.get(chave) if resultado: return json.loads(resultado) resultados = self._executar_busca(tipo, query) ttl = self.ttl_por_tipo.get(tipo, 600) self.redis.setex(chave, ttl, json.dumps(resultados)) return resultados def _executar_busca(self, tipo: str, query: str) -> List[Dict]: pass

Invalidação reativa por eventos

Quando um usuário atualiza um produto, você invalida imediatamente o cache associado. Essa abordagem é muito mais eficiente do que aguardar o TTL expirar naturalmente.
class SearchWithEventInvalidation: def __init__(self): self.redis = redis.Redis(decode_responses=True) def buscar(self, query: str) -> List[Dict]: chave = f"busca:{query.lower()}" resultado = self.redis.get(chave) if resultado: return json.loads(resultado) resultados = self._executar_busca(query) self.redis.setex(chave, 3600, json.dumps(resultados)) return resultados def atualizar_produto(self, produto_id: int, dados: Dict) -> None: # Atualiza o banco de dados self._salvar_banco(produto_id, dados) # Invalida os caches afetados pela mudança self._invalidar_caches_relacionados(produto_id) def _invalidar_caches_relacionados(self, produto_id: int) -> None: # Localiza todas as chaves potencialmente afetadas chaves_pattern = self.redis.keys("busca:*") for chave in chaves_pattern: # Aplica lógica para determinar se o cache é afetado self.redis.delete(chave)
Atenção: o uso de KEYS com padrão curinga em produção pode bloquear o Redis em bases grandes. Prefira SCAN para iteração segura e não bloqueante.

Otimizações avançadas: busca facetada com cache

Buscas complexas com múltiplos filtros, como as de e-commerce, exigem uma abordagem mais sofisticada. O cache simples não cobre todas as combinações possíveis de parâmetros.
class FacetedSearchCache: def __init__(self): self.redis = redis.Redis(decode_responses=True) def buscar( self, query: str, categoria: str = None, preco_min: int = None, preco_max: int = None, pagina: int = 1 ) -> Dict[str, Any]: # Constrói chave determinística a partir dos filtros filtros = f"{categoria or 'all'}:{preco_min or 0}:{preco_max or 'inf'}" chave = f"busca:facetada:{query.lower()}:{filtros}:{pagina}" resultado = self.redis.get(chave) if resultado: return json.loads(resultado) dados = self._buscar_facetado(query, categoria, preco_min, preco_max, pagina) self.redis.setex(chave, 1800, json.dumps(dados)) return dados def _buscar_facetado( self, query: str, categoria: str, preco_min: int, preco_max: int, pagina: int ) -> Dict[str, Any]: pass
A chave determinística garante que a mesma combinação de filtros sempre acesse o mesmo registro no cache, evitando duplicações e inconsistências.

Monitorando performance e cache hits

Sem métricas, você não sabe se o cache está cumprindo seu papel. Implemente contadores básicos desde o início.
class MonitoredSearchCache: def __init__(self): self.redis = redis.Redis(decode_responses=True) self.stats_key = "cache:stats" def buscar(self, query: str) -> List[Dict]: chave = f"busca:{query.lower()}" # Incrementa o total de tentativas self.redis.incr(f"{self.stats_key}:tentativas") resultado = self.redis.get(chave) if resultado: self.redis.incr(f"{self.stats_key}:hits") return json.loads(resultado) # Registra miss e executa busca real self.redis.incr(f"{self.stats_key}:misses") resultados = self._executar_busca(query) self.redis.setex(chave, 3600, json.dumps(resultados)) return resultados def obter_taxa_acerto(self) -> float: hits = int(self.redis.get(f"{self.stats_key}:hits") or 0) tentativas = int(self.redis.get(f"{self.stats_key}:tentativas") or 1) return (hits / tentativas) * 100 if tentativas > 0 else 0 def _executar_busca(self, query: str) -> List[Dict]: pass
Uma taxa de acerto abaixo de 60% geralmente indica que os TTLs estão muito curtos ou que as chaves de cache não estão sendo geradas de forma consistente.

Melhores práticas e armadilhas a evitar

Use chaves versionadas. Se o schema dos seus dados mudar, incluir uma versão na chave (como busca:v2:{query}) evita que resultados obsoletos sejam retornados para os usuários.
Defina TTLs adequados ao contexto. Dados que raramente mudam podem permanecer em cache por horas; informações dinâmicas precisam de janelas de minutos. Não use o mesmo TTL para tudo.
Prefira invalidação seletiva. Remover apenas os caches afetados por uma mudança é muito mais eficiente do que limpar toda a base de cache de uma vez.
Evite armazenar objetos muito grandes. Se seus resultados chegam a megabytes, o Redis fica sobrecarregado e o ganho de performance se perde. Considere armazenar apenas os IDs e buscar os detalhes do banco sob demanda.
Monitore continuamente. Taxa de acerto baixa é um sinal claro de que a estratégia de cache precisa ser revisada. Ferramentas como RedisInsight facilitam essa análise em tempo real.

Cache que funciona de verdade

Redis não é mágica, é matemática aplicada. Quando você entende que 80% das buscas são repetições, otimizar essas operações gera retorno imediato e mensurável. Comece com TTL fixo, depois evolua para invalidação reativa conforme aprende os padrões de uso da sua aplicação.
Implemente métricas desde o primeiro dia, ajuste os TTLs com base em dados reais e invalide de forma seletiva. Seu banco de dados vai operar com muito menos pressão, e seus usuários vão perceber a diferença na velocidade.
documentação oficial do Redis cobre todos os comandos e padrões necessários para levar essa implementação ao ambiente de produção com segurança.

Próximos passos

Dominar cache em Python é fundamental para construir aplicações que escalam. Se você quer aprofundar seus conhecimentos em Python e também aprender outras tecnologias essenciais para desenvolvimento backend, confira a Formação em Python da Rocketseat. Lá você vai encontrar conteúdo prático e estruturado para levar suas aplicações do desenvolvimento ao ambiente de produção.
 

Conheça o Rocketseat Para Empresas

Oferecemos soluções personalizadas para empresas de todos os portes.

Rocketseat

Rocketseat

Ecossistema de educação contínua referência em programação e Inteligência Artificial.

Artigos_

Explore conteúdos relacionados

Descubra mais artigos que complementam seu aprendizado e expandem seu conhecimento.

Imagem contendo uma carta e um símbolo de check
NewsletterReceba conteúdos inéditos e novidades gratuitamente