Metadata-Version: 2.1
Name: langchain-redis
Version: 0.3.0
Summary: An integration package connecting Redis and LangChain for AI working memory
Home-page: https://github.com/langchain-ai/langchain-redis
License: MIT
Author: Brian Sam-Bodden
Author-email: bsb@redis.com
Requires-Python: >=3.10,<3.14
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Provides-Extra: langcache
Requires-Dist: anyio (>=4.9.0)
Requires-Dist: certifi (>=2025.4.26)
Requires-Dist: fsspec (>=2026.6.0)
Requires-Dist: hf-xet (!=1.2.1)
Requires-Dist: httpcore (>=1.0.9)
Requires-Dist: jinja2 (>=3.1.6)
Requires-Dist: langcache (>=0.11.0) ; extra == "langcache"
Requires-Dist: langchain-core (>=1.6.4)
Requires-Dist: python-ulid (>=3.0.0)
Requires-Dist: redis (>=7.1)
Requires-Dist: redisvl (>=0.27.1)
Requires-Dist: typing-extensions (>=4.13.2)
Requires-Dist: urllib3 (>=2.4.0)
Project-URL: Repository, https://github.com/langchain-ai/langchain-redis
Project-URL: Source Code, https://github.com/langchain-ai/langchain-redis/tree/main/libs/redis
Description-Content-Type: text/markdown

# langchain-redis

This package contains the LangChain integration with Redis, providing powerful tools for vector storage, semantic caching, and chat history management.

## Installation

```bash
pip install -U langchain-redis
```

This will install the package along with its dependencies, including `redis`, `redisvl`, and `python-ulid`.

To use `LangCacheSemanticCache`, install the optional `langcache` extra:

```bash
pip install -U "langchain-redis[langcache]"
```

## Quickstart

Start Redis, install the package, and run a similarity search:

```bash
docker run -d --name redis -p 6379:6379 redis
```

```python
from langchain_openai import OpenAIEmbeddings  # any LangChain Embeddings works
from langchain_redis import RedisVectorStore

vector_store = RedisVectorStore(
    OpenAIEmbeddings(),
    index_name="quickstart",
    redis_url="redis://localhost:6379",
)

vector_store.add_texts(["Redis is a fast in-memory database"])
docs = vector_store.similarity_search("quick key-value store", k=1)
print(docs[0].page_content)
```

## Redis version compatibility

`langchain-redis` is tested against Redis 8.2 and newer. Most functionality works on any Redis with the Query Engine, with a few features gated by server version:

| Feature | Redis 8.2 | Redis 8.4+ |
|---|:---:|:---:|
| Vector search (KNN / range / MMR) | ✓ | ✓ |
| Full-text search | ✓ | ✓ |
| Hybrid search — aggregate method (`FT.AGGREGATE`) | ✓ | ✓ |
| Hybrid search — native method (`FT.HYBRID`) | ✗ | ✓ |
| SVS-VAMANA vector indexing (with compression) | ✓ | ✓ |

SVS-VAMANA indexing requires Redis 8.2 or newer.

`hybrid_search()` uses LINEAR fusion by default and detects the server version
automatically: it uses `FT.HYBRID` on Redis 8.4+ and falls back to the aggregate
method on older servers. RRF fusion is available explicitly on Redis 8.4+.

## Configuration

Every component takes the connection as a `redis_url` argument (or a
pre-existing client via `redis_client`; see Advanced Configuration for the
`RedisConfig` class). The URL scheme selects the deployment mode:

```python
redis_url = "redis://localhost:6379"                # standard
redis_url = "redis://username:password@host:6379"   # with authentication
redis_url = "rediss://host:6380"                    # SSL/TLS

# Redis Sentinel (high availability):
# redis+sentinel://[username:password@]host1:port1[,host2:port2,...]/service_name
redis_url = "redis+sentinel://sentinel1:26379,sentinel2:26379/mymaster"
```

The same URL works for every component:

```python
from langchain_redis import RedisCache, RedisChatMessageHistory, RedisVectorStore
from langchain_openai import OpenAIEmbeddings

url = "redis+sentinel://sentinel1:26379,sentinel2:26379/mymaster"

vector_store = RedisVectorStore(
    OpenAIEmbeddings(), index_name="my_index", redis_url=url
)
cache = RedisCache(redis_url=url, ttl=3600)
history = RedisChatMessageHistory(session_id="user_123", redis_url=url)
```

