2023年6月28日

使用Redis进行嵌入向量搜索

本笔记本将引导您完成一个简单流程:下载一些数据,进行嵌入处理,然后使用选定的向量数据库进行索引和搜索。对于希望在安全环境中存储和搜索我们的嵌入数据以支持生产用例(如聊天机器人、主题建模等)的客户来说,这是一个常见需求。

什么是向量数据库

向量数据库是一种专为存储、管理和搜索嵌入向量而设计的数据库。近年来,由于AI在解决涉及自然语言、图像识别和其他非结构化数据用例方面的效果日益显著,使用嵌入技术将非结构化数据(文本、音频、视频等)编码为向量供机器学习模型使用的做法呈现爆发式增长。向量数据库已成为企业实现和扩展这些用例的有效解决方案。

为什么使用向量数据库

向量数据库使企业能够利用本代码库中分享的众多嵌入应用场景(例如问答系统、聊天机器人和推荐服务),并在安全、可扩展的环境中实现这些应用。许多客户在小规模场景下已通过嵌入技术解决问题,但性能和安全性阻碍了其投入生产环境——我们认为向量数据库是解决这一问题的关键组件。本指南将带您了解文本数据嵌入的基础知识,将其存储于向量数据库,并用于语义搜索。

演示流程

演示流程如下:

  • 设置: 导入包并设置任何需要的变量
  • 加载数据: 加载数据集并使用OpenAI嵌入进行编码
  • Redis
    • 设置: 配置Redis-Py客户端。更多详情请点击此处
    • 索引数据: 为所有可用字段创建搜索索引,支持向量搜索和混合搜索(向量+全文搜索)。
    • 搜索数据: 带着不同目标运行几个示例查询。

完成本笔记本的学习后,您应该对如何设置和使用向量数据库有了基本理解,可以继续探索利用我们嵌入功能的更复杂用例。

设置

导入所需的库并设置我们想要使用的嵌入模型。

# We'll need to install the Redis client
!pip install redis

#Install wget to pull zip file
!pip install wget
import openai

from typing import List, Iterator
import pandas as pd
import numpy as np
import os
import wget
from ast import literal_eval

# Redis client library for Python
import redis

# I've set this to our new embeddings model, this can be changed to the embedding model of your choice
EMBEDDING_MODEL = "text-embedding-3-small"

# Ignore unclosed SSL socket warnings - optional in case you get these errors
import warnings

warnings.filterwarnings(action="ignore", message="unclosed", category=ResourceWarning)
warnings.filterwarnings("ignore", category=DeprecationWarning) 

加载数据

在本节中,我们将加载在此会话之前准备好的嵌入数据。

embeddings_url = 'https://cdn.openai.com/API/examples/data/vector_database_wikipedia_articles_embedded.zip'

# The file is ~700 MB so this will take some time
wget.download(embeddings_url)
import zipfile
with zipfile.ZipFile("vector_database_wikipedia_articles_embedded.zip","r") as zip_ref:
    zip_ref.extractall("../data")
article_df = pd.read_csv('../data/vector_database_wikipedia_articles_embedded.csv')
article_df.head()
id url 标题 文本内容 标题向量 内容向量 向量id
0 1 https://simple.wikipedia.org/wiki/April 四月 四月是公历年中第四个月份... [0.001009464613161981, -0.020700545981526375, ... [-0.011253940872848034, -0.013491976074874401,... 0
1 2 https://simple.wikipedia.org/wiki/August August 八月(Aug.)是一年中的第八个月份... [0.0009286514250561595, 0.000820168002974242, ... [0.0003609954728744924, 0.007262262050062418, ... 1
2 6 https://simple.wikipedia.org/wiki/Art 艺术 艺术是一种表达想象力的创造性活动... [0.003393713850528002, 0.0061537534929811954, ... [-0.004959689453244209, 0.015772193670272827, ... 2
3 8 https://simple.wikipedia.org/wiki/A A A或a是英语字母表中的第一个字母... [0.0153952119871974, -0.013759135268628597, 0.... [0.024894846603274345, -0.022186409682035446, ... 3
4 9 https://simple.wikipedia.org/wiki/Air Air 空气指的是地球的大气层。空气是一种... [0.02224554680287838, -0.02044147066771984, -... [0.021524671465158463, 0.018522677943110466, -... 4
# Read vectors from strings back into a list
article_df['title_vector'] = article_df.title_vector.apply(literal_eval)
article_df['content_vector'] = article_df.content_vector.apply(literal_eval)

