首页 / 文章

将 Redis 向量演示迁移到共享的 Azure AI Search 索引中

将 LangGraph 向量搜索从 Redis 示例迁移到共享的 Azure AI Search 索引:声明架构,按会话过滤,通过查询删除数据,同时保留其他用户的数据。

发布者:REACTAPP.TOP阅读约 6 分钟1396 词

来源: From Redis to Azure AI Search: The Full Migration

为何 Redis 并非最终选择

本地 Redis 向量搜索非常适合在笔记本电脑上做演示。而共享环境、基于服务等级协议的搜索功能以及 Azure 原生的身份验证机制,则促使团队转向 Azure AI Search。这种迁移方式保留了 LangGraph 流程,仅更换了向量后端。

传统 AI 搜索与这种模式的区别

传统的关键词搜索是根据词汇重叠度来排序的。而采用迁移后的方案则会在文本旁存储嵌入向量,并通过语义相似度进行检索——必要时还可结合过滤器使用。Azure AI Search 则成为这些向量的托管中心。

必须做出的那个决定:共享索引还是会话级索引

Redis演示通常会为每个会话创建一个临时索引。而Azure AI Search则更倾向于使用带有会话(或租户)过滤字段的共享索引。通过删除整个索引来“清除会话”是错误的方法,正确的做法是通过过滤条件进行删除。

uv add langgraph langchain-core langchain-openai langchain-text-splitters \
       azure-ai-documentintelligence azure-core \
       langchain-redis redisvl \
       python-dotenv
def get_vector_store() -> AzureSearch:
    global _vector_store
    if _vector_store is None:
        _vector_store = AzureSearch(
            azure_search_endpoint=config.AZURE_SEARCH_ENDPOINT,
            azure_search_key=config.AZURE_SEARCH_API_KEY,
            index_name=config.AZURE_SEARCH_INDEX_NAME,
            embedding_function=embeddings,
            fields=_FIELDS,
            vector_search_dimensions=config.EMBEDDING_DIMENSIONS,
        )
    return _vector_store
_FIELDS = [
    SimpleField(name="id", type=SearchFieldDataType.String, key=True, filterable=True),
    SearchableField(name="content", type=SearchFieldDataType.String),
    SearchField(
        name="content_vector",
        type="Collection(Edm.Single)",
        searchable=True,
        vector_search_dimensions=config.EMBEDDING_DIMENSIONS,
        vector_search_profile_name="myHnswProfile",
    ),
    SearchableField(name="metadata", type=SearchFieldDataType.String),
    SimpleField(name="session_id", type=SearchFieldDataType.String, filterable=True),
    SimpleField(name="file_name", type=SearchFieldDataType.String, filterable=True),
    SimpleField(name="chunk_index", type=SearchFieldDataType.Int32, filterable=True, sortable=True),
]
state["retrieved"] = vs.similarity_search(
    state["query"],
    k=config.TOP_K,
    filters=f"session_id eq '{state['session_id']}'",
)
def clear_session(session_id: str) -> None:
    vs = get_vector_store()
    results = vs.client.search(
        search_text="*",
        filter=f"session_id eq '{session_id}'",
        select=["id"],
    )
    ids = [r["id"] for r in results]
    if ids:
        vs.delete(ids=ids)
az search service create \
  --name "$SEARCH_SERVICE_NAME" \
  --resource-group "$RESOURCE_GROUP" \
  --location "$LOCATION" \
  --sku basic \
  --partition-count 1 \
  --replica-count 1

SEARCH_ADMIN_KEY=$(az search admin-key show \
  --resource-group "$RESOURCE_GROUP" \
  --service-name "$SEARCH_SERVICE_NAME" \
  --query primaryKey -o tsv)
