Dev guideRecipesAPI ReferenceChangelog
Dev guideRecipesUser GuidesNuGetDev CommunityOptimizely AcademySubmit a ticketLog In
Dev guide

Personalized search and filtering

Build personalized and filtered experiences with Optimizely Graph while avoiding performance issues and query complexity pitfalls.

Personalized and filtered experiences deliver relevant content to each user based on their preferences, behavior, location, or other attributes. Optimizely Graph provides powerful filtering capabilities, but complex queries require careful structuring to avoid performance issues.

This guide focuses on GraphQL query patterns: how to structure where clauses, implement taxonomy filtering, and optimize query performance. For framework-specific implementation of these queries, see Dynamic frontend integration.

Understanding personalized search

Personalized search adapts content based on user context:

  • User preferences – Filter by user-selected interests, categories, or topics
  • Behavioral data – Show content based on viewing history or engagement
  • Location-based – Display geographically relevant content
  • Taxonomy-based – Filter by categories, tags, or hierarchical structures
  • Temporal – Show content relevant to time of day, season, or events

Basic filtering patterns

Filter by single field

Filter content by a specific field value:

query GetProductsByCategory($category: String!) {
  Product(
    where: { category: { eq: $category } }
    orderBy: { popularity: DESC }
    limit: 20
  ) {
    items {
      name
      description
      price
      category
      image {
        url
      }
    }
  }
}

Filter by multiple fields (AND logic)

Combine multiple filters using nested where conditions:

query GetFilteredProducts(
  $category: String!
  $minPrice: Float!
  $maxPrice: Float!
  $inStock: Boolean!
) {
  Product(
    where: {
      category: { eq: $category }
      price: { gte: $minPrice, lte: $maxPrice }
      inStock: { eq: $inStock }
    }
    orderBy: { price: ASC }
  ) {
    items {
      name
      price
      inStock
    }
  }
}

Filter by multiple values (OR logic)

Use the in operator to match any of multiple values:

query GetArticlesByCategories($categories: [String!]!) {
  Article(
    where: { category: { in: $categories } }
    orderBy: { publishedDate: DESC }
    limit: 50
  ) {
    items {
      title
      category
      publishedDate
      excerpt
    }
  }
}

Taxonomy-based filtering

Taxonomies organize content into hierarchical structures. Graph supports filtering by taxonomy at any level.

Flat taxonomy filtering

Filter by tags or flat category lists:

query GetContentByTags($tags: [String!]!) {
  Content(
    where: { 
      tags: { in: $tags }
    }
  ) {
    items {
      heading
      tags {
        tag
      }
    }
  }
}

Hierarchical taxonomy filtering

Filter by hierarchical categories (e.g., Electronics > Computers > Laptops):

query GetProductsByTaxonomy($categoryPath: String!) {
  Product(
    where: {
      taxonomy: {
        categoryPath: { startsWith: $categoryPath }
      }
    }
  ) {
    items {
      name
      taxonomy {
        categoryPath
        categoryName
        level
      }
    }
  }
}

Example usage:

  • $categoryPath = "Electronics" returns all electronics
  • $categoryPath = "Electronics/Computers" returns only computers
  • $categoryPath = "Electronics/Computers/Laptops" returns only laptops

Performance optimization

1. Use facets for filter options

Facets show available filter values and counts efficiently:

query GetProductsWithFacets($category: String) {
  Product(
    where: { category: { eq: $category } }
    limit: 20
  ) {
    items {
      name
      price
      brand
    }
    facets {
      brand {
        name
        count
      }
      priceRange {
        name
        count
      }
    }
  }
}

Benefits:

  • Single query returns both results and filter options
  • Shows counts for each filter value
  • Users see only relevant filter combinations

2. Implement pagination

Always paginate large result sets. Use skip and limit for numbered pages:

query GetPaginatedResults($skip: Int = 0, $limit: Int = 20) {
  Article(
    where: { status: { eq: "published" } }
    orderBy: { publishedDate: DESC }
    skip: $skip
    limit: $limit
  ) {
    items {
      title
      excerpt
    }
    total
  }
}

Calculate skip from the page number – skip: (page - 1) * pageSize – and use total to work out the number of pages. limit defaults to 20 and accepts a maximum of 100.

See Skip and limit pagination for details. Skip/limit reaches only the first 10,000 hits – for deeper result sets, switch to Cursor-based pagination.