# Set vector_id to be a string
article_df['vector_id'] = article_df['vector_id'].apply(str)
article_df.info(show_counts=True)
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 25000 entries, 0 to 24999
Data columns (total 7 columns):
 #   Column          Non-Null Count  Dtype 
---  ------          --------------  ----- 
 0   id              25000 non-null  int64 
 1   url             25000 non-null  object
 2   title           25000 non-null  object
 3   text            25000 non-null  object
 4   title_vector    25000 non-null  object
 5   content_vector  25000 non-null  object
 6   vector_id       25000 non-null  object
dtypes: int64(1), object(6)
memory usage: 1.3+ MB

Redis

本教程介绍的下一款向量数据库是Redis。您很可能已经了解Redis。但您可能不知道的是RediSearch模块。多年来,企业一直在所有主要云服务提供商、Redis Cloud以及本地环境中使用带有RediSearch模块的Redis。最近,Redis团队在该模块中除了原有功能外,还新增了向量存储和搜索能力。

鉴于Redis庞大的生态系统,很可能已有您所需语言的客户端库。您可以使用任何标准的Redis客户端库来执行RediSearch命令,但使用封装了RediSearch API的库最为便捷。以下是几个示例,您可以在此处找到更多客户端库。

项目语言许可证作者星标数
jedisJavaMITRedisStars
redis-pyPythonMITRedisStars
node-redisNode.jsMITRedisStars
nredisstack.NETMITRedisStars
redisearch-goGoBSDRedisredisearch-go-stars
redisearch-api-rsRustBSDRedisredisearch-api-rs-stars

在下面的单元格中,我们将引导您使用Redis作为向量数据库。由于你们中的许多人可能已经熟悉Redis API,这对大多数人来说应该很熟悉。

设置

有多种方式可以部署带有RediSearch的Redis。最简单的方法是使用Docker,但部署时还有许多其他可选方案。如需了解其他部署选项,请参阅本仓库中的redis目录

本教程中,我们将使用Docker上的Redis Stack。

通过运行以下docker命令启动一个带有RediSearch(Redis Stack)的Redis版本

$ cd redis
$ docker compose up -d

这还包括用于管理Redis数据库的RedisInsight图形界面,启动docker容器后可通过http://localhost:8001访问该界面。

您已完成所有设置,可以开始使用了!接下来,我们将导入并创建客户端,用于与刚刚创建的Redis数据库进行通信。

import redis
from redis.commands.search.indexDefinition import (
    IndexDefinition,
    IndexType
)
from redis.commands.search.query import Query
from redis.commands.search.field import (
    TextField,
    VectorField
)

REDIS_HOST =  "localhost"
REDIS_PORT = 6379
REDIS_PASSWORD = "" # default for passwordless Redis

# Connect to Redis
redis_client = redis.Redis(
    host=REDIS_HOST,
    port=REDIS_PORT,
    password=REDIS_PASSWORD
)
redis_client.ping()
True

创建搜索索引

以下单元格将展示如何在Redis中指定和创建搜索索引。我们将

  1. 设置一些常量来定义我们的索引,比如距离度量和索引名称
  2. 使用RediSearch字段定义索引模式
  3. 创建索引
