Skip to main content
This recipe demonstrates building a product catalog search with filtering, sorting, typo tolerance, and relevance boosting.

Schema Design

The schema balances searchability with filtering and sorting capabilities:
import { Redis, s } from "@upstash/redis";

const redis = Redis.fromEnv();

const products = await redis.search.createIndex({
  name: "products",
  dataType: "json",
  prefix: "product:",
  schema: s.object({
    // Full-text searchable fields
    name: s.string(),
    description: s.string(),
    brand: s.string().noStem(), // "Nike" shouldn't stem to "Nik"

    // Exact-match category for filtering
    category: s.string().noTokenize(), // "Electronics > Audio" as single token

    // Numeric fields for filtering and sorting
    price: s.number("F64"), // Enable sorting by price
    rating: s.number("F64"), // Enable sorting by rating
    reviewCount: s.number("U64"),

    // Boolean for stock filtering
    inStock: s.boolean(),

    // Date for "new arrivals" queries
    createdAt: s.date().fast(),
  }),
});

Sample Data

await redis.json.set("product:1", "$", {
  name: "Sony WH-1000XM5 Wireless Headphones",
  description: "Industry-leading noise cancellation with premium sound quality. 30-hour battery life with quick charging.",
  brand: "Sony",
  category: "Electronics > Audio > Headphones",
  price: 349.99,
  rating: 4.8,
  reviewCount: 2847,
  inStock: true,
  createdAt: "2024-01-15T00:00:00Z",
});

await redis.json.set("product:2", "$", {
  name: "Apple AirPods Pro 2nd Generation",
  description: "Active noise cancellation, transparency mode, and spatial audio. MagSafe charging case included.",
  brand: "Apple",
  category: "Electronics > Audio > Earbuds",
  price: 249.99,
  rating: 4.7,
  reviewCount: 5621,
  inStock: true,
  createdAt: "2024-02-20T00:00:00Z",
});

await redis.json.set("product:3", "$", {
  name: "Bose QuietComfort Ultra Headphones",
  description: "World-class noise cancellation with immersive spatial audio. Luxurious comfort for all-day wear.",
  brand: "Bose",
  category: "Electronics > Audio > Headphones",
  price: 429.99,
  rating: 4.6,
  reviewCount: 1253,
  inStock: false,
  createdAt: "2024-03-10T00:00:00Z",
});

Waiting for Indexing

Index updates are batched for performance, so newly added data may not appear in search results immediately. Use SEARCH.WAITINDEXING to ensure all pending updates are processed before querying:
// Wait for all pending index updates to complete
await products.waitIndexing();
This is especially useful in scripts or tests where you need to query immediately after inserting data. In production, the slight indexing delay is usually acceptable and calling this after every write is not recommended. The simplest search uses smart matching for natural language queries:
// User types "wireless headphones" in search box
const results = await products.query({
  filter: {
    name: "wireless headphones",
  },
});
Smart matching automatically handles this by:
  1. Prioritizing exact phrase matches (“wireless headphones” adjacent)
  2. Including documents with both terms in any order
  3. Finding fuzzy matches for typos

Search with Typo Tolerance

Users often misspell product names. Use fuzzy matching to handle typos:
// User types "wireles headphons" (two typos)
const results = await products.query({
  filter: {
    $should: [
      { name: { $fuzzy: "wireles" } },
      { name: { $fuzzy: "headphons" } },
    ],
  },
});
Combine text search with filters for category, price, and availability:
// Search within a category, price range, and only in-stock items
const results = await products.query({
  filter: {
    $must: {
      description: "noise cancellation",
      category: "Electronics > Audio > Headphones",
      inStock: true,
      price: { $gte: 200, $lte: 400 },
    },
  },
});

Boosting Premium Results

Promote featured products or preferred brands using score boosting:
// Search for headphones, boosting Sony and in-stock items
const results = await products.query({
  filter: {
    $must: {
      name: "headphones",
    },
    $should: [
      { brand: "Sony", $boost: 5.0 }, // Preferred brand
      { inStock: true, $boost: 10.0 }, // Strongly prefer in-stock
      { description: "premium", $boost: 2.0 },
    ],
  },
});

Sorting and Pagination

Sort results by price or rating, with pagination for large result sets:
// Page 1: Top-rated headphones, 20 per page
const page1 = await products.query({
  filter: {
    category: "Electronics > Audio > Headphones",
  },
  orderBy: {
    rating: "DESC",
  },
  limit: 20,
});

// Page 2
const page2 = await products.query({
  filter: {
    category: "Electronics > Audio > Headphones",
  },
  orderBy: {
    rating: "DESC",
  },
  limit: 20,
  offset: 20,
});

// Sort by price, cheapest first
const cheapest = await products.query({
  filter: {
    name: "headphones",
    inStock: true,
  },
  orderBy: {
    price: "ASC",
  },
  limit: 10,
});

New Arrivals

Find recently added products using date range queries:
// Products added after a specific date
const newArrivals = await products.query({
  filter: {
    createdAt: { $gte: "2026-01-01T00:00:00Z" },
    inStock: true,
  },
  orderBy: {
    createdAt: "DESC",
  },
  limit: 10,
});

Excluding Out-of-Stock Items

Use $mustNot to filter out unavailable products:
// Search results excluding out-of-stock items
const results = await products.query({
  filter: {
    $must: {
      name: "headphones",
    },
    $mustNot: {
      inStock: false,
    },
  },
});
Search across multiple categories using $in:
// Find products in either headphones or earbuds categories
const results = await products.query({
  filter: {
    $must: {
      description: "noise cancellation",
      category: {
        $in: [
          "Electronics > Audio > Headphones",
          "Electronics > Audio > Earbuds",
        ],
      },
    },
  },
});

Counting Results

Use SEARCH.COUNT to get the number of matching documents without retrieving them. This is useful for pagination UI (“Showing 1-20 of 156 results”) or analytics:
// Count all products in a category
const totalHeadphones = await products.count({
  filter: {
    category: "Electronics > Audio > Headphones",
  },
});

Key Takeaways

  • Use NOTOKENIZE for categories and codes that should match exactly
  • Use NOSTEM for brand names to prevent unwanted stemming
  • Mark price, rating, and date fields as FAST for sorting
  • Combine $must, $should, and $mustNot for complex filtering
  • Use $boost to promote featured or preferred items
  • Use SEARCH.COUNT to get result counts for pagination UI
  • Smart matching handles most natural language queries automatically