Optimizely CMS: Find vs. Database Architectural Analysis

In the lifecycle of developing Optimizely CMS applications, architects and developers frequently face a critical decision: "Should we use Optimizely (Episerver) Find or query the database directly via ContentLoader?" This is not merely a performance question; it is a strategic architectural decision affecting scalability, maintainability, and operational costs.

This article analyzes a real-world case study: optimizing ProductCatalogBlock queries in a multi-site e-commerce system with a relatively small footprint (approx. 25 products per category).

Case Study: Product Catalog Optimization

Project Context

We managed an Optimizely CMS multi-site system handling product catalogs for various brands. Each category utilized approximately 25 ProductCatalogBlock items to display product info on landing pages. Initially, the team implemented an Optimizely Find solution, assuming search-based retrieval was inherently superior for performance.

The Find-Based Solution

The original implementation leveraged the IClient interface to filter content indexed in the cloud:

public List<ProductCatalogBlock> GetProductsByCategoryId(int categoryId)
{
    try
    {
        var searchResults = _searchClient
            .Search<ProductCatalogBlock>()
            .Filter(x => x.CategoryId.Match(categoryId))
            .Filter(x => x.IsActive.Match(true))
            .GetResult();

        return searchResults?.ToList() ?? new List<ProductCatalogBlock>();
    }
    catch (Exception)
    {
        // Fallback or empty list
        return new List<ProductCatalogBlock>();
    }
}

The Database-Based Solution (Optimized)

As an alternative, we tested a direct IContentLoader approach using batch loading to minimize roundtrips to the SQL database:

public List<ProductCatalogBlock> GetProductsFromCategoryPage(ContentReference categoryPageRef)
{
    if (ContentReference.IsNullOrEmpty(categoryPageRef)) return new List<ProductCatalogBlock>();

    var categoryPage = _contentLoader.Get<CategoryPage>(categoryPageRef);
    
    // Batch load: Retrieve all content area items in one call
    var contentLinks = categoryPage.ProductsContentArea.Items
        .Select(i => i.ContentLink)
        .ToList();

    var allContent = _contentLoader.GetItems(contentLinks, CultureInfo.CurrentUICulture);
    
    return allContent.OfType<ProductCatalogBlock>()
                     .Where(x => x.IsActive)
                     .ToList();
}

Performance Benchmarking

We conducted benchmarks using a production-like dataset. The results challenged the initial assumption that "Find is always faster."

Approach Avg Latency 95th Percentile Complexity
Find-based ~150ms ~250ms High (Cloud IO)
Database-based ~20ms ~35ms Low (In-process)

Detailed Analysis

  • Find Approach: Scalable for massive datasets (1,000+ items) and complex filtering. However, for 25 items, the network overhead to the Find index and cloud latency becomes a bottleneck.
  • Database Approach: Extremely fast for small datasets due to Optimizely's internal level-2 caching. It provides real-time data without indexing delays.

Architectural Principles: The Right Tool

Anti-Pattern: Premature Optimization

Using Find for 25 items is a classic Premature Optimization. While the intent is to be "future-proof," it introduces unnecessary dependencies and latency for the current requirements.

Hybrid Architecture Pattern

A robust architecture adapts to the data volume. For systems where product counts vary wildly, consider a Hybrid approach:

public class ProductCatalogService : IProductCatalogService
{
    private const int FIND_THRESHOLD = 100;
    private readonly ISynchronizedObjectInstanceCache _cache;
    
    public List<ProductCatalogBlock> GetProducts(int categoryId)
    {
        var cacheKey = $"ProductCatalog_{categoryId}";
        return _cache.Get(cacheKey, () => 
        {
            int count = GetEstimateCount(categoryId);
            return count > FIND_THRESHOLD 
                ? GetFromFind(categoryId) 
                : GetFromDatabase(categoryId);
        }, TimeSpan.FromMinutes(15));
    }
}

Strategic Insights: Caching Best Practices

To maximize the Database-based approach, utilize ISynchronizedObjectInstanceCache. Unlike standard memory cache, it synchronizes across load-balanced instances.

public class OptimizedProductCatalogService
{
    private readonly ISynchronizedObjectInstanceCache _cache;
    private readonly IContentEvents _contentEvents;
    
    public OptimizedProductCatalogService(ISynchronizedObjectInstanceCache cache, IContentEvents contentEvents)
    {
        _cache = cache;
        _contentEvents.PublishedContent += OnContentPublished;
    }
    
    private void OnContentPublished(object sender, ContentEventArgs e)
    {
        if (e.Content is ProductCatalogBlock block)
        {
            _cache.Remove($"ProductCatalog_{block.CategoryId}");
        }
    }
}

Conclusion

In this case study, reverting to the database approach for small datasets was the correct architectural decision. Optimizely Find is a powerful tool, but it should be applied when the problem matches its strengths: large-scale search, complex facets, and heavy filtering.

Key Takeaway: Start simple with IContentLoader and intelligent caching. Only transition to Find when the metrics justify the added complexity and latency overhead.

← Quay lại Blog
Optimizely CMS: Find vs. Database Architectural Analysis - Ginbok