# Constants
VECTOR_DIM = len(article_df['title_vector'][0]) # length of the vectors
VECTOR_NUMBER = len(article_df)                 # initial number of vectors
INDEX_NAME = "embeddings-index"                 # name of the search index
PREFIX = "doc"                                  # prefix for the document keys
DISTANCE_METRIC = "COSINE"                      # distance metric for the vectors (ex. COSINE, IP, L2)
# Define RediSearch fields for each of the columns in the dataset
title = TextField(name="title")
url = TextField(name="url")
text = TextField(name="text")
title_embedding = VectorField("title_vector",
    "FLAT", {
        "TYPE": "FLOAT32",
        "DIM": VECTOR_DIM,
        "DISTANCE_METRIC": DISTANCE_METRIC,
        "INITIAL_CAP": VECTOR_NUMBER,
    }
)
text_embedding = VectorField("content_vector",
    "FLAT", {
        "TYPE": "FLOAT32",
        "DIM": VECTOR_DIM,
        "DISTANCE_METRIC": DISTANCE_METRIC,
        "INITIAL_CAP": VECTOR_NUMBER,
    }
)
fields = [title, url, text, title_embedding, text_embedding]
# Check if index exists
try:
    redis_client.ft(INDEX_NAME).info()
    print("Index already exists")
except:
    # Create RediSearch Index
    redis_client.ft(INDEX_NAME).create_index(
        fields = fields,
        definition = IndexDefinition(prefix=[PREFIX], index_type=IndexType.HASH)
    )

将文档加载到索引中

现在我们已经有了一个搜索索引,可以将文档加载到其中。我们将使用与之前示例相同的文档。在Redis中,可以使用Hash或JSON(如果除了RediSearch还使用RedisJSON)数据类型来存储文档。在本示例中,我们将使用HASH数据类型。下面的单元格将展示如何将文档加载到索引中。

def index_documents(client: redis.Redis, prefix: str, documents: pd.DataFrame):
    records = documents.to_dict("records")
    for doc in records:
        key = f"{prefix}:{str(doc['id'])}"

        # create byte vectors for title and content
        title_embedding = np.array(doc["title_vector"], dtype=np.float32).tobytes()
        content_embedding = np.array(doc["content_vector"], dtype=np.float32).tobytes()

        # replace list of floats with byte vectors
        doc["title_vector"] = title_embedding
        doc["content_vector"] = content_embedding

        client.hset(key, mapping = doc)
index_documents(redis_client, PREFIX, article_df)
print(f"Loaded {redis_client.info()['db0']['keys']} documents in Redis search index with name: {INDEX_NAME}")
Loaded 25000 documents in Redis search index with name: embeddings-index

运行搜索查询

现在我们已经有了一个搜索索引并将文档加载其中,可以运行搜索查询了。下面我们将提供一个函数来执行搜索查询并返回结果。通过这个函数,我们将运行几个查询示例,展示如何利用Redis作为向量数据库。每个例子都将演示在使用Redis开发搜索应用时需要留意的特定功能。

  1. 返回字段: 您可以指定希望在搜索结果中返回哪些字段。如果您只想返回文档中的部分字段而不需要单独调用检索文档,这将非常有用。在下面的示例中,我们将仅在搜索结果中返回title字段。
  2. 混合搜索: 您可以将向量搜索与任何其他RediSearch字段(如全文搜索、标签、地理和数值)结合使用进行混合搜索。在下面的示例中,我们将把向量搜索与全文搜索结合起来。
def search_redis(
    redis_client: redis.Redis,
    user_query: str,
    index_name: str = "embeddings-index",
    vector_field: str = "title_vector",
    return_fields: list = ["title", "url", "text", "vector_score"],
    hybrid_fields = "*",
    k: int = 20,
) -> List[dict]:

    # Creates embedding vector from user query
    embedded_query = openai.Embedding.create(input=user_query,
                                            model=EMBEDDING_MODEL,
                                            )["data"][0]['embedding']

    # Prepare the Query
    base_query = f'{hybrid_fields}=>[KNN {k} @{vector_field} $vector AS vector_score]'
    query = (
        Query(base_query)
         .return_fields(*return_fields)
         .sort_by("vector_score")
         .paging(0, k)
         .dialect(2)
    )
    params_dict = {"vector": np.array(embedded_query).astype(dtype=np.float32).tobytes()}

    # perform vector search
    results = redis_client.ft(index_name).search(query, params_dict)
    for i, article in enumerate(results.docs):
        score = 1 - float(article.vector_score)
        print(f"{i}. {article.title} (Score: {round(score ,3) })")
    return results.docs
