Tagging

The tagging system in Nicolas Cache provides a powerful way to organize and manage cache entries. Tags allow you to group related cache items and perform bulk operations on them.

Understanding Tags

Tags are labels that you can attach to cache entries. Each cache entry can have multiple tags, and each tag can be associated with multiple entries.

Key Concepts:

  • Many-to-Many Relationship: One entry can have multiple tags, one tag can have multiple entries

  • Automatic Cleanup: Tags are automatically removed when no entries reference them

  • Bulk Operations: Delete or retrieve all entries with a specific tag

  • No Hierarchy: Tags are flat, but you can create logical hierarchies using naming conventions

Basic Tag Operations

Adding Tags

from nicolas.cache import Cache

cache = Cache(backend="memory")

# Single tag
cache.set("user:1", user_data, tags=["users"])

# Multiple tags
cache.set("user:1", user_data, tags=["users", "active", "premium"])

# Tags as any iterable
tag_set = {"users", "employees", "department:IT"}
cache.set("user:2", user_data, tags=tag_set)

Retrieving by Tag

# Get all entries with a specific tag
all_users = cache.get_by_tag("users")
# Returns: {"user:1": {...}, "user:2": {...}}

# Process tagged entries
for key, value in cache.get_by_tag("active").items():
    print(f"Active user {key}: {value['name']}")

Deleting by Tag

# Delete all entries with a tag
deleted_count = cache.delete_by_tag("temporary")
print(f"Deleted {deleted_count} temporary entries")

# Clear all user sessions
cache.delete_by_tag("sessions")

Tag Naming Strategies

Hierarchical Tags

Use colons to create logical hierarchies:

# Category hierarchy
cache.set("product:123", product, tags=[
    "products",
    "category:electronics",
    "category:electronics:phones",
    "brand:apple"
])

# Get all electronics
electronics = cache.get_by_tag("category:electronics")

# Get only phones
phones = cache.get_by_tag("category:electronics:phones")

Entity-Based Tags

Tag by entity type and ID:

# User-specific tags
user_id = 123
cache.set(f"profile:{user_id}", profile, tags=[
    "profiles",
    f"user:{user_id}",
    f"org:{profile['org_id']}"
])

cache.set(f"settings:{user_id}", settings, tags=[
    "settings",
    f"user:{user_id}"
])

# Invalidate all data for a user
cache.delete_by_tag(f"user:{user_id}")

Time-Based Tags

Use tags for time-based grouping:

from datetime import datetime, date

now = datetime.now()
today = date.today()

cache.set("report:daily", report_data, tags=[
    "reports",
    f"date:{today.isoformat()}",
    f"month:{today.strftime('%Y-%m')}",
    f"year:{today.year}",
    f"hour:{now.hour}"
])

# Delete all reports from today
cache.delete_by_tag(f"date:{today.isoformat()}")

# Get all reports from current month
monthly_reports = cache.get_by_tag(f"month:{today.strftime('%Y-%m')}")

Status Tags

Track entry states:

# Order processing
cache.set(f"order:{order_id}", order_data, tags=[
    "orders",
    f"customer:{customer_id}",
    "status:pending"
])

# Update status by replacing tags
order_data['status'] = 'processing'
cache.set(f"order:{order_id}", order_data, tags=[
    "orders",
    f"customer:{customer_id}",
    "status:processing"  # Changed from pending
])

# Get all pending orders
pending = cache.get_by_tag("status:pending")

Advanced Patterns

Cache Invalidation Strategies

class ProductCache:
    def __init__(self, cache):
        self.cache = cache

    def set_product(self, product):
        """Cache product with comprehensive tagging."""
        self.cache.set(
            f"product:{product['id']}",
            product,
            tags=[
                "products",
                f"category:{product['category_id']}",
                f"brand:{product['brand_id']}",
                f"price_range:{self._get_price_range(product['price'])}",
                "available" if product['stock'] > 0 else "out_of_stock"
            ]
        )

    def invalidate_category(self, category_id):
        """Invalidate all products in a category."""
        return self.cache.delete_by_tag(f"category:{category_id}")

    def invalidate_brand(self, brand_id):
        """Invalidate all products from a brand."""
        return self.cache.delete_by_tag(f"brand:{brand_id}")

    def _get_price_range(self, price):
        if price < 100:
            return "budget"
        elif price < 500:
            return "mid"
        else:
            return "premium"

Dependency Tracking