3. Limit result count

Set reasonable limits to prevent overloading:

query GetTopArticles {
  Article(
    where: { featured: { eq: true } }
    orderBy: { views: DESC }
    limit: 10  # Never fetch unlimited results
  ) {
    items {
      title
    }
  }
}

User-specific personalization

Filter by user preferences

Pass user preference data (interests, location) as GraphQL variables to filter content, and use the locale argument for the user's language:

query GetPersonalizedContent(
  $categories: [String!]!
  $location: String!
  $locale: [Locales]
) {
  Content(
    locale: $locale
    where: {
      category: { in: $categories }
      location: { eq: $location }
    }
    orderBy: { Created: DESC }
    limit: 20
  ) {
    items {
      heading
      excerpt
      category
      Created
    }
  }
}

See locale for details. For framework implementation examples, see Dynamic frontend integration.

Location-based filtering

Filter by location with the distance operator on a GeoPoint field, and rank the results nearest-first by passing the same origin to orderBy:

query GetNearbyEvents(
  $latitude: Float!
  $longitude: Float!
  $radiusKm: Float!
  $startDate: DateTime!
) {
  Event(
    where: {
      location: {
        distance: {
          origin: { lat: $latitude, lon: $longitude }
          radius: $radiusKm
          unit: KM
        }
      }
      startDate: { gte: $startDate }
    }
    orderBy: { location: { origin: { lat: $latitude, lon: $longitude } } }
  ) {
    total
    items {
      name
      startDate
      location {
        lat
        lon
      }
    }
  }
}

This requires location to be defined as a GeoPoint field on the content type. See Geo search for the withIn polygon operator, distance facets, and the full list of units.

Complex filter combinations

Filter with exclusions

Combine positive and negative filters. Use notIn to exclude a set of values – clauses at the same level are an implicit _and:

query GetFilteredContent(
  $includeCategories: [String!]!
  $excludeTags: [String!]!
) {
  Content(
    where: {
      category: { in: $includeCategories }
      status: { eq: "published" }
      tags: { tag: { notIn: $excludeTags } }
    }
  ) {
    items {
      heading
      category
      tags { tag }
    }
  }
}

See Logical connectors for _and, _or, and _not when you need nested Boolean logic.

Date range filtering

Filter content by date ranges:

query GetRecentArticles(
  $startDate: DateTime!
  $endDate: DateTime!
) {
  Article(
    where: {
      Created: { 
        gte: $startDate
        lte: $endDate 
      }
    }
    orderBy: { Created: DESC }
  ) {
    items {
      title
      Created
      author
    }
  }
}

Best practices

1. Validate user input

Always validate filter parameters before querying:

  • Prevent injection attacks by validating category/field names against allowed values
  • Limit array sizes (e.g., max 10 categories in a filter)
  • Validate numeric ranges (ensure min < max, no negative values where inappropriate)
  • Sanitize string inputs

2. Cache filter results

Cache common filter combinations to improve performance. For caching implementation details, see Caching best practices and Dynamic frontend integration (framework-specific caching).

3. Debounce filter changes

Prevent excessive queries during user interaction by debouncing input changes (wait 300–500 ms after the user stops typing before querying). This reduces server load and improves performance.

4. Provide filter feedback

Show users what filters are active with visual indicators (chips, tags, or a filter summary). Allow users to easily remove individual filters.

Common pitfalls

1. Over-filtering

Too many simultaneous filters can return empty results:

// ✅ Good: Show filter counts before applying
const { data } = useQuery(GET_FACETS, { variables: { category } });
// Display facet.count to show how many results each filter will return

2. Unbounded queries

Never query without limits:

// ✅ Good: Always set a limit
query { Article(limit: 100) { items { title } } }

Graph always sets a default limit, even if the query does not specify one.

3. Ignoring query complexity

Complex nested queries can time out:

// ❌ Bad: Too complex
query {
  Content {
    items {
      relatedItems {
        nestedItems {
          deeplyNestedItems {
            // Too deep
          }
        }
      }
    }
  }
}

// ✅ Good: Flatten into multiple queries
query GetContent { Content { items { _metadata { key }, relatedKeys } } }
query GetRelated($keys: [String!]!) { Content(where: { _metadata: { key: { in: $keys } } }) { items {...} } }

Next steps


Did this page help you?