.env.
Lição 0: do zero ao primeiro comando (15 min)
FT.AGGREGATE: a economia do reino¶
Preço médio por raridade, item mais caro por tipo, tabela de liquidação com desconto: fazer isso na aplicação significa puxar todos os documentos e somar em Java. FT.AGGREGATE roda o pipeline no servidor, sobre o mesmo índice da lição anterior: agrupa (GROUPBY), reduz (REDUCE COUNT, AVG, MAX), calcula campos (APPLY), filtra, ordena e pagina. Só as linhas finais viajam pela rede.
O que o lab faz¶
- Garantir que o índice
{p}:idx:itemsexiste (a lição cria com o mesmo schema se faltar) - Agrupar por raridade com
COUNTeAVG price, ordenado pela média - Achar o
MAX pricede cada tipo - Calcular
sale_price = price * 0.9comAPPLYe paginar comLIMIT - Filtrar linhas do pipeline com
FILTER "@level >= 40"
Faça agora¶
./quest run 201-02 jedis
./quest run 201-02 lettuce # opcional: mesmo lab, outro client
./quest verify 201-02
O código¶
AggregationResult byRarity = jedis.ftAggregate(index, new AggregationBuilder("*")
.groupBy("@rarity", Reducers.count().as("n"), Reducers.avg("@price").as("avg_price"))
.sortBy(SortedField.desc("@avg_price")));
for (Row row : byRarity.getRows())
System.out.println(row.getString("rarity") + " " + row.getLong("n") + " " + row.getDouble("avg_price"));
AggregationResult byType = jedis.ftAggregate(index, new AggregationBuilder("*")
.groupBy("@type", Reducers.max("@price").as("max_price"), Reducers.count().as("n"))
.sortBy(SortedField.desc("@max_price")));
AggregationResult sale = jedis.ftAggregate(index, new AggregationBuilder("@type:{arma}")
.load("@name", "@price")
.apply("@price * 0.9", "sale_price")
.sortBy(SortedField.desc("@price"))
.limit(0, 5));
AggregationResult veterans = jedis.ftAggregate(index, new AggregationBuilder("*")
.load("@name", "@level", "@rarity")
.filter("@level >= 40")
.sortBy(SortedField.desc("@level"))
.limit(0, 20));
AggregationReply<String, String> byRarity = redis.ftAggregate(index, "*",
AggregateArgs.<String, String>builder()
.groupBy(GroupBy.<String, String>of("@rarity")
.reduce(Reducer.<String, String>count().as("n"))
.reduce(Reducer.<String, String>avg("@price").as("avg_price")))
.sortBy(SortBy.of("@avg_price", SortDirection.DESC))
.build());
for (SearchReply<String, String> page : byRarity.getReplies())
for (SearchReply.SearchResult<String, String> row : page.getResults())
System.out.println(row.getFields()); // {rarity=lendario, n=2, avg_price=48000}
redis.ftAggregate(index, "@type:{arma}", AggregateArgs.<String, String>builder()
.load("@name").load("@price")
.apply("@price * 0.9", "sale_price")
.sortBy(SortBy.of("@price", SortDirection.DESC))
.limit(0, 5)
.build());
redis.ftAggregate(index, "*", AggregateArgs.<String, String>builder()
.load("@name").load("@level").load("@rarity")
.filter("@level >= 40")
.sortBy(SortBy.of("@level", SortDirection.DESC))
.limit(0, 20)
.build());
No Redis Insight¶
Nada novo aparece no Browser: agregação lê o índice e os documentos, não escreve chave nenhuma. No Workbench, cole FT.AGGREGATE {p}:idx:items "*" GROUPBY 1 @rarity REDUCE COUNT 0 AS n REDUCE AVG 1 @price AS avg_price SORTBY 2 @avg_price DESC: o Insight mostra o resultado como tabela, uma linha por raridade. No Profiler, rode a lição e compare o pipeline que o builder montou com o comando mostrado no console: é o mesmo, argumento por argumento.
Por dentro
O pipeline roda na ordem em que você escreve as etapas:
| Etapa | O que faz | Na lição |
|---|---|---|
| query | filtra documentos pelo índice, antes de tudo | "*", "@type:{arma}" |
LOAD n @campo ... |
traz campos do documento para o pipeline (só o que precisa) | LOAD 2 @name @price |
GROUPBY n @campo REDUCE ... |
agrupa e reduz: COUNT 0, AVG 1 @price, MAX, MIN, SUM, COUNT_DISTINCT, QUANTILE, TOLIST |
GROUPBY 1 @rarity REDUCE COUNT 0 AS n REDUCE AVG 1 @price AS avg_price |
APPLY "expr" AS alias |
campo calculado por linha: aritmética, round, upper, funções de data |
APPLY "@price * 0.9" AS sale_price |
FILTER "expr" |
filtra linhas do pipeline, depois de LOAD, APPLY ou GROUPBY | FILTER "@level >= 40" |
SORTBY n @campo DESC |
ordena as linhas; MAX k limita quantas ordenar |
SORTBY 2 @avg_price DESC |
LIMIT offset count |
pagina; sem LIMIT o padrão é 0 10 |
LIMIT 0 5, LIMIT 0 20 |
O resultado são linhas (mapas campo, valor), não documentos: no Jedis, AggregationResult.getRows() e Row.getString, getLong, getDouble; no Lettuce, AggregationReply.getReplies() traz páginas de SearchReply, e cada resultado expõe getFields().
FILTER "@level >= 40" e @level:[40 +inf] na query dão o mesmo resultado aqui, mas não custam o mesmo: a query usa o índice e descarta documentos antes de carregar qualquer coisa; o FILTER avalia linha a linha depois do LOAD. Use FILTER para campos calculados (@n > 8 depois de um GROUPBY) e deixe o resto para a query.
Em produção
- Filtre na query sempre que o campo estiver no índice; reserve
FILTERpara o que só existe depois deAPPLYouGROUPBY. - Resultados grandes pedem cursor em vez de
LIMITgigante:WITHCURSOR COUNT 500eFT.CURSOR READ(JedisftAggregateIteratoroucursor(...); LettucewithCursor(...)eftCursorread). - Agregação gasta CPU do shard. Um painel que roda o mesmo GROUPBY a cada segundo merece um cache com TTL (uma chave JSON ou STRING com o resultado) em vez de bater no índice toda vez.
Desafio
Acrescente um terceiro reducer ao grupo por raridade, Reducers.min("@price").as("min_price") no Jedis ou Reducer.<String, String>min("@price").as("min_price") no Lettuce, e ordene por @n decrescente. Esperado: comum e raro empatam com 11 itens; o min_price de comum é 10 (Anel de Cobre) e o de lendario é 46000.