perf(search): normalise terms, cap cache growth and rate limit - #9
Open
Guajir0-code wants to merge 2 commits into
Open
perf(search): normalise terms, cap cache growth and rate limit#9Guajir0-code wants to merge 2 commits into
Guajir0-code wants to merge 2 commits into
Conversation
The app points Eloquent at an existing WordPress database, so no migration creates wp_posts, wp_users, wp_terms and friends. Any test that touched the database therefore could not run, and the single feature test in the repo (GET / asserting 200) failed on a missing database. Add a schema builder for the wp_* tables the app reads, small row builders, and smoke coverage for all seven routes in routes/web.php, including the highlighted-post exclusion, search, and the published/scheduled filtering the global scope is responsible for. withoutVite() keeps the suite independent of `npm run build`. ExampleTest is dropped: RoutesTest covers GET / properly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every distinct ?s= value produced its own cache entry, kept for a day, each one backed by a LIKE '%term%' scan over post_title and post_content. Nothing capped how many of those a visitor could create, and "Games", "games" and " games " were three entries for one result set. * normalise the term once, in the controller, so the query, the cache key and the value echoed back to the page all agree * ignore terms below 3 characters, which match most of the table and return nothing useful * cap terms at 60 characters * keep search results for 5 minutes instead of a day * rate limit to 20/minute per IP, applied only to requests carrying ?s= so ordinary home page traffic and pagination are untouched Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problema
Cada valor distinto de
?s=cria uma entrada de cache própria, guardada por um dia, respaldada por umLIKE '%termo%'sobrepost_titleepost_content. Nada limita quantas dessas um visitante consegue criar.Causa
O termo vai direto do
?s=para omd5(), sem normalização nem validação. Disso decorrem três coisas:Entradas duplicadas.
Games,gamesegamesproduzem três hashes, três entradas de cache e três varreduras completas — para um único conjunto de resultados.Crescimento sem limite. Um script (ou um crawler mal comportado) pedindo
?s=<aleatório>em laço gera uma entrada nova por requisição, cada uma viva por 24 horas. ComCACHE_STORE=database, que é o padrão do.env.example, isso cresce numa tabela dentro do próprio banco do WordPress.Cada miss custa uma varredura.
LIKE '%termo%'não usa índice; o%inicial obriga a percorrer a tabela. Compost_contentincluído, cada consulta lê o corpo de todos os posts.Não há rate limit em nenhuma rota do projeto.
Solução
Normalização única, no controller
Feito no controller, e não dentro do service, para que o valor usado na consulta seja o mesmo devolvido para a página. Se o service normalizasse por dentro, uma busca por
ABmostraria o termo na tela enquanto a listagem ignorava o filtro.TTL curto para busca
Resultado de busca é indexado por entrada arbitrária de visitante: são muitos, e cada um só interessa a quem digitou aquele termo exato. Cinco minutos absorvem repetição sem acumular.
Rate limit apenas na busca
Aplicado com
->middleware('throttle:search')na rota da home. Tráfego normal, incluindo paginação, não é afetado — só requisições que carregam?s=.Como validar
A cobertura inclui a tabela de normalização (caixa, espaços, nulo, vazio, curto demais), o corte de comprimento, busca funcionando, insensibilidade a caixa, termo curto sendo tratado como "sem busca", e o rate limit — este último confirmando também que 25 requisições à home sem
?s=passam sem serem limitadas.Verificado nos dois sentidos: contra o código anterior, 13 dos 14 falham.
Impacto
Um teste existente em
RoutesTestfoi ajustado: ele esperavasearchTermde volta na caixa original. A normalização é intencional, então a expectativa passou a ser o termo normalizado.Fora de escopo: o índice FULLTEXT
A causa raiz do custo por consulta é o
LIKE '%termo%', e a correção adequada seria um índiceFULLTEXTsobrepost_titleepost_contentcomMATCH ... AGAINST.Deixei de fora deliberadamente, por dois motivos:
ALTER TABLE wp_postsno banco de produção do WordPress. É a única mudança de schema que o trabalho de auditoria identificou, e precisa de janela de baixo tráfego e decisão de quem opera o banco.FULLTEXTdo MySQL. Entregar um caminho de código exercitado apenas em produção seria pior do que não entregá-lo.O comando, para quando houver decisão:
É aditivo e o WordPress ignora índices que não conhece. O rollback é
DROP INDEX ft_post_search ON wp_posts.As proteções deste PR reduzem bastante a frequência com que a varredura acontece, mas não mudam o custo de cada uma.
Também fora de escopo