## Features

### 1. Vector Store

The `RedisVectorStore` class provides a vector database implementation using Redis.

#### Usage

```python
from langchain_redis import RedisVectorStore, RedisConfig
from langchain_openai import OpenAIEmbeddings
from redisvl.query.filter import Tag

embeddings = OpenAIEmbeddings()  # any LangChain Embeddings implementation works

config = RedisConfig(
    index_name="my_vectors",
    redis_url="redis://localhost:6379",
    distance_metric="COSINE",  # Options: COSINE, L2, IP
    metadata_schema=[
        {"name": "category", "type": "tag"},
    ],
)

vector_store = RedisVectorStore(embeddings, config=config)

# Adding documents
texts = ["Document 1 content", "Document 2 content"]
metadatas = [{"category": "science"}, {"category": "history"}]
vector_store.add_texts(texts, metadatas=metadatas)

# Adding documents with custom keys
custom_keys = ["doc1", "doc2"]
vector_store.add_texts(texts, metadatas=metadatas, keys=custom_keys)

# Similarity search
query = "Sample query"
docs = vector_store.similarity_search(query, k=2)

# Similarity search with score
docs_and_scores = vector_store.similarity_search_with_score(query, k=2)

# Relevance scores (higher is more relevant) and score-threshold retrieval
docs_and_relevance = vector_store.similarity_search_with_relevance_scores(
    query, k=2, score_threshold=0.5
)
retriever = vector_store.as_retriever(
    search_type="similarity_score_threshold",
    search_kwargs={"k": 4, "score_threshold": 0.5},
)

# Similarity search with filtering
from redisvl.query.filter import Tag

filter_expr = Tag("category") == "science"
filtered_docs = vector_store.similarity_search(query, k=2, filter=filter_expr)

# Tag filters support wildcard patterns
filtered_docs = vector_store.similarity_search(
    query, k=2, filter=Tag("category") % "scien*"
)

# Maximum marginal relevance search
docs = vector_store.max_marginal_relevance_search(query, k=2, fetch_k=10)
```

Metadata is always stored and returned with documents, but only fields
declared in `metadata_schema` are indexed and filterable. Filtering on an
undeclared field raises an `Unknown field` error.

`similarity_search_with_score` returns the raw Redis distance, where lower is
closer. The relevance scores above use RedisVL's normalization instead, so they
fall in `[0, 1]` and higher is closer: `(2 - d) / 2` for `COSINE` and
`1 / (1 + d)` for `L2`. The two scale differently, so pick `score_threshold`
per index (with `COSINE`, unrelated vectors score about 0.5). `IP` indexes
raise a `ValueError`, since RedisVL has no normalization for them. Use `COSINE`,
which is normalized inner product. `distance_threshold` stays a raw distance
when you combine it with these methods.

#### Hybrid and full-text search

`hybrid_search` combines BM25 text scoring with vector similarity in a single
query, and `full_text_search` exposes BM25-only retrieval. The examples below
share this product catalog:

```python
from redisvl.query.filter import Num, Tag

store = RedisVectorStore(
    embeddings,
    index_name="products",
    redis_url="redis://localhost:6379",
    metadata_schema=[
        {"name": "category", "type": "tag"},
        {"name": "title", "type": "text"},
        {"name": "price", "type": "numeric"},
    ],
)

store.add_texts(
    [
        "Lightweight running shoes with cushioned soles",
        "Waterproof hiking boots for rough mountain trails",
        "Ergonomic office chair with lumbar support",
    ],
    metadatas=[
        {"category": "footwear", "title": "running shoes", "price": 89},
        {"category": "footwear", "title": "hiking boots", "price": 129},
        {"category": "furniture", "title": "office chair", "price": 249},
    ],
)
```

