Skip to content

Commit e7e2411

Browse files
committed
feat: add Redis Sentinel support (#81)
- Add is_sentinel_url() method to RedisConfig for Sentinel URL detection - Update RedisConfig.redis() to use RedisVL ConnectionFactory for Sentinel URLs - Add Sentinel support to RedisCache and RedisSemanticCache - Add Sentinel support to RedisChatMessageHistory - Add comprehensive unit tests for Sentinel connections - Update README with Sentinel configuration examples and usage All components now support redis+sentinel:// URLs for high availability Redis deployments. Closes #81
1 parent 0e6c3c1 commit e7e2411

File tree

6 files changed

+340
-91
lines changed

6 files changed

+340
-91
lines changed

libs/redis/README.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,79 @@ export REDIS_URL="redis://username:password@localhost:6379"
2020

2121
Alternatively, you can pass the Redis URL directly when initializing the components or use the `RedisConfig` class for more detailed configuration.
2222

23+
### Redis Connection Options
24+
25+
This package supports various Redis deployment modes through different connection URL schemes:
26+
27+
#### Standard Redis Connection
28+
```python
29+
# Standard Redis
30+
redis_url = "redis://localhost:6379"
31+
32+
# Redis with authentication
33+
redis_url = "redis://username:password@localhost:6379"
34+
35+
# Redis SSL/TLS
36+
redis_url = "rediss://localhost:6380"
37+
```
38+
39+
#### Redis Sentinel Connection
40+
41+
Redis Sentinel provides high availability for Redis. You can connect to a Sentinel-managed Redis deployment using the `redis+sentinel://` URL scheme:
42+
43+
```python
44+
# Single Sentinel node
45+
redis_url = "redis+sentinel://sentinel-host:26379/mymaster"
46+
47+
# Multiple Sentinel nodes (recommended for high availability)
48+
redis_url = "redis+sentinel://sentinel1:26379,sentinel2:26379,sentinel3:26379/mymaster"
49+
50+
# Sentinel with authentication
51+
redis_url = "redis+sentinel://username:password@sentinel1:26379,sentinel2:26379/mymaster"
52+
```
53+
54+
The Sentinel URL format is: `redis+sentinel://[username:password@]host1:port1[,host2:port2,...]/service_name`
55+
56+
Where:
57+
- `host:port` - One or more Sentinel node addresses
58+
- `service_name` - The name of the Redis master service (e.g., "mymaster")
59+
60+
**Example using Sentinel with RedisVectorStore:**
61+
```python
62+
from langchain_redis import RedisVectorStore, RedisConfig
63+
from langchain_openai import OpenAIEmbeddings
64+
65+
config = RedisConfig(
66+
redis_url="redis+sentinel://sentinel1:26379,sentinel2:26379/mymaster",
67+
index_name="my_index"
68+
)
69+
70+
vector_store = RedisVectorStore(
71+
embeddings=OpenAIEmbeddings(),
72+
config=config
73+
)
74+
```
75+
76+
**Example using Sentinel with RedisCache:**
77+
```python
78+
from langchain_redis import RedisCache
79+
80+
cache = RedisCache(
81+
redis_url="redis+sentinel://sentinel1:26379,sentinel2:26379/mymaster",
82+
ttl=3600
83+
)
84+
```
85+
86+
**Example using Sentinel with RedisChatMessageHistory:**
87+
```python
88+
from langchain_redis import RedisChatMessageHistory
89+
90+
history = RedisChatMessageHistory(
91+
session_id="user_123",
92+
redis_url="redis+sentinel://sentinel1:26379,sentinel2:26379/mymaster"
93+
)
94+
```
95+
2396
## Features
2497

2598
### 1. Vector Store

libs/redis/langchain_redis/cache.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,20 @@ def __init__(
132132
prefix: Optional[str] = "redis",
133133
redis_client: Optional[Redis] = None,
134134
):
135-
self.redis = redis_client or Redis.from_url(redis_url)
135+
if redis_client is not None:
136+
self.redis = redis_client
137+
elif redis_url.startswith("redis+sentinel://"):
138+
# For Sentinel URLs, use RedisVL's connection factory
139+
from redisvl.redis.connection import ( # type: ignore[import-untyped]
140+
RedisConnectionFactory,
141+
)
142+
143+
self.redis = RedisConnectionFactory.get_redis_connection(
144+
redis_url=redis_url
145+
)
146+
else:
147+
self.redis = Redis.from_url(redis_url)
148+
136149
try:
137150
self.redis.client_setinfo("LIB-NAME", __full_lib_name__) # type: ignore
138151
except ResponseError:
@@ -359,7 +372,20 @@ def __init__(
359372
prefix: Optional[str] = "llmcache",
360373
redis_client: Optional[Redis] = None,
361374
):
362-
self.redis = redis_client or Redis.from_url(redis_url)
375+
if redis_client is not None:
376+
self.redis = redis_client
377+
elif redis_url.startswith("redis+sentinel://"):
378+
# For Sentinel URLs, use RedisVL's connection factory
379+
from redisvl.redis.connection import ( # type: ignore[import-untyped]
380+
RedisConnectionFactory,
381+
)
382+
383+
self.redis = RedisConnectionFactory.get_redis_connection(
384+
redis_url=redis_url
385+
)
386+
else:
387+
self.redis = Redis.from_url(redis_url)
388+
363389
self.embeddings = embeddings
364390
self.prefix = prefix
365391
vectorizer = EmbeddingsVectorizer(embeddings=self.embeddings)

libs/redis/langchain_redis/chat_message_history.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,19 @@ def __init__(
124124
if not session_id or not isinstance(session_id, str):
125125
raise ValueError("session_id must be a non-empty, valid string")
126126

127-
self.redis_client = redis_client or Redis.from_url(redis_url, **kwargs)
127+
if redis_client is not None:
128+
self.redis_client = redis_client
129+
elif redis_url.startswith("redis+sentinel://"):
130+
# For Sentinel URLs, use RedisVL's connection factory
131+
from redisvl.redis.connection import ( # type: ignore[import-untyped]
132+
RedisConnectionFactory,
133+
)
134+
135+
self.redis_client = RedisConnectionFactory.get_redis_connection(
136+
redis_url=redis_url, **kwargs
137+
)
138+
else:
139+
self.redis_client = Redis.from_url(redis_url, **kwargs)
128140

129141
# Configure Redis client to use a no-op push handler when PubSub is initialized
130142
if hasattr(self.redis_client, "pubsub_configs"):

libs/redis/langchain_redis/config.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -496,13 +496,35 @@ def to_index_schema(self) -> IndexSchema:
496496

497497
return IndexSchema.from_dict({"index": index_info, "fields": fields})
498498

499+
def is_sentinel_url(self) -> bool:
500+
"""Check if the redis_url is a Sentinel URL.
501+
502+
Returns:
503+
bool: True if the URL uses the redis+sentinel:// scheme, False otherwise.
504+
"""
505+
return self.redis_url is not None and self.redis_url.startswith(
506+
"redis+sentinel://"
507+
)
508+
499509
def redis(self) -> Redis:
500510
if self.redis_client is not None:
501511
return self.redis_client
502512
elif self.redis_url is not None:
503-
if self.connection_args is not None:
504-
return Redis.from_url(self.redis_url, **self.connection_args)
513+
if self.is_sentinel_url():
514+
# For Sentinel URLs, use RedisVL's connection factory
515+
# which properly handles Sentinel connections
516+
from redisvl.redis.connection import ( # type: ignore[import-untyped]
517+
RedisConnectionFactory,
518+
)
519+
520+
return RedisConnectionFactory.get_redis_connection(
521+
redis_url=self.redis_url, **(self.connection_args or {})
522+
)
505523
else:
506-
return Redis.from_url(self.redis_url)
524+
# For standard URLs, use Redis.from_url
525+
if self.connection_args is not None:
526+
return Redis.from_url(self.redis_url, **self.connection_args)
527+
else:
528+
return Redis.from_url(self.redis_url)
507529
else:
508530
raise ValueError("Either redis_client or redis_url must be provided")

0 commit comments

Comments
 (0)