class DependencyCache:
    def __init__(self, cache):
        self.cache = cache

    def cache_with_dependencies(self, key, value, depends_on):
        """Cache with dependency tracking."""
        tags = ["cached_values"]

        # Add dependency tags
        for dep in depends_on:
            tags.append(f"depends:{dep}")

        self.cache.set(key, value, tags=tags)

    def invalidate_dependents(self, dependency):
        """Invalidate all entries depending on something."""
        return self.cache.delete_by_tag(f"depends:{dependency}")

# Usage
dep_cache = DependencyCache(cache)

# Cache computed value that depends on user and config
dep_cache.cache_with_dependencies(
    "computed:result",
    compute_result(),
    depends_on=["user:123", "config:main"]
)

# When user changes, invalidate dependent caches
dep_cache.invalidate_dependents("user:123")

Multi-Tenant Caching

class TenantCache:
    def __init__(self, cache, tenant_id):
        self.cache = cache
        self.tenant_id = tenant_id

    def set(self, key, value, tags=None):
        """Set with tenant isolation."""
        tags = tags or []
        tags.append(f"tenant:{self.tenant_id}")

        full_key = f"tenant:{self.tenant_id}:{key}"
        self.cache.set(full_key, value, tags=tags)

    def get(self, key):
        """Get with tenant isolation."""
        full_key = f"tenant:{self.tenant_id}:{key}"
        return self.cache.get(full_key)

    def clear_tenant_cache(self):
        """Clear all cache for this tenant."""
        return self.cache.delete_by_tag(f"tenant:{self.tenant_id}")

# Usage
tenant_cache = TenantCache(cache, tenant_id="acme_corp")
tenant_cache.set("config", config_data)
tenant_cache.clear_tenant_cache()  # Clear all Acme Corp cache

Tag Performance

Performance Considerations

  1. Number of Tags: More tags per entry means more bookkeeping

  2. Tag Cardinality: Unique tags for every entry defeats the purpose

  3. Tag Names: Shorter tag names use less memory

# Good - reasonable number of meaningful tags
cache.set("user:123", data, tags=["users", "active", "premium"])

# Bad - too many unique tags
cache.set("user:123", data, tags=[
    f"user:{id}",
    f"timestamp:{time.time()}",  # Unique for every entry!
    f"random:{uuid.uuid4()}"      # Defeats grouping purpose
])

Memory Overhead

Each tag maintains a set of keys in Redis:

# Redis memory usage example
cache:tag:users -> {user:1, user:2, ..., user:1000}  # ~8KB for 1000 users
cache:key_tags:user:1 -> {users, active, premium}    # ~100 bytes per key

Best Practices

  1. Use Consistent Naming

# Good - consistent format
tags = [
    "type:user",
    "status:active",
    "role:admin"
]

# Bad - inconsistent
tags = [
    "user-type",
    "active_status",
    "AdminRole"
]
  1. Plan Your Tag Hierarchy

# Well-planned hierarchy
tags = [
    "content",
    "content:articles",
    "content:articles:published",
    "author:john",
    "category:tech"
]
  1. Avoid Over-Tagging

# Appropriate tagging
cache.set("session:123", session, tags=["sessions", f"user:{user_id}"])

# Over-tagging
cache.set("session:123", session, tags=[
    "sessions", "active_sessions", "user_sessions",
    "temporary", "data", "cache", "storage"  # Too generic
])
  1. Document Your Tagging Schema

"""
Tagging Schema:
- users: All user-related data
- user:{id}: Specific user's data
- org:{id}: Organization-specific data
- temp: Temporary data (cleared daily)
- config: Configuration data
- api: API response cache
"""
  1. Regular Cleanup

# Scheduled cleanup of temporary data
def daily_cleanup():
    cache.delete_by_tag("temp")
    cache.delete_by_tag(f"date:{yesterday}")

Tag Implementation Details

The tagging system is implemented differently in each backend:

Memory Backend:

  • Tags stored in Python dictionaries

  • O(1) tag addition/removal

  • O(n) retrieval by tag

Redis Backend:

  • Tags stored as Redis sets

  • Atomic tag operations

  • Efficient set operations for retrieval

Data Structure:

# Conceptual structure
cache_entries = {
    "user:1": {value: {...}, tags: {"users", "active"}},
    "user:2": {value: {...}, tags: {"users", "inactive"}}
}

tag_index = {
    "users": {"user:1", "user:2"},
    "active": {"user:1"},
    "inactive": {"user:2"}
}