AZURE_OPENAI_ENDPOINT=https://aif-cus-aisearchegs-dev-01.cognitiveservices.azure.com/
AZURE_OPENAI_API_KEY=<<redacted>>
# AZURE_OPENAI_API_VERSION = os.environ.get("AZURE_OPENAI_API_VERSION", "2024-10-21")
AZURE_OPENAI_EMBEDDING_DEPLOYMENT=text-embedding-3-small
AZURE_OPENAI_CHAT_DEPLOYMENT="gpt-5-mini"
# --- Azure Document Intelligence (used for all file text extraction) ---
DOCUMENTINTELLIGENCE_ENDPOINT=https://adi-cus-aisearchegs-dev-01.cognitiveservices.azure.com/
DOCUMENTINTELLIGENCE_KEY=<<redacted>>
AZURE_SEARCH_ENDPOINT=https://ais-cus-aisearchegs-dev-01.search.windows.net
AZURE_SEARCH_API_KEY=<<redacted>>
AZURE_SEARCH_INDEX_NAME=chat-file-search-index
azure.core.exceptions.HttpResponseError: The vector field 'content_vector'
must have the properties 'dimensions' and 'vectorSearchConfiguration' set.
TypeError: Object of type float32 is not JSON serializable
from azure.search.documents._utils.model_base import SdkJSONEncoder
_original_default = SdkJSONEncoder.default

def _numpy_safe_default(self, o):
    if isinstance(o, np.floating):
        return float(o)
    if isinstance(o, np.integer):
        return int(o)
    return _original_default(self, o)

SdkJSONEncoder.default = _numpy_safe_default
uv run python -m aisearchmanualvectorization.main \
  --session demo1 \
  --files ./source/sample_bank_statement.pdf ./source/sample_loan_agreement.pdf
uv run python -m aisearchmanualvectorization.main \
  --session demo1 \
  --query "What's the overdraft protection fee?"

A: The overdraft protection transfer fee is $10.00 per occurrence.

Sources:
  - sample_bank_statement.pdf (chunk 7): Overdraft Protection...
  - sample_bank_statement.pdf (chunk 8): Interest Rate & Annual Percentage Yield...
  - sample_bank_statement.pdf (chunk 9): Monthly Maintenance Fee...
  - sample_bank_statement.pdf (chunk 3): <td>-$10.44</td> <td>$8,096.52</td>...

逐步说明:代码中发生了哪些变化

get_vector_store() —— 不再是会话级作用域

现在返回的是与共享索引名称绑定的客户端。会话隔离功能通过元数据过滤器实现,而非独立的索引。

字段结构 —— 明确声明

需要提前定义向量维度、内容字段以及可过滤的会话ID。在Redis中有效的默认设置无法匹配Azure AI Search的字段类型。

retrieve_node — 使用字符串过滤器而非过滤对象

使用 session_id eq '…'(或等效的OData过滤器),以防止某个用户的数据片段混入另一个用户的结果中。

clear_session() — 通过查询删除,而非直接删除索引

针对会话密钥发起带过滤条件的删除操作。为每个会话重新创建索引会导致服务达到限制,并延缓系统启动速度。

迁移检查清单

  • 使嵌入维度与索引向量字段匹配
  • 仅在嵌入模型发生变化时才重新进行嵌入处理;否则直接重新插入现有数据片段
  • 在切换之前,用Redis和Azure AI Search同时处理典型查询案例
  • 尽可能将敏感信息移至Key Vault/托管身份系统中
  • 更新操作手册:不再假设“重启docker中的redis即可清除状态”

结果与权衡

Azure AI Search提供了托管式扩展能力、门户工具以及基于角色的访问控制功能。同时它也带来了资源配置的延迟问题以及共享索引的设计限制。对于PDF问答图谱而言,节点结构保持稳定,仅有存储适配器和会话生命周期发生了变化。

总结

可将Redis视为辅助轮,而Azure AI Search视为主行驶轮胎。在过滤条件中保持会话隔离,明确声明数据结构,并通过查询来删除数据。LangGraph编排系统几乎不会察觉到这些变化——如果它有所反应,那就说明抽象层存在过大的信息泄露。

