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_keyis 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()— TheGraphClientis 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 Type | Key Pattern | TTL |
|---|---|---|
| Single passage | gl:passage:{passageId}[:{suffix}] | 3600s (1 hour) |
| Chapter | gl:chapter:{bookId}.{chapter}:{translation} | 3600s (1 hour) |
| Topic | gl:topic:{topicId} | 3600s (1 hour) |
| Lexicon entry | gl:lexicon:{wordId} | 86400s (24 hours) |
| Graph connections | gl: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:
- Scripture content is immutable after ingest
- Re-ingest (the only write path) is an infrequent batch operation
- 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 MATCHis used for nullable relationships to avoid dropping rowscollect(DISTINCT ...)prevents duplicates from multi-hop matches- Graph writes use
MERGEfor idempotency (handled by the Ingest service)
Related Pages
- Content Service Overview — service configuration and setup
- Endpoints — all available API endpoints
- Architecture > Data > Redis — Redis caching strategy
- Architecture > Data > FalkorDB — graph model details