```python
# Hybrid search: text relevance + vector similarity, one ranked list.
# Uses LINEAR fusion with FT.HYBRID on Redis 8.4+ and transparently falls
# back to the equivalent FT.AGGREGATE-based combination on older servers.
docs = store.hybrid_search("running shoes", k=2)

# With scores, an explicit engine, and linear weighting:
# score = alpha * vector_score + (1 - alpha) * text_score
results = store.hybrid_search_with_score(
    "running shoes",
    k=2,
    method="aggregate",       # "auto" (default), "ft_hybrid", or "aggregate"
    alpha=0.5,
    filter=Tag("category") == "footwear",
)

# Full-text (BM25) search, weighting title matches higher than body text
docs = store.full_text_search(
    "running shoes", k=2, text_fields={"title": 5.0, "text": 1.0}
)
```

Hybrid scores are combined scores (rank-based when RRF is selected), not
comparable with the cosine distances returned by
`similarity_search_with_score`.

#### Filter-based deletion

Documents can be deleted by what they are, not just by id, using the same
`FilterExpression` type as search. This is useful for re-syncing a changed
source, tenant offboarding, or retention policies. Continuing with the product
catalog above:

```python
# Delete every document matching a filter
result = store.delete_by_filter(Tag("category") == "furniture")
print(result.processed, result.completed)

# Preview a purge before running it
preview = store.delete_by_filter(Num("price") > 100, dry_run=True)
print(preview.matched)
```

`delete_by_filter()` returns RedisVL's `BulkResult`. Check `completed` as well
as `processed`: RedisVL can stop a bulk deletion early under concurrent writes,
in which case matching documents may remain.

Filter deletion is automatically scoped to the store's own index, so indexes
sharing a `key_prefix` cannot delete each other's documents.
Generated schemas include the required exact `_index_name` TAG marker;
existing or custom schemas without that marker must be migrated or recreated
before searching, filter deletion, or direct-ID access. Existing custom TAG
fields that store the raw index name must also be migrated or reindexed: current
schemas store a hashed index-ownership marker, so changing only the field type
is not sufficient. Until migration, direct-ID operations remain available for
legacy TEXT markers, but searches and filter deletion are refused because TEXT
tokenization cannot provide exact index isolation.

#### Migrating legacy ownership markers

Use `migrate_index_ownership()` to preview and upgrade a legacy index. Supply
the original, fully populated `RedisConfig` for a generated schema, or the
original `IndexSchema` for a custom schema. Every call, including previews and
reruns, requires exactly one source, validated against the live index.

`TEMPORARY` indexes are unsupported: rebuilding them deletes their documents.

Use a separate migration client with `protocol=2`, connected to the application's
Redis deployment. Explicit `protocol=3` clients are rejected before any Redis
operations because migration does not yet support their query and cursor replies.

```python
from redis import Redis

from langchain_redis import migrate_index_ownership

migration_client = Redis.from_url(redis_url, protocol=2)

# Use the same definition that originally created the index.
migration_source = {"source_config": original_config}
# For a custom schema, use this instead:
# migration_source = {"source_schema": original_schema}

aliases = ["products-search"]  # Use [] when the index has no aliases.
preview = migrate_index_ownership(
    "products",
    redis_client=migration_client,
    aliases=aliases,
    **migration_source,
)
print(preview.schema_action, preview.would_update)

# Stop all writers and back up the index before applying.
result = migrate_index_ownership(
    "products",
    redis_client=migration_client,
    aliases=aliases,
    dry_run=False,
    **migration_source,
)
print(result.updated, result.completed)
# Restart writers and recreate existing RedisVectorStore instances afterwards.
```

Generated configurations must include `embedding_dimensions`. Validation checks
reported core vector settings and vector `INDEXMISSING`. All vector tuning is
taken from the supplied source without comparison; preserve the original tuning
and phonetic settings there.
Indexes with overlapping prefixes of the same storage type cannot be migrated
automatically because ownership of legacy documents is ambiguous. Recreate those
indexes with disjoint prefixes instead.

#### Vector index tuning

`vector_attrs` exposes algorithm-specific index attributes without writing a
full custom schema:

```python
# HNSW with tuned build/query parameters
config = RedisConfig(
    index_name="tuned_hnsw",
    indexing_algorithm="HNSW",
    vector_attrs={"m": 16, "ef_construction": 200, "ef_runtime": 20},
)

# SVS-VAMANA (Redis 8.2+) with vector compression for reduced memory
config = RedisConfig(
    index_name="compressed",
    indexing_algorithm="SVS-VAMANA",
    vector_attrs={"compression": "LeanVec4x8", "reduce": 256},
)
```