切换建议:在Azure AI Search处理生产流量期间,让Redis保持只读状态一周,同时对比黄金问题集上的前三名结果重叠度。如果重叠度下降,应在指责嵌入模型之前先检查过滤条件及字段名称。这类迁移中出现的大多数功能退化问题都是过滤错误,而非向量计算错误。

切换建议:在 Azure AI Search 处理生产流量期间,将 Redis 设为只读状态一周,并对比黄金问题集上前3名的重叠情况。如果重叠度下降,应在指责嵌入模型之前先检查过滤条件及字段名称。此类迁移中出现的问题大多属于过滤错误,而非向量数学错误。

切换建议:在 Azure AI Search 处理生产流量期间,将 Redis 设为只读状态一周,并对比黄金问题集上前3名的重叠情况。如果重叠度下降,应在指责嵌入模型之前先检查过滤条件及字段名称。此类迁移中出现的问题大多属于过滤错误,而非向量数学错误。

切换建议:在 Azure AI Search 处理生产流量期间,将 Redis 设为只读状态一周,并对比黄金问题集上前3名的重叠情况。如果重叠度下降,应在指责嵌入模型之前先检查过滤条件及字段名称。此类迁移中出现的问题大多属于过滤错误,而非向量数学错误。

切换建议:在 Azure AI Search 处理生产流量期间,将 Redis 设为只读状态一周,并对比黄金问题集上前3名的重叠情况。如果重叠度下降,应在指责嵌入模型之前先检查过滤条件及字段名称。此类迁移中出现的问题大多属于过滤错误,而非向量数学错误。

切换建议:在 Azure AI Search 处理生产流量期间,将 Redis 设为只读状态一周,并对比黄金问题集上前3名的重叠情况。如果重叠度下降,应在指责嵌入模型之前先检查过滤条件及字段名称。此类迁移中出现的问题大多属于过滤错误,而非向量数学错误。

切换建议:在 Azure AI Search 处理生产流量期间,将 Redis 设为只读状态一周,并对比黄金问题集上前3名的重叠情况。如果重叠度下降,应在指责嵌入模型之前先检查过滤条件及字段名称。此类迁移中出现的问题大多属于过滤错误,而非向量数学错误。

切换建议:在 Azure AI Search 处理生产流量期间,将 Redis 设为只读状态一周,并对比黄金问题集上前3名的重叠情况。如果重叠度下降,应在指责嵌入模型之前先检查过滤条件及字段名称。此类迁移中出现的问题大多属于过滤错误,而非向量数学错误。

切换建议:在 Azure AI Search 处理生产流量期间,将 Redis 设为只读状态一周,并对比黄金问题集上前3名的重叠情况。如果重叠度下降,应在指责嵌入模型之前先检查过滤条件及字段名称。此类迁移中出现的问题大多属于过滤错误,而非向量数学错误。

切换建议:在 Azure AI Search 处理生产流量期间,将 Redis 设为只读状态一周,并对比黄金问题集上前3名的重叠情况。如果重叠度下降,应在指责嵌入模型之前先检查过滤条件及字段名称。此类迁移中出现的问题大多属于过滤错误,而非向量数学错误。

切换建议:在 Azure AI Search 处理生产流量期间,将 Redis 设为只读状态一周,并对比黄金问题集上前3名的重叠情况。如果重叠度下降,应在指责嵌入模型之前先检查过滤条件及字段名称。此类迁移中出现的问题大多属于过滤错误,而非向量数学错误。

模式规范非常重要:在创建 Azure AI Search 索引时需要遵循,而在升级嵌入模型时同样不可或缺。维度不匹配会引发明显错误;可过滤字段的不匹配则表现为空结果集,不易被察觉。应添加启动时检查逻辑,读取索引定义,并将向量维度与会话字段名称与当前配置进行比对。