# For using OpenAI to generate query embedding
openai.api_key = os.getenv("OPENAI_API_KEY", "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
results = search_redis(redis_client, 'modern art in Europe', k=10)
0. Museum of Modern Art (Score: 0.875)
1. Western Europe (Score: 0.867)
2. Renaissance art (Score: 0.864)
3. Pop art (Score: 0.86)
4. Northern Europe (Score: 0.855)
5. Hellenistic art (Score: 0.853)
6. Modernist literature (Score: 0.847)
7. Art film (Score: 0.843)
8. Central Europe (Score: 0.843)
9. European (Score: 0.841)
results = search_redis(redis_client, 'Famous battles in Scottish history', vector_field='content_vector', k=10)
0. Battle of Bannockburn (Score: 0.869)
1. Wars of Scottish Independence (Score: 0.861)
2. 1651 (Score: 0.853)
3. First War of Scottish Independence (Score: 0.85)
4. Robert I of Scotland (Score: 0.846)
5. 841 (Score: 0.844)
6. 1716 (Score: 0.844)
7. 1314 (Score: 0.837)
8. 1263 (Score: 0.836)
9. William Wallace (Score: 0.835)

基于Redis的混合查询

前面的示例展示了如何使用RediSearch运行向量搜索查询。在本节中,我们将展示如何将向量搜索与其他RediSearch字段结合进行混合搜索。在下面的示例中,我们将把向量搜索与全文搜索结合起来。

def create_hybrid_field(field_name: str, value: str) -> str:
    return f'@{field_name}:"{value}"'
# search the content vector for articles about famous battles in Scottish history and only include results with Scottish in the title
results = search_redis(redis_client,
                       "Famous battles in Scottish history",
                       vector_field="title_vector",
                       k=5,
                       hybrid_fields=create_hybrid_field("title", "Scottish")
                       )
0. First War of Scottish Independence (Score: 0.892)
1. Wars of Scottish Independence (Score: 0.889)
2. Second War of Scottish Independence (Score: 0.879)
3. List of Scottish monarchs (Score: 0.873)
4. Scottish Borders (Score: 0.863)
# run a hybrid query for articles about Art in the title vector and only include results with the phrase "Leonardo da Vinci" in the text
results = search_redis(redis_client,
                       "Art",
                       vector_field="title_vector",
                       k=5,
                       hybrid_fields=create_hybrid_field("text", "Leonardo da Vinci")
                       )

# find specific mention of Leonardo da Vinci in the text that our full-text-search query returned
mention = [sentence for sentence in results[0].text.split("\n") if "Leonardo da Vinci" in sentence][0]
mention
0. Art (Score: 1.0)
1. Paint (Score: 0.896)
2. Renaissance art (Score: 0.88)
3. Painting (Score: 0.874)
4. Renaissance (Score: 0.846)
'In Europe, after the Middle Ages, there was a "Renaissance" which means "rebirth". People rediscovered science and artists were allowed to paint subjects other than religious subjects. People like Michelangelo and Leonardo da Vinci still painted religious pictures, but they also now could paint mythological pictures too. These artists also invented perspective where things in the distance look smaller in the picture. This was new because in the Middle Ages people would paint all the figures close up and just overlapping each other. These artists used nudity regularly in their art.'

如需更多使用Redis作为向量数据库的示例,请参阅本仓库vector_databases/redis目录中的README和示例