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 return2. 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
- Learn Dynamic frontend integration to implement these patterns in React/Vue
- Explore Faceted search for advanced filtering UI
- Review GraphQL best practices for query optimization
Updated 1 day ago
