Redis 向量存储¶
在本笔记本中,我们将快速演示如何使用 RedisVectorStore。
如果您在 Colab 上打开此 Notebook,可能需要安装 LlamaIndex 🦙。
%pip install -U llama-index llama-index-vector-stores-redis llama-index-embeddings-cohere llama-index-embeddings-openai
import os
import getpass
import sys
import logging
import textwrap
import warnings
warnings.filterwarnings("ignore")
# Uncomment to see debug logs
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
from llama_index.vector_stores.redis import RedisVectorStore
启动 Redis¶
启动 Redis 最简单的方式是使用 Redis Stack Docker 镜像,或快速注册一个 免费的 Redis Cloud 实例。
要跟随本教程的每个步骤,请按以下方式启动镜像:
docker run --name redis-vecdb -d -p 6379:6379 -p 8001:8001 redis/redis-stack:latest
该命令还会在 8001 端口启动 RedisInsight 可视化界面,可通过 http://localhost:8001 访问。
配置 OpenAI¶
首先让我们添加 OpenAI 的 API 密钥。这将使我们能够访问 OpenAI 以获取嵌入向量并使用 ChatGPT。
oai_api_key = getpass.getpass("OpenAI API Key:")
os.environ["OPENAI_API_KEY"] = oai_api_key
下载数据
!mkdir -p 'data/paul_graham/'
!wget 'https://raw.githubusercontent.com/run-llama/llama_index/main/docs/docs/examples/data/paul_graham/paul_graham_essay.txt' -O 'data/paul_graham/paul_graham_essay.txt'
--2024-04-10 19:35:33-- https://raw.githubusercontent.com/run-llama/llama_index/main/docs/docs/examples/data/paul_graham/paul_graham_essay.txt Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 2606:50c0:8003::154, 2606:50c0:8000::154, 2606:50c0:8002::154, ... Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|2606:50c0:8003::154|:443... connected. HTTP request sent, awaiting response... 200 OK Length: 75042 (73K) [text/plain] Saving to: ‘data/paul_graham/paul_graham_essay.txt’ data/paul_graham/pa 100%[===================>] 73.28K --.-KB/s in 0.03s 2024-04-10 19:35:33 (2.15 MB/s) - ‘data/paul_graham/paul_graham_essay.txt’ saved [75042/75042]
读取数据集¶
这里我们将使用一系列保罗·格雷厄姆(Paul Graham)的散文作为文本来源,将其转化为嵌入向量,存储到 RedisVectorStore 中,并通过查询为我们的LLM问答循环提供上下文。
# load documents
documents = SimpleDirectoryReader("./data/paul_graham").load_data()
print(
"Document ID:",
documents[0].id_,
"Document Filename:",
documents[0].metadata["file_name"],
)
Document ID: 7056f7ba-3513-4ef4-9792-2bd28040aaed Document Filename: paul_graham_essay.txt
初始化默认 Redis 向量存储¶
现在我们已经准备好了文档,可以使用默认设置来初始化 Redis 向量存储。这将允许我们将向量存储在 Redis 中,并创建一个用于实时搜索的索引。
from llama_index.core import StorageContext
from redis import Redis
# create a Redis client connection
redis_client = Redis.from_url("redis://localhost:6379")
# create the vector store wrapper
vector_store = RedisVectorStore(redis_client=redis_client, overwrite=True)
# load storage context
storage_context = StorageContext.from_defaults(vector_store=vector_store)
# build and load index from documents and storage context
index = VectorStoreIndex.from_documents(
documents, storage_context=storage_context
)
# index = VectorStoreIndex.from_vector_store(vector_store=vector_store)
19:39:17 llama_index.vector_stores.redis.base INFO Using default RedisVectorStore schema. 19:39:19 httpx INFO HTTP Request: POST https://api.openai.com/v1/embeddings "HTTP/1.1 200 OK" 19:39:19 llama_index.vector_stores.redis.base INFO Added 22 documents to index llama_index
查询默认向量存储¶
现在我们的数据已存入索引,可以针对该索引进行提问。
索引将把这些数据作为大语言模型(LLM)的知识库。as_query_engine() 的默认设置使用 OpenAI 的嵌入技术和 GPT 作为语言模型,因此除非选择自定义或本地语言模型,否则需要提供 OpenAI 密钥。
接下来我们将测试针对索引的搜索功能,然后结合 LLM 实现完整的检索增强生成(RAG)。
query_engine = index.as_query_engine()
retriever = index.as_retriever()
result_nodes = retriever.retrieve("What did the author learn?")
for node in result_nodes:
print(node)
19:39:22 httpx INFO HTTP Request: POST https://api.openai.com/v1/embeddings "HTTP/1.1 200 OK" 19:39:22 llama_index.vector_stores.redis.base INFO Querying index llama_index with filters * 19:39:22 llama_index.vector_stores.redis.base INFO Found 2 results for query with id ['llama_index/vector_adb6b7ce-49bb-4961-8506-37082c02a389', 'llama_index/vector_e39be1fe-32d0-456e-b211-4efabd191108'] Node ID: adb6b7ce-49bb-4961-8506-37082c02a389 Text: What I Worked On February 2021 Before college the two main things I worked on, outside of school, were writing and programming. I didn't write essays. I wrote what beginning writers were supposed to write then, and probably still are: short stories. My stories were awful. They had hardly any plot, just characters with strong feelings, which I ... Score: 0.820 Node ID: e39be1fe-32d0-456e-b211-4efabd191108 Text: Except for a few officially anointed thinkers who went to the right parties in New York, the only people allowed to publish essays were specialists writing about their specialties. There were so many essays that had never been written, because there had been no way to publish them. Now they could be, and I was going to write them. [12] I've wor... Score: 0.819
response = query_engine.query("What did the author learn?")
print(textwrap.fill(str(response), 100))
19:39:25 httpx INFO HTTP Request: POST https://api.openai.com/v1/embeddings "HTTP/1.1 200 OK" 19:39:25 llama_index.vector_stores.redis.base INFO Querying index llama_index with filters * 19:39:25 llama_index.vector_stores.redis.base INFO Found 2 results for query with id ['llama_index/vector_adb6b7ce-49bb-4961-8506-37082c02a389', 'llama_index/vector_e39be1fe-32d0-456e-b211-4efabd191108'] 19:39:27 httpx INFO HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK" The author learned that working on things that weren't prestigious often led to valuable discoveries and indicated the right kind of motives. Despite the lack of initial prestige, pursuing such work could be a sign of genuine potential and appropriate motivations, steering clear of the common pitfall of being driven solely by the desire to impress others.
result_nodes = retriever.retrieve("What was a hard moment for the author?")
for node in result_nodes:
print(node)
19:39:27 httpx INFO HTTP Request: POST https://api.openai.com/v1/embeddings "HTTP/1.1 200 OK" 19:39:27 llama_index.vector_stores.redis.base INFO Querying index llama_index with filters * 19:39:27 llama_index.vector_stores.redis.base INFO Found 2 results for query with id ['llama_index/vector_adb6b7ce-49bb-4961-8506-37082c02a389', 'llama_index/vector_e39be1fe-32d0-456e-b211-4efabd191108'] Node ID: adb6b7ce-49bb-4961-8506-37082c02a389 Text: What I Worked On February 2021 Before college the two main things I worked on, outside of school, were writing and programming. I didn't write essays. I wrote what beginning writers were supposed to write then, and probably still are: short stories. My stories were awful. They had hardly any plot, just characters with strong feelings, which I ... Score: 0.802 Node ID: e39be1fe-32d0-456e-b211-4efabd191108 Text: Except for a few officially anointed thinkers who went to the right parties in New York, the only people allowed to publish essays were specialists writing about their specialties. There were so many essays that had never been written, because there had been no way to publish them. Now they could be, and I was going to write them. [12] I've wor... Score: 0.799
response = query_engine.query("What was a hard moment for the author?")
print(textwrap.fill(str(response), 100))
19:39:29 httpx INFO HTTP Request: POST https://api.openai.com/v1/embeddings "HTTP/1.1 200 OK" 19:39:29 llama_index.vector_stores.redis.base INFO Querying index llama_index with filters * 19:39:29 llama_index.vector_stores.redis.base INFO Found 2 results for query with id ['llama_index/vector_adb6b7ce-49bb-4961-8506-37082c02a389', 'llama_index/vector_e39be1fe-32d0-456e-b211-4efabd191108'] 19:39:31 httpx INFO HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK" A hard moment for the author was when one of his programs on the IBM 1401 mainframe didn't terminate, leading to a technical error and an uncomfortable situation with the data center manager.
index.vector_store.delete_index()
19:39:34 llama_index.vector_stores.redis.base INFO Deleting index llama_index
from llama_index.core.settings import Settings
from llama_index.embeddings.cohere import CohereEmbedding
# set up Cohere Key
co_api_key = getpass.getpass("Cohere API Key:")
os.environ["CO_API_KEY"] = co_api_key
# set llamaindex to use Cohere embeddings
Settings.embed_model = CohereEmbedding()
from redisvl.schema import IndexSchema
custom_schema = IndexSchema.from_dict(
{
# customize basic index specs
"index": {
"name": "paul_graham",
"prefix": "essay",
"key_separator": ":",
},
# customize fields that are indexed
"fields": [
# required fields for llamaindex
{"type": "tag", "name": "id"},
{"type": "tag", "name": "doc_id"},
{"type": "text", "name": "text"},
# custom metadata fields
{"type": "numeric", "name": "updated_at"},
{"type": "tag", "name": "file_name"},
# custom vector field definition for cohere embeddings
{
"type": "vector",
"name": "vector",
"attrs": {
"dims": 1024,
"algorithm": "hnsw",
"distance_metric": "cosine",
},
},
],
}
)
custom_schema.index
IndexInfo(name='paul_graham', prefix='essay', key_separator=':', storage_type=<StorageType.HASH: 'hash'>)
custom_schema.fields
{'id': TagField(name='id', type='tag', path=None, attrs=TagFieldAttributes(sortable=False, separator=',', case_sensitive=False, withsuffixtrie=False)),
'doc_id': TagField(name='doc_id', type='tag', path=None, attrs=TagFieldAttributes(sortable=False, separator=',', case_sensitive=False, withsuffixtrie=False)),
'text': TextField(name='text', type='text', path=None, attrs=TextFieldAttributes(sortable=False, weight=1, no_stem=False, withsuffixtrie=False, phonetic_matcher=None)),
'updated_at': NumericField(name='updated_at', type='numeric', path=None, attrs=NumericFieldAttributes(sortable=False)),
'file_name': TagField(name='file_name', type='tag', path=None, attrs=TagFieldAttributes(sortable=False, separator=',', case_sensitive=False, withsuffixtrie=False)),
'vector': HNSWVectorField(name='vector', type='vector', path=None, attrs=HNSWVectorFieldAttributes(dims=1024, algorithm=<VectorIndexAlgorithm.HNSW: 'HNSW'>, datatype=<VectorDataType.FLOAT32: 'FLOAT32'>, distance_metric=<VectorDistanceMetric.COSINE: 'COSINE'>, initial_cap=None, m=16, ef_construction=200, ef_runtime=10, epsilon=0.01))}
了解更多关于 Redis 的模式与索引设计。
from datetime import datetime
def date_to_timestamp(date_string: str) -> int:
date_format: str = "%Y-%m-%d"
return int(datetime.strptime(date_string, date_format).timestamp())
# iterate through documents and add new field
for document in documents:
document.metadata["updated_at"] = date_to_timestamp(
document.metadata["last_modified_date"]
)
vector_store = RedisVectorStore(
schema=custom_schema, # provide customized schema
redis_client=redis_client,
overwrite=True,
)
storage_context = StorageContext.from_defaults(vector_store=vector_store)
# build and load index from documents and storage context
index = VectorStoreIndex.from_documents(
documents, storage_context=storage_context
)
19:40:05 httpx INFO HTTP Request: POST https://api.cohere.ai/v1/embed "HTTP/1.1 200 OK" 19:40:06 httpx INFO HTTP Request: POST https://api.cohere.ai/v1/embed "HTTP/1.1 200 OK" 19:40:06 httpx INFO HTTP Request: POST https://api.cohere.ai/v1/embed "HTTP/1.1 200 OK" 19:40:06 llama_index.vector_stores.redis.base INFO Added 22 documents to index paul_graham
查询向量存储并基于元数据过滤¶
现在我们已经将额外的元数据索引到 Redis 中,接下来尝试一些带过滤条件的查询。
from llama_index.core.vector_stores import (
MetadataFilters,
MetadataFilter,
ExactMatchFilter,
)
retriever = index.as_retriever(
similarity_top_k=3,
filters=MetadataFilters(
filters=[
ExactMatchFilter(key="file_name", value="paul_graham_essay.txt"),
MetadataFilter(
key="updated_at",
value=date_to_timestamp("2023-01-01"),
operator=">=",
),
MetadataFilter(
key="text",
value="learn",
operator="text_match",
),
],
condition="and",
),
)
result_nodes = retriever.retrieve("What did the author learn?")
for node in result_nodes:
print(node)
19:40:22 httpx INFO HTTP Request: POST https://api.cohere.ai/v1/embed "HTTP/1.1 200 OK"
19:40:22 llama_index.vector_stores.redis.base INFO Querying index paul_graham with filters ((@file_name:{paul_graham_essay\.txt} @updated_at:[1672549200 +inf]) @text:(learn))
19:40:22 llama_index.vector_stores.redis.base INFO Found 3 results for query with id ['essay:0df3b734-ecdb-438e-8c90-f21a8c80f552', 'essay:01108c0d-140b-4dcc-b581-c38b7df9251e', 'essay:ced36463-ac36-46b0-b2d7-935c1b38b781']
Node ID: 0df3b734-ecdb-438e-8c90-f21a8c80f552
Text: All that seemed left for philosophy were edge cases that people
in other fields felt could safely be ignored. I couldn't have put
this into words when I was 18. All I knew at the time was that I kept
taking philosophy courses and they kept being boring. So I decided to
switch to AI. AI was in the air in the mid 1980s, but there were two
things...
Score: 0.410
Node ID: 01108c0d-140b-4dcc-b581-c38b7df9251e
Text: It was not, in fact, simply a matter of teaching SHRDLU more
words. That whole way of doing AI, with explicit data structures
representing concepts, was not going to work. Its brokenness did, as
so often happens, generate a lot of opportunities to write papers
about various band-aids that could be applied to it, but it was never
going to get us ...
Score: 0.390
Node ID: ced36463-ac36-46b0-b2d7-935c1b38b781
Text: Grad students could take classes in any department, and my
advisor, Tom Cheatham, was very easy going. If he even knew about the
strange classes I was taking, he never said anything. So now I was in
a PhD program in computer science, yet planning to be an artist, yet
also genuinely in love with Lisp hacking and working away at On Lisp.
In other...
Score: 0.389
从 Redis 现有索引恢复¶
从索引恢复需要提供 Redis 连接客户端(或 URL)、设置 overwrite=False 参数,并传入之前使用的相同 schema 对象。(为方便起见,可通过 .to_yaml() 方法将 schema 对象导出为 YAML 文件进行配置)
custom_schema.to_yaml("paul_graham.yaml")
vector_store = RedisVectorStore(
schema=IndexSchema.from_yaml("paul_graham.yaml"),
redis_client=redis_client,
)
index = VectorStoreIndex.from_vector_store(vector_store=vector_store)
19:40:28 redisvl.index.index INFO Index already exists, not overwriting.
在不久的将来——我们将实现一个便捷方法,仅通过索引名称即可加载:
RedisVectorStore.from_existing_index(index_name="paul_graham", redis_client=redis_client)
彻底删除文档或索引¶
有时可能需要删除文档或整个索引,这时可以使用 delete 和 delete_index 方法来实现。
document_id = documents[0].doc_id
document_id
'7056f7ba-3513-4ef4-9792-2bd28040aaed'
print("Number of documents before deleting", redis_client.dbsize())
vector_store.delete(document_id)
print("Number of documents after deleting", redis_client.dbsize())
Number of documents before deleting 22 19:40:32 llama_index.vector_stores.redis.base INFO Deleted 22 documents from index paul_graham Number of documents after deleting 0
然而,Redis 索引仍然存在(未关联任何文档),以支持持续更新操作。
vector_store.index_exists()
True
# now lets delete the index entirely
# this will delete all the documents and the index
vector_store.delete_index()
19:40:37 llama_index.vector_stores.redis.base INFO Deleting index paul_graham
print("Number of documents after deleting", redis_client.dbsize())
Number of documents after deleting 0
故障排查¶
若查询结果为空,请检查以下常见问题:
索引结构¶
与其他向量数据库不同,Redis 要求用户显式定义索引结构,主要原因包括:
- Redis 支持多种应用场景,既包括实时向量搜索,也涵盖标准文档存储/检索、缓存、消息传递、发布订阅、会话管理等。并非所有记录属性都需要建立搜索索引,这既出于效率考量,也旨在减少用户误操作风险。
- 使用 Redis 和 LlamaIndex 时,所有索引结构至少必须包含以下字段:
id、doc_id、text和vector。
初始化 RedisVectorStore 时可使用默认结构(假设采用 OpenAI 嵌入模型),或自定义结构(参见前文说明)。
键前缀问题¶
Redis 要求所有记录必须包含键前缀,用于将键空间划分为不同"分区",以支持潜在的不同应用、用例和客户端。
请确保索引结构中指定的 prefix 在代码中保持一致(与特定索引绑定)。要查看索引创建时使用的前缀,可在 Redis CLI 中执行 FT.INFO <索引名称> 并查看 index_definition => prefixes 字段。
数据与索引分离¶
Redis 将数据集中的记录与索引视为独立实体,这种设计为更新、增量更新和索引结构迁移提供了更高灵活性。
若存在现有索引需要删除,可在 Redis CLI 中执行 FT.DROPINDEX <索引名称>。注意:除非传递 DD 参数,否则该操作不会删除实际数据。
使用元数据时返回空查询¶
如果在索引创建完成后才添加元数据,并尝试基于该元数据进行查询,将返回空结果。
Redis 仅在索引创建时对字段建立索引(与前述前缀索引机制类似)。