#### Using an existing index or schema

To connect to an index that already exists in Redis (created by another
process, or by redisvl directly):

```python
vector_store = RedisVectorStore.from_existing_index(
    index_name="products",
    embedding=embeddings,
    redis_url="redis://localhost:6379",
)
```

A redisvl `IndexSchema` or schema YAML file can also define the index
instead of the individual config fields:

```python
from redisvl.schema import IndexSchema

schema = IndexSchema.from_yaml("index.yaml")   # or IndexSchema.from_dict(...)
config = RedisConfig(schema=schema, redis_url="redis://localhost:6379")
vector_store = RedisVectorStore(embeddings, config=config)
```

Searches are always scoped by the internal `_index_name` ownership field.
Generated schemas add this exact TAG field automatically. Existing and custom
schemas must define `_index_name` as TAG before they can be searched; missing,
legacy TEXT, and unsupported marker fields are refused instead of running
without exact index isolation. Build metadata filters with RedisVL
`FilterExpression` helpers such as `Tag` and `Num` when values come from user
input. Non-empty raw Redis query strings are rejected because they cannot be
safely combined with the index ownership filter.

#### Features
- Efficient vector storage and retrieval
- Hybrid search combining BM25 text scoring and vector similarity (RRF or
  linear fusion), with automatic engine selection by server version
- Full-text (BM25) search with per-field weighting
- Support for metadata filtering, including wildcard tag patterns
- Filter-based bulk deletion with dry-run support
- Multiple distance metrics: Cosine similarity, L2, and Inner Product
- FLAT, HNSW, and SVS-VAMANA indexing algorithms with tunable
  algorithm-specific attributes (including SVS-VAMANA vector compression)
- Maximum marginal relevance search
- Custom key support for document indexing
- Multi-prefix indexes: one searchable index spanning several key namespaces
- Index-level stopwords configuration

### 2. Cache

The `RedisCache`, `RedisSemanticCache`, and `LangCacheSemanticCache` classes provide caching mechanisms for LLM calls.

#### Usage

```python
from langchain_redis import RedisCache, RedisSemanticCache, LangCacheSemanticCache
from langchain_openai import OpenAI, OpenAIEmbeddings
from langchain_core.outputs import Generation

# Standard cache
cache = RedisCache(redis_url="redis://localhost:6379", ttl=3600)

# Semantic cache
embeddings = OpenAIEmbeddings()  # any LangChain Embeddings implementation works
semantic_cache = RedisSemanticCache(
    redis_url="redis://localhost:6379",
    embeddings=embeddings,
    distance_threshold=0.1
)

# LangCache semantic cache - manages embeddings for you
langcache_cache = LangCacheSemanticCache(
    cache_id="your-cache-id",
    api_key="your-api-key",
    distance_threshold=0.1
)

# Using cache with an LLM
llm = OpenAI(cache=cache)  # or semantic_cache / langcache_cache

# Async cache operations
await cache.aupdate("prompt", "llm_string", [Generation(text="cached_response")])
cached_result = await cache.alookup("prompt", "llm_string")
```

#### Features
- Efficient caching of LLM responses
- TTL support for automatic cache expiration
- Semantic caching for similarity-based retrieval
- Asynchronous cache operations

#### Upgrading an existing semantic cache
`RedisSemanticCache` scopes entries by LLM with a tag field in its index. An index created by an earlier version raises a `ValueError` until it is upgraded. Stop the applications using the cache, then run:

```python
RedisSemanticCache.migrate_index(
    redis_url="redis://localhost:6379",
    name="llmcache",
    backfill=False,
)
```

This keeps the existing keys, but entries cached before the upgrade are no longer returned. Set `backfill=True` to also migrate those entries and keep the cache warm; this scans the whole cache and is safe to rerun if it gets interrupted.

