diff --git a/docs.json b/docs.json
index 920c86ab..f9348742 100644
--- a/docs.json
+++ b/docs.json
@@ -868,6 +868,7 @@
"redis/tutorials/python_url_shortener",
"redis/tutorials/api_with_cdk",
"redis/tutorials/agent_memory",
+ "redis/tutorials/analytics_with_redis",
"redis/tutorials/auto_complete_with_serverless_redis",
"redis/tutorials/aws_app_runner_with_redis",
"redis/tutorials/cloud_run_sessions",
diff --git a/llms-full.txt b/llms-full.txt
index 9884e52c..30bb92cd 100644
--- a/llms-full.txt
+++ b/llms-full.txt
@@ -18849,6 +18849,39 @@ We believe that Upstash is the best storage for your Lambda Functions because:
* Designed for low latency data access
* The lovely simple RedisĀ® API
+## Detailed Tutorials
+
+
+
+ Count and query events with counters, bitmaps, and Redis Search
+
+
+ Short-term and long-term memory for AI agents on Upstash Redis
+
+
+ Speed up your Laravel application with Upstash Redis caching
+
+
+ Build realtime apps on Cloudflare Workers backed by Upstash Redis
+
+
+
- [AWS Lambda](https://upstash.com/docs/redis/quickstarts/aws-lambda.md)
- [Azure Functions](https://upstash.com/docs/redis/quickstarts/azure-functions.md)
- [Cloudflare Workers](https://upstash.com/docs/redis/quickstarts/cloudflareworkers.md)
@@ -24581,6 +24614,39 @@ SEARCH.COUNT products '{"price": {"$lt": 150}}'
+## Next Steps
+
+
+
+ Define the fields you want to index and how they are matched
+
+
+ Learn the JSON-based query language with filters and operators
+
+
+ Group and summarize your indexed data with aggregation pipelines
+
+
+ Complete, real-world examples you can adapt to your own use cases
+
+
+
# Indices
Source: https://upstash.com/docs/redis/search/index-management
@@ -25155,6 +25221,39 @@ Upstash Redis Search is our first extension beyond the official Redis spec. Usin
regex support, and more.
* **Production-Ready Performance**: Built on Tantivy, a fast full-text search engine library written in Rust
+## Next Steps
+
+
+
+ Create your first index and run a query in a few lines of code
+
+
+ Define the fields you want to index and how they are matched
+
+
+ Learn the JSON-based query language with filters and operators
+
+
+ Complete, real-world examples you can adapt to your own use cases
+
+
+
# $boost
Source: https://upstash.com/docs/redis/search/query-operators/boolean-operators/boost
@@ -28755,6 +28854,320 @@ wait isn't needed.
[Smooth Text Streaming in AI SDK v5](https://upstash.com/blog/smooth-streaming).
* Learn more about what Redis Search can do in the [Search docs](/docs/redis/search/introduction).
+# Building Analytics with Redis
+Source: https://upstash.com/docs/redis/tutorials/analytics_with_redis
+
+Most teams reach for a dedicated analytics product the moment they want to count
+something. But if you already run Redis (for caching, sessions, or rate limiting),
+you are sitting on one of the best analytics engines available.
+
+Why Redis?
+
+* **It's already there.** No new vendor, no new pipeline, no nightly ETL job. You
+ write events from the same code that serves your requests.
+* **It's fast.** Counters, sets, and bitmaps are O(1) or close to it. You can
+ increment a metric on every request without thinking about it, and read the
+ result back in single-digit milliseconds.
+* **It's serverless-friendly.** With [Upstash Redis](https://upstash.com/) you talk
+ to it over HTTP, so it works from edge functions, Lambdas, and the browser-facing
+ routes of a Next.js app, exactly where analytics events originate.
+
+There are two broad philosophies for doing analytics on Redis. This tutorial walks
+through both, when to use each, and the tooling we've built to make the second one
+painless.
+
+## Two philosophies
+
+There are two ways to think about this:
+
+1. **Plan ahead.** Decide what you want to measure up front and record it in a
+ compact structure (like a bitmap) built to answer that exact question.
+2. **Record everything.** Capture each event with its metadata, index it with Redis
+ Search, and figure out the questions later.
+
+| | Plan ahead | Record everything |
+| --- | --- | --- |
+| **You decide...** | the questions up front | the questions later |
+| **Storage** | counters, bitmaps, sets | event documents (JSON) |
+| **Cost** | tiny, fixed | grows with event volume |
+| **Querying** | read the counter | filter / aggregate with Redis Search |
+| **Good for** | DAU, funnels, feature usage | ad-hoc product analytics |
+
+
+You don't have to pick one. Many apps use bitmaps for the handful of metrics they
+watch daily, and event recording for everything they might want to explore later.
+
+
+## Philosophy 1: Plan ahead
+
+If you know in advance what you want to measure, you can record it in a structure
+that answers that exact question for almost no storage. The classic example is the
+**bitmap**: one bit per user, per day.
+
+### Daily active users with a bitmap
+
+A bitmap is a string where you can flip individual bits by offset. Use the user ID
+as the offset and you get a per-day "did this user show up?" record that costs one
+bit per user, roughly **1.2 MB for 10 million users**.
+
+```ts
+import { Redis } from "@upstash/redis";
+
+const redis = Redis.fromEnv();
+
+// Mark user 1234 as active today
+function markActive(userId: number, day = today()) {
+ return redis.setbit(`active:${day}`, userId, 1);
+}
+
+// How many unique users were active today?
+function dailyActiveUsers(day = today()) {
+ // bitcount accepts a key alone at runtime, but the TS SDK wants an
+ // explicit byte range; 0..-1 covers the whole bitmap.
+ return redis.bitcount(`active:${day}`, 0, -1);
+}
+
+function today() {
+ return new Date().toISOString().slice(0, 10); // "2024-06-15"
+}
+```
+
+`setbit` is O(1) and `bitcount` counts the set bits in one pass. Done.
+
+### Combining days: weekly actives and retention
+
+Because each day is its own bitmap, you can answer questions about *ranges* of days
+with bitwise operations, without storing anything extra.
+
+```ts
+// Weekly active users: OR the last 7 daily bitmaps together
+async function weeklyActiveUsers(days: string[]) {
+ if (days.length === 0) return 0;
+ const [first, ...rest] = days.map((d) => `active:${d}`);
+ await redis.bitop("or", "active:week", first, ...rest);
+ return redis.bitcount("active:week", 0, -1);
+}
+
+// Retention: users active on BOTH day A and day B
+async function retained(dayA: string, dayB: string) {
+ await redis.bitop("and", "tmp:retained", `active:${dayA}`, `active:${dayB}`);
+ return redis.bitcount("tmp:retained", 0, -1);
+}
+```
+
+`OR` gives you "active on any of these days", `AND` gives you "active on all of
+them", the building blocks of retention and funnel analysis. The same idea powers
+feature-adoption flags: keep a bitmap per feature and `AND` it against your active
+users to see adoption.
+
+### When this breaks down
+
+The catch is right there in the name: you have to **plan ahead**. A bitmap answers
+the one question you designed it for. The moment someone asks "okay, but how many of
+those users were on mobile, in Germany, on the new checkout flow?" you're stuck:
+that dimension was never recorded. You'd need to have created a separate bitmap for
+every combination in advance, which doesn't scale.
+
+That's where the second philosophy comes in.
+
+## Philosophy 2: Record events, query later
+
+Instead of deciding the questions up front, record each event as a document with
+whatever metadata you have on hand, index it with
+[Redis Search](/docs/redis/search/introduction), and ask your questions *afterwards*.
+
+The flow is:
+
+1. Write each event as a JSON document under a known key prefix.
+2. Define a search index over that prefix once.
+3. Query and aggregate however you like (filters, ranges, group-bys) without
+ having planned for any specific question.
+
+### Define the index
+
+You define the index a single time. Redis Search then automatically picks up any
+key matching the prefix; there is no separate "insert into index" step.
+
+```ts
+import { Redis, s } from "@upstash/redis";
+
+const redis = Redis.fromEnv();
+
+const events = await redis.search.createIndex({
+ name: "events-idx",
+ prefix: "event:",
+ dataType: "json",
+ existsOk: true, // don't throw if the index already exists
+ schema: s.object({
+ name: s.keyword(), // "pageview", "signup", "purchase"
+ path: s.keyword(), // "/pricing"
+ country: s.keyword(), // "DE"
+ device: s.keyword(), // "mobile"
+ amount: s.number("F64"), // for purchase events
+ ts: s.date(),
+ }),
+});
+```
+
+### Record events
+
+Events are just JSON written with a regular Redis command. The index picks them up
+on its own.
+
+```ts
+async function track(event: {
+ name: string;
+ path?: string;
+ country?: string;
+ device?: string;
+ amount?: number;
+}) {
+ const id = crypto.randomUUID();
+ await redis.json.set(`event:${id}`, "$", {
+ ...event,
+ ts: new Date().toISOString(),
+ });
+}
+
+await track({ name: "pageview", path: "/pricing", country: "DE", device: "mobile" });
+await track({ name: "purchase", amount: 49.0, country: "DE", device: "mobile" });
+```
+
+
+Indexing is asynchronous. In a long-running app you don't need to think about it,
+but in a tight loop, like a script or a test that writes events and immediately
+queries them, call `await events.waitIndexing()` first so the documents you just
+wrote are searchable:
+
+```ts
+await events.waitIndexing();
+```
+
+
+### Now ask anything
+
+Here's the payoff. None of these queries needed to be anticipated when you recorded
+the events.
+
+```ts
+// How many mobile pageviews from Germany?
+const { count } = await events.count({
+ filter: {
+ $and: [
+ { name: { $eq: "pageview" } },
+ { device: { $eq: "mobile" } },
+ { country: { $eq: "DE" } },
+ ],
+ },
+});
+
+// Total revenue and average order value, broken down by country
+const revenue = await events.aggregate({
+ filter: { name: { $eq: "purchase" } },
+ aggregations: {
+ by_country: {
+ $terms: { field: "country", size: 20 },
+ $aggs: {
+ total: { $avg: { field: "amount" } },
+ orders: { $count: { field: "amount" } },
+ },
+ },
+ },
+});
+
+// Top pages this week
+const topPages = await events.aggregate({
+ filter: { ts: { $gte: "2024-06-10T00:00:00Z" } }, // s.date() expects an RFC 3339 string
+ aggregations: {
+ pages: { $terms: { field: "path", size: 10 } },
+ },
+});
+```
+
+Filters, numeric ranges, date ranges, group-bys (`$terms`), histograms, facets,
+percentiles: all available, all decided at query time. See the
+[querying](/docs/redis/search/querying) and [aggregating](/docs/redis/search/aggregations)
+docs for the full set.
+
+## The complexity, and a PoC that hides it
+
+The event-recording approach is more flexible, but it does come with moving parts
+that the simple examples above gloss over:
+
+* **Schema management**: keeping the index schema in sync as your events evolve.
+* **Capturing events from the frontend**: you need an endpoint, batching, and a
+ client to send events without slowing down the page.
+* **Sessions and context**: tying events together and attaching shared metadata.
+* **Exploring the data**: a query API is not a dashboard.
+
+To explore how far this can be smoothed over, we built a proof-of-concept SDK,
+[**`@upstash/redis-analytics`**](https://github.com/upstash/redis-analytics), that packages these pieces:
+
+* A **ready-to-use React hook** that auto-creates a session and captures pageviews.
+* A single **backend endpoint** you drop into your app (e.g. a Next.js route).
+* **Middleware** hooks for resolving feature flags and attaching server-side context.
+* A **schema registry** that infers field types from your events and maintains the
+ Redis Search index for you.
+* An **admin dashboard** for exploring captured analytics.
+
+A minimal end-to-end wiring looks like this:
+
+```ts
+// lib/analytics.ts (backend client)
+import { AnalyticsBackendClient } from "@upstash/redis-analytics";
+
+export const analytics = new AnalyticsBackendClient({
+ redis: {
+ url: process.env.UPSTASH_REDIS_REST_URL!,
+ token: process.env.UPSTASH_REDIS_REST_TOKEN!,
+ },
+});
+```
+
+```ts
+// app/api/analytics/route.ts (the single endpoint)
+import { analytics } from "@/lib/analytics";
+
+const handler = analytics.getHandler();
+export const POST = handler;
+export const GET = handler;
+```
+
+```tsx
+// frontend (the hook)
+"use client";
+import { createAnalyticsHook } from "@upstash/redis-analytics/react";
+
+const useAnalytics = createAnalyticsHook({ endpoint: "/api/analytics" });
+
+export function BuyButton() {
+ const { captureEvent, sessionId } = useAnalytics();
+ return (
+
+ );
+}
+```
+
+## Which should you use?
+
+* Reach for **bitmaps and counters** when you have a short, stable list of metrics
+ you watch every day. They're nearly free and answer instantly.
+* Reach for **event recording with Redis Search** when you want to explore your data
+ and can't predict every question in advance.
+
+Both run on the Redis you already have. Start with whichever matches the questions
+you have today; you can always add the other later.
+
# Deploy a Serverless API with AWS CDK and AWS Lambda
Source: https://upstash.com/docs/redis/tutorials/api_with_cdk
diff --git a/llms.txt b/llms.txt
index 15a1b40a..af527dbb 100644
--- a/llms.txt
+++ b/llms.txt
@@ -788,6 +788,7 @@
- [Connecting with Read-Only Access](https://upstash.com/docs/redis/troubleshooting/readonly_connection.md)
- [ERR XReadGroup is cancelled](https://upstash.com/docs/redis/troubleshooting/stream_pel_limit.md)
- [Agent Memory with Redis Search](https://upstash.com/docs/redis/tutorials/agent_memory.md): Build short-term and long-term memory for AI agents on Upstash Redis. Store working memory with TTLs and recall long-term memories with Redis Search full-text queries.
+- [Building Analytics with Redis](https://upstash.com/docs/redis/tutorials/analytics_with_redis.md): Two approaches to building analytics on Upstash Redis: pre-planned counters with bitmaps, and flexible event recording queried with Redis Search.
- [Deploy a Serverless API with AWS CDK and AWS Lambda](https://upstash.com/docs/redis/tutorials/api_with_cdk.md)
- [Autocomplete API with Serverless Redis](https://upstash.com/docs/redis/tutorials/auto_complete_with_serverless_redis.md)
- [Build Stateful Applications with AWS App Runner and Serverless Redis](https://upstash.com/docs/redis/tutorials/aws_app_runner_with_redis.md): This tutorial shows how to create a serverless and stateful application using AWS App Runner and Redis
diff --git a/redis/overall/usecases.mdx b/redis/overall/usecases.mdx
index 89564f08..2d36181a 100755
--- a/redis/overall/usecases.mdx
+++ b/redis/overall/usecases.mdx
@@ -64,3 +64,36 @@ We believe that Upstash is the best storage for your Lambda Functions because:
- Serverless just like Lambda functions itself
- Designed for low latency data access
- The lovely simple RedisĀ® API
+
+## Detailed Tutorials
+
+
+
+ Count and query events with counters, bitmaps, and Redis Search
+
+
+ Short-term and long-term memory for AI agents on Upstash Redis
+
+
+ Speed up your Laravel application with Upstash Redis caching
+
+
+ Build realtime apps on Cloudflare Workers backed by Upstash Redis
+
+
diff --git a/redis/search/getting-started.mdx b/redis/search/getting-started.mdx
index 441b439e..07c00430 100644
--- a/redis/search/getting-started.mdx
+++ b/redis/search/getting-started.mdx
@@ -165,3 +165,36 @@ SEARCH.COUNT products '{"price": {"$lt": 150}}'
+
+## Next Steps
+
+
+
+ Define the fields you want to index and how they are matched
+
+
+ Learn the JSON-based query language with filters and operators
+
+
+ Group and summarize your indexed data with aggregation pipelines
+
+
+ Complete, real-world examples you can adapt to your own use cases
+
+
diff --git a/redis/search/introduction.mdx b/redis/search/introduction.mdx
index b1b7d8a5..1a4619da 100644
--- a/redis/search/introduction.mdx
+++ b/redis/search/introduction.mdx
@@ -11,3 +11,36 @@ Upstash Redis Search is our first extension beyond the official Redis spec. Usin
- **Intuitive Query Language**: A type-safe JSON-based query syntax with boolean operators, fuzzy matching, phrase queries,
regex support, and more.
- **Production-Ready Performance**: Built on Tantivy, a fast full-text search engine library written in Rust
+
+## Next Steps
+
+
+
+ Create your first index and run a query in a few lines of code
+
+
+ Define the fields you want to index and how they are matched
+
+
+ Learn the JSON-based query language with filters and operators
+
+
+ Complete, real-world examples you can adapt to your own use cases
+
+
diff --git a/redis/search/recipes/overview.mdx b/redis/search/recipes/overview.mdx
index 4f7dca2d..dc3ab80a 100644
--- a/redis/search/recipes/overview.mdx
+++ b/redis/search/recipes/overview.mdx
@@ -22,3 +22,10 @@ These recipes demonstrate different patterns:
- **Blog Search**: Optimized for content discovery with relevance ranking and visual feedback
- **User Directory**: Optimized for quick lookups with autocomplete and exact matching
+### Related Tutorials
+
+For more end-to-end examples that use Redis Search, see:
+
+- [Building Analytics with Redis](/redis/tutorials/analytics_with_redis): Record flexible events and query them with Redis Search.
+- [Agent Memory with Redis Search](/redis/tutorials/agent_memory): Recall long-term memories for AI agents with full-text queries.
+
diff --git a/redis/tutorials/analytics_with_redis.mdx b/redis/tutorials/analytics_with_redis.mdx
new file mode 100644
index 00000000..35ca77f8
--- /dev/null
+++ b/redis/tutorials/analytics_with_redis.mdx
@@ -0,0 +1,315 @@
+---
+title: Building Analytics with Redis
+description: "Two approaches to building analytics on Upstash Redis: pre-planned counters with bitmaps, and flexible event recording queried with Redis Search."
+---
+
+Most teams reach for a dedicated analytics product the moment they want to count
+something. But if you already run Redis (for caching, sessions, or rate limiting),
+you are sitting on one of the best analytics engines available.
+
+Why Redis?
+
+- **It's already there.** No new vendor, no new pipeline, no nightly ETL job. You
+ write events from the same code that serves your requests.
+- **It's fast.** Counters, sets, and bitmaps are O(1) or close to it. You can
+ increment a metric on every request without thinking about it, and read the
+ result back in single-digit milliseconds.
+- **It's serverless-friendly.** With [Upstash Redis](https://upstash.com/) you talk
+ to it over HTTP, so it works from edge functions, Lambdas, and the browser-facing
+ routes of a Next.js app, exactly where analytics events originate.
+
+There are two broad philosophies for doing analytics on Redis. This tutorial walks
+through both, when to use each, and the tooling we've built to make the second one
+painless.
+
+## Two philosophies
+
+There are two ways to think about this:
+
+1. **Plan ahead.** Decide what you want to measure up front and record it in a
+ compact structure (like a bitmap) built to answer that exact question.
+2. **Record everything.** Capture each event with its metadata, index it with Redis
+ Search, and figure out the questions later.
+
+| | Plan ahead | Record everything |
+| --- | --- | --- |
+| **You decide...** | the questions up front | the questions later |
+| **Storage** | counters, bitmaps, sets | event documents (JSON) |
+| **Cost** | tiny, fixed | grows with event volume |
+| **Querying** | read the counter | filter / aggregate with Redis Search |
+| **Good for** | DAU, funnels, feature usage | ad-hoc product analytics |
+
+
+You don't have to pick one. Many apps use bitmaps for the handful of metrics they
+watch daily, and event recording for everything they might want to explore later.
+
+
+## Philosophy 1: Plan ahead
+
+If you know in advance what you want to measure, you can record it in a structure
+that answers that exact question for almost no storage. The classic example is the
+**bitmap**: one bit per user, per day.
+
+### Daily active users with a bitmap
+
+A bitmap is a string where you can flip individual bits by offset. Use the user ID
+as the offset and you get a per-day "did this user show up?" record that costs one
+bit per user, roughly **1.2 MB for 10 million users**.
+
+```ts
+import { Redis } from "@upstash/redis";
+
+const redis = Redis.fromEnv();
+
+// Mark user 1234 as active today
+function markActive(userId: number, day = today()) {
+ return redis.setbit(`active:${day}`, userId, 1);
+}
+
+// How many unique users were active today?
+function dailyActiveUsers(day = today()) {
+ // bitcount accepts a key alone at runtime, but the TS SDK wants an
+ // explicit byte range; 0..-1 covers the whole bitmap.
+ return redis.bitcount(`active:${day}`, 0, -1);
+}
+
+function today() {
+ return new Date().toISOString().slice(0, 10); // "2024-06-15"
+}
+```
+
+`setbit` is O(1) and `bitcount` counts the set bits in one pass. Done.
+
+### Combining days: weekly actives and retention
+
+Because each day is its own bitmap, you can answer questions about *ranges* of days
+with bitwise operations, without storing anything extra.
+
+```ts
+// Weekly active users: OR the last 7 daily bitmaps together
+async function weeklyActiveUsers(days: string[]) {
+ if (days.length === 0) return 0;
+ const [first, ...rest] = days.map((d) => `active:${d}`);
+ await redis.bitop("or", "active:week", first, ...rest);
+ return redis.bitcount("active:week", 0, -1);
+}
+
+// Retention: users active on BOTH day A and day B
+async function retained(dayA: string, dayB: string) {
+ await redis.bitop("and", "tmp:retained", `active:${dayA}`, `active:${dayB}`);
+ return redis.bitcount("tmp:retained", 0, -1);
+}
+```
+
+`OR` gives you "active on any of these days", `AND` gives you "active on all of
+them", the building blocks of retention and funnel analysis. The same idea powers
+feature-adoption flags: keep a bitmap per feature and `AND` it against your active
+users to see adoption.
+
+### When this breaks down
+
+The catch is right there in the name: you have to **plan ahead**. A bitmap answers
+the one question you designed it for. The moment someone asks "okay, but how many of
+those users were on mobile, in Germany, on the new checkout flow?" you're stuck:
+that dimension was never recorded. You'd need to have created a separate bitmap for
+every combination in advance, which doesn't scale.
+
+That's where the second philosophy comes in.
+
+## Philosophy 2: Record events, query later
+
+Instead of deciding the questions up front, record each event as a document with
+whatever metadata you have on hand, index it with
+[Redis Search](/redis/search/introduction), and ask your questions *afterwards*.
+
+The flow is:
+
+1. Write each event as a JSON document under a known key prefix.
+2. Define a search index over that prefix once.
+3. Query and aggregate however you like (filters, ranges, group-bys) without
+ having planned for any specific question.
+
+### Define the index
+
+You define the index a single time. Redis Search then automatically picks up any
+key matching the prefix; there is no separate "insert into index" step.
+
+```ts
+import { Redis, s } from "@upstash/redis";
+
+const redis = Redis.fromEnv();
+
+const events = await redis.search.createIndex({
+ name: "events-idx",
+ prefix: "event:",
+ dataType: "json",
+ existsOk: true, // don't throw if the index already exists
+ schema: s.object({
+ name: s.keyword(), // "pageview", "signup", "purchase"
+ path: s.keyword(), // "/pricing"
+ country: s.keyword(), // "DE"
+ device: s.keyword(), // "mobile"
+ amount: s.number("F64"), // for purchase events
+ ts: s.date(),
+ }),
+});
+```
+
+### Record events
+
+Events are just JSON written with a regular Redis command. The index picks them up
+on its own.
+
+```ts
+async function track(event: {
+ name: string;
+ path?: string;
+ country?: string;
+ device?: string;
+ amount?: number;
+}) {
+ const id = crypto.randomUUID();
+ await redis.json.set(`event:${id}`, "$", {
+ ...event,
+ ts: new Date().toISOString(),
+ });
+}
+
+await track({ name: "pageview", path: "/pricing", country: "DE", device: "mobile" });
+await track({ name: "purchase", amount: 49.0, country: "DE", device: "mobile" });
+```
+
+
+Indexing is asynchronous. In a long-running app you don't need to think about it,
+but in a tight loop, like a script or a test that writes events and immediately
+queries them, call `await events.waitIndexing()` first so the documents you just
+wrote are searchable:
+
+```ts
+await events.waitIndexing();
+```
+
+
+### Now ask anything
+
+Here's the payoff. None of these queries needed to be anticipated when you recorded
+the events.
+
+```ts
+// How many mobile pageviews from Germany?
+const { count } = await events.count({
+ filter: {
+ $and: [
+ { name: { $eq: "pageview" } },
+ { device: { $eq: "mobile" } },
+ { country: { $eq: "DE" } },
+ ],
+ },
+});
+
+// Total revenue and average order value, broken down by country
+const revenue = await events.aggregate({
+ filter: { name: { $eq: "purchase" } },
+ aggregations: {
+ by_country: {
+ $terms: { field: "country", size: 20 },
+ $aggs: {
+ total: { $avg: { field: "amount" } },
+ orders: { $count: { field: "amount" } },
+ },
+ },
+ },
+});
+
+// Top pages this week
+const topPages = await events.aggregate({
+ filter: { ts: { $gte: "2024-06-10T00:00:00Z" } }, // s.date() expects an RFC 3339 string
+ aggregations: {
+ pages: { $terms: { field: "path", size: 10 } },
+ },
+});
+```
+
+Filters, numeric ranges, date ranges, group-bys (`$terms`), histograms, facets,
+percentiles: all available, all decided at query time. See the
+[querying](/redis/search/querying) and [aggregating](/redis/search/aggregations)
+docs for the full set.
+
+## The complexity, and a PoC that hides it
+
+The event-recording approach is more flexible, but it does come with moving parts
+that the simple examples above gloss over:
+
+- **Schema management**: keeping the index schema in sync as your events evolve.
+- **Capturing events from the frontend**: you need an endpoint, batching, and a
+ client to send events without slowing down the page.
+- **Sessions and context**: tying events together and attaching shared metadata.
+- **Exploring the data**: a query API is not a dashboard.
+
+To explore how far this can be smoothed over, we built a proof-of-concept SDK,
+[**`@upstash/redis-analytics`**](https://github.com/upstash/redis-analytics), that packages these pieces:
+
+- A **ready-to-use React hook** that auto-creates a session and captures pageviews.
+- A single **backend endpoint** you drop into your app (e.g. a Next.js route).
+- **Middleware** hooks for resolving feature flags and attaching server-side context.
+- A **schema registry** that infers field types from your events and maintains the
+ Redis Search index for you.
+- An **admin dashboard** for exploring captured analytics.
+
+A minimal end-to-end wiring looks like this:
+
+```ts
+// lib/analytics.ts (backend client)
+import { AnalyticsBackendClient } from "@upstash/redis-analytics";
+
+export const analytics = new AnalyticsBackendClient({
+ redis: {
+ url: process.env.UPSTASH_REDIS_REST_URL!,
+ token: process.env.UPSTASH_REDIS_REST_TOKEN!,
+ },
+});
+```
+
+```ts
+// app/api/analytics/route.ts (the single endpoint)
+import { analytics } from "@/lib/analytics";
+
+const handler = analytics.getHandler();
+export const POST = handler;
+export const GET = handler;
+```
+
+```tsx
+// frontend (the hook)
+"use client";
+import { createAnalyticsHook } from "@upstash/redis-analytics/react";
+
+const useAnalytics = createAnalyticsHook({ endpoint: "/api/analytics" });
+
+export function BuyButton() {
+ const { captureEvent, sessionId } = useAnalytics();
+ return (
+
+ );
+}
+```
+
+## Which should you use?
+
+- Reach for **bitmaps and counters** when you have a short, stable list of metrics
+ you watch every day. They're nearly free and answer instantly.
+- Reach for **event recording with Redis Search** when you want to explore your data
+ and can't predict every question in advance.
+
+Both run on the Redis you already have. Start with whichever matches the questions
+you have today; you can always add the other later.