diff --git a/docs/extras/ecosystem/integrations/redis.mdx b/docs/extras/ecosystem/integrations/redis.mdx index a47ac9bf5e4..e1f8e0e0031 100644 --- a/docs/extras/ecosystem/integrations/redis.mdx +++ b/docs/extras/ecosystem/integrations/redis.mdx @@ -8,6 +8,36 @@ It is broken into two parts: installation and setup, and then references to spec ## Wrappers +All wrappers needing a redis url connection string to connect to the database support either a stand alone Redis server +or a High-Availability setup with Replication and Redis Sentinels. + +### Redis Standalone connection url +For standalone Redis server the official redis connection url formats can be used as describe in the python redis modules +"from_url()" method [Redis.from_url](https://redis-py.readthedocs.io/en/stable/connections.html#redis.Redis.from_url) + +Example: `redis_url = "redis://:secret-pass@localhost:6379/0"` + +### Redis Sentinel connection url + +For [Redis sentinel setups](https://redis.io/docs/management/sentinel/) the connection scheme is "redis+sentinel". +This is an un-offical extensions to the official IANA registered protocol schemes as long as there is no connection url +for Sentinels available. + +Example: `redis_url = "redis+sentinel://:secret-pass@sentinel-host:26379/mymaster/0"` + +The format is `redis+sentinel://[[username]:[password]]@[host-or-ip]:[port]/[service-name]/[db-number]` +with the default values of "service-name = mymaster" and "db-number = 0" if not set explicit. +The service-name is the redis server monitoring group name as configured within the Sentinel. + +The current url format limits the connection string to one sentinel host only (no list can be given) and +booth Redis server and sentinel must have the same password set (if used). + +### Redis Cluster connection url + +Redis cluster is not supported right now for all methods requiring a "redis_url" parameter. +The only way to use a Redis Cluster is with LangChain classes accepting a preconfigured Redis client like `RedisCache` +(example below). + ### Cache The Cache wrapper allows for [Redis](https://redis.io) to be used as a remote, low-latency, in-memory cache for LLM prompts and responses. diff --git a/docs/extras/modules/data_connection/vectorstores/integrations/redis.ipynb b/docs/extras/modules/data_connection/vectorstores/integrations/redis.ipynb index e7c658e17dc..7edfd71cef6 100644 --- a/docs/extras/modules/data_connection/vectorstores/integrations/redis.ipynb +++ b/docs/extras/modules/data_connection/vectorstores/integrations/redis.ipynb @@ -8,7 +8,11 @@ "\n", ">[Redis (Remote Dictionary Server)](https://en.wikipedia.org/wiki/Redis) is an in-memory data structure store, used as a distributed, in-memory key–value database, cache and message broker, with optional durability.\n", "\n", - "This notebook shows how to use functionality related to the [Redis vector database](https://redis.com/solutions/use-cases/vector-database/)." + "This notebook shows how to use functionality related to the [Redis vector database](https://redis.com/solutions/use-cases/vector-database/).\n", + "\n", + "As database either Redis standalone server or Redis Sentinel HA setups are supported for connections with the \"redis_url\"\n", + "parameter. More information about the different formats of the redis connection url can be found in the LangChain\n", + "[Redis Readme](../../../../integrations/redis.md) file" ] }, { @@ -258,6 +262,52 @@ "source": [ "Redis.delete(keys, redis_url=\"redis://localhost:6379\")" ] + }, + { + "cell_type": "markdown", + "source": [ + "### Redis connection Url examples\n", + "\n", + "Valid Redis Url scheme are:\n", + "1. `redis://` - Connection to Redis standalone, unencrypted\n", + "2. `rediss://` - Connection to Redis standalone, with TLS encryption\n", + "3. `redis+sentinel://` - Connection to Redis server via Redis Sentinel, unencrypted\n", + "4. `rediss+sentinel://` - Connection to Redis server via Redis Sentinel, booth connections with TLS encryption\n", + "\n", + "More information about additional connection parameter can be found in the redis-py documentation at https://redis-py.readthedocs.io/en/stable/connections.html" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "# connection to redis standalone at localhost, db 0, no password\n", + "redis_url=\"redis://localhost:6379\"\n", + "# connection to host \"redis\" port 7379 with db 2 and password \"secret\" (old style authentication scheme without username / pre 6.x)\n", + "redis_url=\"redis://:secret@redis:7379/2\"\n", + "# connection to host redis on default port with user \"joe\", pass \"secret\" using redis version 6+ ACLs\n", + "redis_url=\"redis://joe:secret@redis/0\"\n", + "\n", + "# connection to sentinel at localhost with default group mymaster and db 0, no password\n", + "redis_url=\"redis+sentinel://localhost:26379\"\n", + "# connection to sentinel at host redis with default port 26379 and user \"joe\" with password \"secret\" with default group mymaster and db 0\n", + "redis_url=\"redis+sentinel://joe:secret@redis\"\n", + "# connection to sentinel, no auth with sentinel monitoring group \"zone-1\" and database 2\n", + "redis_url=\"redis+sentinel://redis:26379/zone-1/2\"\n", + "\n", + "# connection to redis standalone at localhost, db 0, no password but with TLS support\n", + "redis_url=\"rediss://localhost:6379\"\n", + "# connection to redis sentinel at localhost and default port, db 0, no password\n", + "# but with TLS support for booth Sentinel and Redis server\n", + "redis_url=\"rediss+sentinel://localhost\"\n" + ], + "metadata": { + "collapsed": false + } } ], "metadata": { diff --git a/langchain/memory/chat_message_histories/redis.py b/langchain/memory/chat_message_histories/redis.py index 442059b4425..13904a463aa 100644 --- a/langchain/memory/chat_message_histories/redis.py +++ b/langchain/memory/chat_message_histories/redis.py @@ -6,6 +6,7 @@ from langchain.schema import ( BaseChatMessageHistory, ) from langchain.schema.messages import BaseMessage, _message_to_dict, messages_from_dict +from langchain.utilities.redis import get_client logger = logging.getLogger(__name__) @@ -29,7 +30,7 @@ class RedisChatMessageHistory(BaseChatMessageHistory): ) try: - self.redis_client = redis.Redis.from_url(url=url) + self.redis_client = get_client(redis_url=url) except redis.exceptions.ConnectionError as error: logger.error(error) diff --git a/langchain/memory/entity.py b/langchain/memory/entity.py index 5691ac51abd..68be77a8dd9 100644 --- a/langchain/memory/entity.py +++ b/langchain/memory/entity.py @@ -15,6 +15,7 @@ from langchain.memory.utils import get_prompt_input_key from langchain.schema import BasePromptTemplate from langchain.schema.language_model import BaseLanguageModel from langchain.schema.messages import BaseMessage, get_buffer_string +from langchain.utilities.redis import get_client logger = logging.getLogger(__name__) @@ -99,7 +100,7 @@ class RedisEntityStore(BaseEntityStore): super().__init__(*args, **kwargs) try: - self.redis_client = redis.Redis.from_url(url=url, decode_responses=True) + self.redis_client = get_client(redis_url=url, decode_responses=True) except redis.exceptions.ConnectionError as error: logger.error(error) diff --git a/langchain/utilities/redis.py b/langchain/utilities/redis.py new file mode 100644 index 00000000000..9255179999d --- /dev/null +++ b/langchain/utilities/redis.py @@ -0,0 +1,140 @@ +from __future__ import annotations + +import logging +from typing import ( + TYPE_CHECKING, + Any, +) +from urllib.parse import urlparse + +if TYPE_CHECKING: + from redis.client import Redis as RedisType + +logger = logging.getLogger(__name__) + + +def get_client(redis_url: str, **kwargs: Any) -> RedisType: + """Get a redis client from the connection url given. This helper accepts + urls for Redis server (TCP with/without TLS or UnixSocket) as well as + Redis Sentinel connections. + + Redis Cluster is not supported. + + Before creating a connection the existence of the database driver is checked + an and ValueError raised otherwise + + To use, you should have the ``redis`` python package installed. + + Example: + .. code-block:: python + + from langchain.utilities.redis import get_client + redis_client = get_client( + redis_url="redis://username:password@localhost:6379" + index_name="my-index", + embedding_function=embeddings.embed_query, + ) + + To use a redis replication setup with multiple redis server and redis sentinels + set "redis_url" to "redis+sentinel://" scheme. With this url format a path is + needed holding the name of the redis service within the sentinels to get the + correct redis server connection. The default service name is "mymaster". The + optional second part of the path is the redis db number to connect to. + + An optional username or password is used for booth connections to the rediserver + and the sentinel, different passwords for server and sentinel are not supported. + And as another constraint only one sentinel instance can be given: + + Example: + .. code-block:: python + + from langchain.utilities.redis import get_client + redis_client = get_client( + redis_url="redis+sentinel://username:password@sentinelhost:26379/mymaster/0" + index_name="my-index", + embedding_function=embeddings.embed_query, + ) + """ + + # Initialize with necessary components. + try: + import redis + except ImportError: + raise ValueError( + "Could not import redis python package. " + "Please install it with `pip install redis>=4.1.0`." + ) + + # check if normal redis:// or redis+sentinel:// url + if redis_url.startswith("redis+sentinel"): + redis_client = _redis_sentinel_client(redis_url, **kwargs) + if redis_url.startswith("rediss+sentinel"): # sentinel with TLS support enables + kwargs["ssl"] = True + if "ssl_cert_reqs" not in kwargs: + kwargs["ssl_cert_reqs"] = "none" + redis_client = _redis_sentinel_client(redis_url, **kwargs) + else: + # connect to redis server from url + redis_client = redis.from_url(redis_url, **kwargs) + return redis_client + + +def _redis_sentinel_client(redis_url: str, **kwargs: Any) -> RedisType: + """helper method to parse an (un-official) redis+sentinel url + and create a Sentinel connection to fetch the final redis client + connection to a replica-master for read-write operations. + + If username and/or password for authentication is given the + same credentials are used for the Redis Sentinel as well as Redis Server. + With this implementation using a redis url only it is not possible + to use different data for authentication on booth systems. + """ + import redis + + parsed_url = urlparse(redis_url) + # sentinel needs list with (host, port) tuple, use default port if none available + sentinel_list = [(parsed_url.hostname or "localhost", parsed_url.port or 26379)] + if parsed_url.path: + # "/mymaster/0" first part is service name, optional second part is db number + path_parts = parsed_url.path.split("/") + service_name = path_parts[1] or "mymaster" + if len(path_parts) > 2: + kwargs["db"] = path_parts[2] + else: + service_name = "mymaster" + + sentinel_args = {} + if parsed_url.password: + sentinel_args["password"] = parsed_url.password + kwargs["password"] = parsed_url.password + if parsed_url.username: + sentinel_args["username"] = parsed_url.username + kwargs["username"] = parsed_url.username + + # check for all SSL related properties and copy them into sentinel_kwargs too, + # add client_name also + for arg in kwargs: + if arg.startswith("ssl") or arg == "client_name": + sentinel_args[arg] = kwargs[arg] + + # sentinel user/pass is part of sentinel_kwargs, user/pass for redis server + # connection as direct parameter in kwargs + sentinel_client = redis.sentinel.Sentinel( + sentinel_list, sentinel_kwargs=sentinel_args, **kwargs + ) + + # redis server might have password but not sentinel - fetch this error and try + # again without pass, everything else cannot be handled here -> user needed + try: + sentinel_client.execute_command("ping") + except redis.exceptions.AuthenticationError as ae: + if "no password is set" in ae.args[0]: + logger.warning( + "Redis sentinel connection configured with password but Sentinel \ +answered NO PASSWORD NEEDED - Please check Sentinel configuration" + ) + sentinel_client = redis.sentinel.Sentinel(sentinel_list, **kwargs) + else: + raise ae + + return sentinel_client.master_for(service_name) diff --git a/langchain/vectorstores/redis.py b/langchain/vectorstores/redis.py index f166d010650..950dbc6a73d 100644 --- a/langchain/vectorstores/redis.py +++ b/langchain/vectorstores/redis.py @@ -28,6 +28,7 @@ from langchain.callbacks.manager import ( ) from langchain.docstore.document import Document from langchain.embeddings.base import Embeddings +from langchain.utilities.redis import get_client from langchain.utils import get_from_dict_or_env from langchain.vectorstores.base import VectorStore, VectorStoreRetriever @@ -111,6 +112,24 @@ class Redis(VectorStore): index_name="my-index", embedding_function=embeddings.embed_query, ) + + To use a redis replication setup with multiple redis server and redis sentinels + set "redis_url" to "redis+sentinel://" scheme. With this url format a path is + needed holding the name of the redis service within the sentinels to get the + correct redis server connection. The default service name is "mymaster". + + An optional username or password is used for booth connections to the rediserver + and the sentinel, different passwords for server and sentinel are not supported. + And as another constraint only one sentinel instance can be given: + + Example: + .. code-block:: python + + vectorstore = Redis( + redis_url="redis+sentinel://username:password@sentinelhost:26379/mymaster/0" + index_name="my-index", + embedding_function=embeddings.embed_query, + ) """ def __init__( @@ -126,19 +145,10 @@ class Redis(VectorStore): **kwargs: Any, ): """Initialize with necessary components.""" - try: - import redis - except ImportError: - raise ValueError( - "Could not import redis python package. " - "Please install it with `pip install redis>=4.1.0`." - ) - self.embedding_function = embedding_function self.index_name = index_name try: - # connect to redis from url - redis_client = redis.from_url(redis_url, **kwargs) + redis_client = get_client(redis_url=redis_url, **kwargs) # check if redis has redisearch module installed _check_redis_module_exist(redis_client, REDIS_REQUIRED_MODULES) except ValueError as e: @@ -280,13 +290,13 @@ class Redis(VectorStore): query (str): The query text for which to find similar documents. k (int): The number of documents to return. Default is 4. score_threshold (float): The minimum matching score required for a document - to be considered a match. Defaults to 0.2. - Because the similarity calculation algorithm is based on cosine similarity, - the smaller the angle, the higher the similarity. + to be considered a match. Defaults to 0.2. + Because the similarity calculation algorithm is based on cosine + similarity, the smaller the angle, the higher the similarity. Returns: List[Document]: A list of documents that are most similar to the query text, - including the match score for each document. + including the match score for each document. Note: If there are no documents that satisfy the score_threshold value, @@ -373,13 +383,16 @@ class Redis(VectorStore): ) -> Tuple[Redis, List[str]]: """Create a Redis vectorstore from raw documents. This is a user-friendly interface that: - 1. Embeds documents. - 2. Creates a new index for the embeddings in Redis. - 3. Adds the documents to the newly created Redis index. - 4. Returns the keys of the newly created documents. + 1. Embeds documents. + 2. Creates a new index for the embeddings in Redis. + 3. Adds the documents to the newly created Redis index. + 4. Returns the keys of the newly created documents. + This is intended to be a quick way to get started. + Example: .. code-block:: python + from langchain.vectorstores import Redis from langchain.embeddings import OpenAIEmbeddings embeddings = OpenAIEmbeddings() @@ -434,12 +447,15 @@ class Redis(VectorStore): ) -> Redis: """Create a Redis vectorstore from raw documents. This is a user-friendly interface that: - 1. Embeds documents. - 2. Creates a new index for the embeddings in Redis. - 3. Adds the documents to the newly created Redis index. + 1. Embeds documents. + 2. Creates a new index for the embeddings in Redis. + 3. Adds the documents to the newly created Redis index. + This is intended to be a quick way to get started. + Example: .. code-block:: python + from langchain.vectorstores import Redis from langchain.embeddings import OpenAIEmbeddings embeddings = OpenAIEmbeddings() @@ -481,7 +497,7 @@ class Redis(VectorStore): raise ValueError("'ids' (keys)() were not provided.") try: - import redis + import redis # noqa: F401 except ImportError: raise ValueError( "Could not import redis python package. " @@ -492,7 +508,7 @@ class Redis(VectorStore): # otherwise passing it to Redis will result in an error. if "redis_url" in kwargs: kwargs.pop("redis_url") - client = redis.from_url(url=redis_url, **kwargs) + client = get_client(redis_url=redis_url, **kwargs) except ValueError as e: raise ValueError(f"Your redis connected error: {e}") # Check if index exists @@ -522,7 +538,7 @@ class Redis(VectorStore): """ redis_url = get_from_dict_or_env(kwargs, "redis_url", "REDIS_URL") try: - import redis + import redis # noqa: F401 except ImportError: raise ValueError( "Could not import redis python package. " @@ -533,7 +549,7 @@ class Redis(VectorStore): # otherwise passing it to Redis will result in an error. if "redis_url" in kwargs: kwargs.pop("redis_url") - client = redis.from_url(url=redis_url, **kwargs) + client = get_client(redis_url=redis_url, **kwargs) except ValueError as e: raise ValueError(f"Your redis connected error: {e}") # Check if index exists @@ -558,7 +574,7 @@ class Redis(VectorStore): """Connect to an existing Redis index.""" redis_url = get_from_dict_or_env(kwargs, "redis_url", "REDIS_URL") try: - import redis + import redis # noqa: F401 except ImportError: raise ValueError( "Could not import redis python package. " @@ -569,7 +585,7 @@ class Redis(VectorStore): # otherwise passing it to Redis will result in an error. if "redis_url" in kwargs: kwargs.pop("redis_url") - client = redis.from_url(url=redis_url, **kwargs) + client = get_client(redis_url=redis_url, **kwargs) # check if redis has redisearch module installed _check_redis_module_exist(client, REDIS_REQUIRED_MODULES) # ensure that the index already exists