#### What is Redis LangCache?
- LangCache is a fully managed, cloud-based service that provides a semantic cache for LLM applications.
- It manages embeddings and vector search for you, allowing you to focus on your application logic.
- See [our docs](https://redis.io/docs/latest/develop/ai/context-engine/langcache/) to learn more, or [try LangCache on Redis Cloud today](https://redis.io/docs/latest/operate/iris/langcache/#get-started-with-langcache-on-redis-cloud).

### 3. Chat History

The `RedisChatMessageHistory` class provides a Redis-based storage for chat message history with efficient search capabilities.

#### Usage

```python
from langchain_redis import RedisChatMessageHistory
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage

# Initialize with optional TTL (time-to-live) in seconds
history = RedisChatMessageHistory(
    session_id="user_123",
    redis_url="redis://localhost:6379",
    ttl=3600,  # Messages will expire after 1 hour
)

# Adding messages
history.add_message(HumanMessage(content="Hello, AI!"))
history.add_message(AIMessage(content="Hello, human! How can I assist you today?"))
history.add_message(SystemMessage(content="This is a system message"))

# Retrieving all messages in chronological order
messages = history.messages

# Searching messages with full-text search
results = history.search_messages("assist", limit=5)  # Returns matching messages

# Get message count
message_count = len(history)

# Clear history for current session
history.clear()

# Delete all sessions and index (use with caution)
history.delete()
```

#### Features
- Fast storage of chat messages with automatic expiration (TTL)
- Support for different message types (Human, AI, System)
- Full-text search capabilities across message content
- Chronological message retrieval
- Session-based message organization
- Customizable key prefixing
- Thread-safe operations
- Efficient RedisVL-based indexing and querying

## Advanced Configuration

The `RedisConfig` class allows for detailed configuration of the Redis integration:

```python
from langchain_redis import RedisConfig

config = RedisConfig(
    index_name="my_index",
    redis_url="redis://localhost:6379",
    distance_metric="COSINE",
    key_prefix="my_prefix",
    vector_datatype="FLOAT32",
    storage_type="hash",
    metadata_schema=[
        {"name": "category", "type": "tag"},
        {"name": "price", "type": "numeric"}
    ]
)
```

Available options include:

- `key_prefix`: a single prefix, or a list of prefixes (e.g.
  `key_prefix=["kb:team_acme", "kb:global"]`). With a list, searches cover
  every namespace and new documents are written under the first prefix.
- `stopwords`: index-level stopwords. `None` (default) uses the Redis
  defaults, `[]` disables them (`STOPWORDS 0`), and a list replaces them.
- `indexing_algorithm`: `"FLAT"`, `"HNSW"`, or `"SVS-VAMANA"`.
  Algorithm-specific parameters go in `vector_attrs` (see Vector index
  tuning above).

Refer to the inline documentation for detailed information on these configuration options.

## Error Handling and Logging

The package uses Python's standard logging module. You can configure logging to get more information about the package's operations:

```python
import logging
logging.basicConfig(level=logging.INFO)
```

The package doesn’t define custom exceptions. It raises standard Python exceptions (mainly `ValueError` for invalid input) and lets Redis and RedisVL errors propagate.

## Performance Considerations

- When loading large datasets, tune `batch_size` on `add_texts`, `add_documents`, `from_texts` or `from_documents`: larger batches mean fewer round trips to Redis and faster writes; smaller batches keep each write lighter.
- Adjust the `k` and `fetch_k` parameters in similarity searches to balance between accuracy and performance.
- Use appropriate indexing algorithms (FLAT, HNSW, SVS-VAMANA) based on your dataset size and query requirements.

## Examples

For more detailed examples and use cases, please refer to the `docs/` directory in this repository.

## Contributing / Development

The library is rooted at `libs/redis`. For all the commands below, `cd` to `libs/redis`:

### Unit Tests

To install dependencies for unit tests:

```bash
poetry install --with test
```

To run unit tests:

```bash
make test
```

To run a specific test:

```bash
TEST_FILE=tests/unit_tests/test_imports.py make test
```

### Integration Tests

You would need an OpenAI API Key to run the integration tests:

```bash
export OPENAI_API_KEY=sk-J3nnYJ3nnYWh0Can1Turnt0Ug1VeMe50mth1n1cAnH0ld0n2
```

To install dependencies for integration tests:

```bash
poetry install --with test,test_integration
```

To run integration tests:

```bash
make integration_tests
```

### Local Development

Install langchain-redis development requirements (for running langchain, running examples, linting, formatting, spell checking, tests, and coverage):

```bash
poetry install --with lint,typing,test,test_integration,codespell
```

Then verify dependency installation:

```bash
make lint
```

## License

This project is licensed under the MIT License (LICENSE).

