Skip to main content
This recipe demonstrates building a searchable employee directory with autocomplete, fuzzy name matching, and department filtering.

Schema Design

The schema uses nested fields for profile data and exact matching for identifiers:
import { Redis, s } from "@upstash/redis";

const redis = Redis.fromEnv();

const users = await redis.search.createIndex({
  name: "users",
  dataType: "json",
  prefix: "user:",
  schema: s.object({
    // Exact username matching (no tokenization)
    username: s.string().noTokenize(),

    // Nested profile fields
    profile: s.object({
      // Name search without stemming (proper nouns)
      firstName: s.string().noStem(),
      lastName: s.string().noStem(),
      displayName: s.string().noStem(),

      // Email as exact match (contains special characters)
      email: s.string().noTokenize(),

      // Searchable bio/about text
      bio: s.string(),
    }),

    // Department and role for filtering
    department: s.string().noTokenize(),
    role: s.string().noTokenize(),
    title: s.string(),

    // Boolean flags
    isActive: s.boolean(),
    isAdmin: s.boolean(),

    // Dates for filtering
    hiredAt: s.date().fast(),
    lastActiveAt: s.date().fast(),
  }),
});

Sample Data

await redis.json.set("user:1", "$", {
  username: "jsmith",
  profile: {
    firstName: "Jane",
    lastName: "Smith",
    displayName: "Jane Smith",
    email: "jane.smith@company.com",
    bio: "Senior software engineer focused on backend systems and distributed computing.",
  },
  department: "Engineering",
  role: "Individual Contributor",
  title: "Senior Software Engineer",
  isActive: true,
  isAdmin: false,
  hiredAt: "2021-03-15T00:00:00Z",
  lastActiveAt: "2024-03-25T14:30:00Z",
});

await redis.json.set("user:2", "$", {
  username: "mjohnson",
  profile: {
    firstName: "Michael",
    lastName: "Johnson",
    displayName: "Mike Johnson",
    email: "michael.johnson@company.com",
    bio: "Engineering manager leading the platform team. Passionate about developer experience.",
  },
  department: "Engineering",
  role: "Manager",
  title: "Engineering Manager",
  isActive: true,
  isAdmin: true,
  hiredAt: "2019-06-01T00:00:00Z",
  lastActiveAt: "2024-03-25T16:45:00Z",
});

await redis.json.set("user:3", "$", {
  username: "swilliams",
  profile: {
    firstName: "Sarah",
    lastName: "Williams",
    displayName: "Sarah Williams",
    email: "sarah.williams@company.com",
    bio: "Product designer specializing in user research and interaction design.",
  },
  department: "Design",
  role: "Individual Contributor",
  title: "Senior Product Designer",
  isActive: true,
  isAdmin: false,
  hiredAt: "2022-01-10T00:00:00Z",
  lastActiveAt: "2024-03-24T11:20:00Z",
});

await redis.json.set("user:4", "$", {
  username: "rbrown",
  profile: {
    firstName: "Robert",
    lastName: "Brown",
    displayName: "Rob Brown",
    email: "robert.brown@company.com",
    bio: "Former engineering lead, now focused on technical writing and documentation.",
  },
  department: "Engineering",
  role: "Individual Contributor",
  title: "Staff Engineer",
  isActive: false,
  isAdmin: false,
  hiredAt: "2018-09-20T00:00:00Z",
  lastActiveAt: "2023-12-15T09: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:
await users.waitIndexing();
Use $fuzzy with prefix: true for search-as-you-type functionality. This approach handles both incomplete words and typos, providing a more forgiving autocomplete experience:
// As user types "ja" in the search box
const suggestions = await users.query({
  filter: {
    "profile.displayName": {
      $fuzzy: {
        value: "ja",
        prefix: true,
      },
    },
  },
  limit: 5,
});
// Matches "Jane Smith", "James Wilson", etc.

// As user types "jn" (typo for "ja")
const typoSuggestions = await users.query({
  filter: {
    "profile.displayName": {
      $fuzzy: {
        value: "jn",
        prefix: true,
        transpositionCostOne: true,
      },
    },
  },
  limit: 5,
});
// Still matches "Jane Smith", "James Wilson", etc.

// As user types "jane smi"
const refinedSuggestions = await users.query({
  filter: {
    "profile.displayName": "jane smi",
  },
  limit: 5,
});
// Smart matching applies fuzzy prefix to last word automatically
// Matches "Jane Smith", "Jane Smithson", etc.
Handle typos and misspellings in name searches:
// User types "Micheal" (common misspelling of "Michael")
const results = await users.query({
  filter: {
    "profile.firstName": {
      $fuzzy: "Micheal",
    },
  },
});
// Matches "Michael Johnson"

// Search with more tolerance for longer names
const fuzzyResults = await users.query({
  filter: {
    "profile.lastName": {
      $fuzzy: {
        value: "Willaims",  // Typo in "Williams"
        distance: 2,
      },
    },
  },
});

// Search across first and last name with fuzzy matching
const combinedFuzzy = await users.query({
  filter: {
    $should: [
      { "profile.firstName": { $fuzzy: "Srah" } },  // Typo
      { "profile.lastName": { $fuzzy: "Srah" } },
    ],
  },
});

Exact Username/Email Lookup

Find users by exact username or email:
// Exact username lookup
const user = await users.query({
  filter: {
    username: "jsmith",
  },
});

// Exact email lookup
const userByEmail = await users.query({
  filter: {
    "profile.email": "jane.smith@company.com",
  },
});

// Find users with email at specific domain
const companyUsers = await users.query({
  filter: {
    "profile.email": {
      $regex: "jane.*@company\\.com",
    },
  },
});

Department and Role Filtering

Filter users by department, role, or both:
// All users in Engineering
const engineers = await users.query({
  filter: {
    department: "Engineering",
    isActive: true,
  },
});

// All managers across departments
const managers = await users.query({
  filter: {
    role: "Manager",
    isActive: true,
  },
});

// Engineers who are managers
const engineeringManagers = await users.query({
  filter: {
    $must: {
      department: "Engineering",
      role: "Manager",
      isActive: true,
    },
  },
});

// Users in Engineering or Design
const productTeam = await users.query({
  filter: {
    department: {
      $in: ["Engineering", "Design", "Product"],
    },
    isActive: true,
  },
});

Search by Skills in Bio

Find users with specific skills or expertise:
// Find users who mention "distributed" in their bio
const distributedExperts = await users.query({
  filter: {
    "profile.bio": "distributed",
    isActive: true,
  },
});

// Find users with multiple skills
const backendEngineers = await users.query({
  filter: {
    $must: {
      "profile.bio": "backend",
      department: "Engineering",
    },
  },
});

// Search for phrase in bio
const uxResearchers = await users.query({
  filter: {
    "profile.bio": {
      $phrase: "user research",
    },
  },
});
Find administrators or users with specific permissions:
// All admin users
const admins = await users.query({
  filter: {
    isAdmin: true,
    isActive: true,
  },
});

// Active non-admin users in Engineering
const regularEngineers = await users.query({
  filter: {
    $must: {
      department: "Engineering",
      isActive: true,
    },
    $mustNot: {
      isAdmin: true,
    },
  },
});

Key Takeaways

  • Use NOTOKENIZE for usernames, emails, and exact-match identifiers
  • Use NOSTEM for proper nouns like names to prevent incorrect stemming
  • Use $fuzzy with prefix: true for search-as-you-type autocomplete with typo tolerance
  • Use $fuzzy to handle typos in name searches
  • Combine nested field paths (e.g., profile.firstName) for structured data