Skip to main content

Graph Client & Caching

The Content service accesses FalkorDB through GraphClient, a wrapper that provides connection pooling, transparent Redis query caching, and a clean interface for executing Cypher queries.

GraphClient

# services/content/src/gospelib_content/db/client.py
from typing import Any

class GraphClient:
"""Async FalkorDB client with connection pooling and Redis query cache."""

def __init__(
self,
*,
falkordb_url: str = "redis://localhost:6379",
graph_name: str = "gospelib",
redis_url: str = "redis://localhost:6380",
max_connections: int = 20,
default_cache_ttl: int = 3600,
) -> None: ...

async def connect(self) -> None:
"""Initialize FalkorDB and Redis cache connections."""
...

async def close(self) -> None:
"""Shut down FalkorDB and Redis cache connections."""
...

async def query(
self,
cypher: str,
params: dict[str, Any] | None = None,
*,
cache_key: str | None = None,
ttl: int | None = None,
) -> list[dict[str, Any]]:
"""Execute a Cypher query with optional Redis caching."""
...

Key Design Decisions

  • Connection pooling — Up to 20 concurrent connections via AsyncFalkorDB.create(max_connections=20)
  • Cache-through pattern — If a cache_key is provided, results are fetched from Redis first. On miss, the query executes against FalkorDB and the result is cached.
  • Configurable TTL — Each query can override the default TTL; immutable content (passages) uses 1 hour, while mutable data uses shorter TTLs.
  • DI via Depends() — The GraphClient is injected into route handlers using FastAPI's dependency injection system. The client lifecycle (connect/close) is managed by the FastAPI lifespan context manager.

Caching Strategy

Cache Key Format

All cache keys follow the pattern gl:<resource>:<id>:<params>:

Content TypeKey PatternTTL
Single passagegl:passage:{passageId}[:{suffix}]3600s (1 hour)
Chaptergl:chapter:{bookId}.{chapter}:{translation}3600s (1 hour)
Topicgl:topic:{topicId}3600s (1 hour)
Lexicon entrygl:lexicon:{wordId}86400s (24 hours)
Graph connectionsgl:connections:{passageId}:{limit}3600s (1 hour)

Scripture content is immutable — once ingested, passage text never changes. This allows aggressive caching with long TTLs.

Cache Invalidation

Cache entries expire via TTL only. There is no active invalidation because:

  1. Scripture content is immutable after ingest
  2. Re-ingest (the only write path) is an infrequent batch operation
  3. After a re-ingest, caches naturally refresh within their TTL window

For a full re-ingest, flush the cache namespace with:

redis-cli -p 6380 --scan --pattern 'gl:*' | xargs redis-cli -p 6380 DEL

Cypher Query Templates

Cypher queries are stored as module-level UPPER_SNAKE_CASE constants:

# services/content/src/gospelib_content/db/queries.py (planned)

GET_PASSAGE = """
MATCH (p:Passage {id: $passage_id})
OPTIONAL MATCH (p)-[:HAS_ORIGINAL]->(w:Witness)
RETURN p, collect(w) AS witnesses
"""

GET_PASSAGE_WITH_WITNESSES = """
MATCH (p:Passage {id: $passage_id})
OPTIONAL MATCH (p)-[:HAS_ORIGINAL]->(witness:Witness)
OPTIONAL MATCH (p)-[:CROSS_REF]->(ref:Passage)
WITH p, collect(DISTINCT witness) AS witnesses, collect(DISTINCT ref) AS refs
RETURN p, witnesses, refs
"""

GET_CONNECTIONS_FOR_PASSAGE = """
MATCH (p:Passage {id: $passage_id})
MATCH (p)-[rel]->(connected)
RETURN type(rel) AS relationship_type,
labels(connected)[0] AS node_type,
connected.id AS connected_id,
connected.title AS connected_title,
rel.weight AS weight
ORDER BY rel.weight DESC
LIMIT $limit
"""

GET_TOPIC_SUBGRAPH = """
MATCH (t:IndexTopic {id: $topic_id})
CALL {
WITH t
MATCH (t)-[:CITES]->(p:Passage)
RETURN 'passage' AS type, p AS node
UNION
WITH t
MATCH (t)-[:SEE_ALSO_TG]->(related:IndexTopic)
RETURN 'topic' AS type, related AS node
}
RETURN type, node
LIMIT $limit
"""

Query Pattern Conventions

  • All queries use parameterized variables ($passage_id, $limit) — never string interpolation
  • OPTIONAL MATCH is used for nullable relationships to avoid dropping rows
  • collect(DISTINCT ...) prevents duplicates from multi-hop matches
  • Graph writes use MERGE for idempotency (handled by the Ingest service)