使用redisvl报错Property "`vector_distance`" not loaded nor in schema

概述

在RAG应用中使用 redis 作为向量数据库。

当使用results = vector_store.similarity_search_with_score(query="测试", k=10, score_threshold=0.1)查询相似的文本内容时报错Property "vector_distance" not loaded nor in schema,本文记录一下排查该问题的过程,希望对遇到同样问题的小伙伴提供一些帮助。

复现代码

redisvl使用的schema如下,关于schema的定义说明查看文档 Schema in RedisVL

python 复制代码
CHUNK_INDEX_NAME = "{kb_id}:doc_chunk"
chunk_index_schema = {
    "index": {
        "name": f"{CHUNK_INDEX_NAME}",
        "prefix": f"{CHUNK_INDEX_NAME}",
        "storage_type": "hash"  # 索引存储类型,可选值:hash、json
    },
    "fields": [
        {"name": "doc_id", "type": "tag"},
        {"name": "chunk_id", "type": "tag"},
        {"name": "kb_id", "type": "tag"},
        {"name": "page", "type": "numeric", "attrs": {"sortable": True}},

        {"name": "content", "type": "text"},
        {
            "name": "content_vector",
            "type": "vector",
            "attrs": {
                "dims": 1024,  # 向量维度, 在创建索引时必填
                "algorithm": "flat",  # 向量检索算法, 在创建索引时必填,实际在使用时如果没有设置会默认为flat,可填写:"flat"(精确搜索)"hnsw"(近似搜索)
                "distance_metric": "cosine",
                "datatype": "float32"
            }
        }
    ]
}

使用的代码如下,这里为了方便使用自定义一些方法,继承了RedisVectorStore

下面的代码已经修改过了,如果要复现该错误,需要将 schema 中 vector 字段对应的 name 和下面类中的 embedding_field的值修改为其他名称。

python 复制代码
from typing import Dict, Any, List

from langchain_core.documents import Document
from langchain_core.embeddings import Embeddings
from langchain_redis import RedisVectorStore, RedisConfig
from redis.commands.search.query import Query
from redisvl.query import FilterQuery
from redisvl.query.filter import Tag
from redisvl.schema import IndexSchema

from logging import getLogger

logger = getLogger(__name__)


def _update_dimensions(embeddings: Embeddings, index_schema: Dict[str, Any]):
    """
    Update the dimensions of the vector index schema.
    :param embeddings:
    :param index_schema:
    :return:
    """
    embedding_dimensions = len(embeddings.embed_query('get vector dimensions'))
    for field in index_schema.get('fields'):
        if field['type'] == 'vector':
            if 'attrs' in field:
                field['attrs'].update({'dims': embedding_dimensions})
            else:
                field['attrs'] = {'dims': embedding_dimensions, 'algorithm': 'flat'}


class RedisSearch(RedisVectorStore):

    def __init__(self, embeddings: Embeddings, index_schema: Dict[str, Any]):
        # update the dimensions of the index schema
        _update_dimensions(embeddings, index_schema)

        schema = IndexSchema.from_dict(index_schema)

        # 获取redis客户端
        config = RedisConfig.from_schema(
            schema,
            redis_client=redis_client,
            content_field="content",
            embedding_field="content_vector"
        )
        super().__init__(embeddings=embeddings, config=config)

    @classmethod
    def from_kb(cls, embeddings: Embeddings, index_schema: Dict[str, Any], kb_id: str) -> 'RedisSearch':
        index_schema['index']['name'] = index_schema['index']['name'].format(kb_id=kb_id)
        index_schema['index']['prefix'] = index_schema['index']['prefix'].format(kb_id=kb_id)

        return cls(embeddings=embeddings, index_schema=index_schema)

    def get_by_doc_id(self,
                      doc_id: str,
                      with_metadata: bool = True,
                      offset: int = None,
                      num: int = None) -> list[Document]:
        """
        根据doc_id查询文档
        Args:
            doc_id: 文档id
            with_metadata: 是否返回元数据
            offset: 分页偏移量
            num: 分页大小

        Returns:
            List[Document]: 文档列表
        """
        return_fields = [self.config.content_field]
        if with_metadata:
            return_fields = self.config.index_schema.field_names

        query = Tag("doc_id") == doc_id
        query_string = str(query)

        return self.search_by_query(offset=offset, num=num, query_string=query_string, return_fields=return_fields)

    def get_by_kb_id(self, kb_id: str, with_metadata: bool = True, offset: int = 0, num: int = 10) -> List[Document]:

        return_fields = [self.config.content_field]
        if with_metadata:
            return_fields = self.config.index_schema.field_names

        query = Tag("kb_id") == kb_id
        query_string = str(query)

        return self.search_by_query(offset=offset, num=num, query_string=query_string, return_fields=return_fields)

    def get_by_tag_name(self, tag_name: str, tag_value: Any, with_metadata: bool = True) -> List[Document]:

        return_fields = [self.config.content_field]
        if with_metadata:
            return_fields = self.config.index_schema.field_names

        query = Tag(tag_name) == tag_value
        query_string = str(query)

        query = (
            FilterQuery(query_string)
            .return_fields(*return_fields)
            .in_order()
            .paging(offset=0, num=10000)  # 默认num为10000,表示查询所有文档
            .dialect(2)
        )
        logger.info(f'redis查询语句: {query.get_args()}')
        results = self.index.query(query)
        # Prepare document results
        docs = []
        for doc in results:
            docs.append(
                Document(
                    id=doc.get(self.config.id_field),
                    page_content=doc[self.config.content_field],
                    metadata={
                        k: v
                        for k, v in doc.items()
                        if k != self.config.content_field and k != "embedding"
                    },
                )
            )
        return docs

    def delete_by_tag_name(self, tag_name: str, tag_value: Any):
        """
        根据tag删除文档
        :param tag_name:
        :param tag_value:
        :return:
        """

        query_results = self.get_by_tag_name(tag_name, tag_value, with_metadata=False)
        ids = [doc.id for doc in query_results]
        if not ids:
            logger.info(f"未查询到{tag_name}={tag_value}对应的数据,无需删除")
            return
        result = self.index.drop_keys(ids)
        logger.info(f"删除{tag_name}={tag_value}对应的数据,数量:{result}")
        if result != len(ids):
            # 删除数据失败
            raise Exception(f"删除{tag_name}={tag_value}对应的数据失败,数量:{result},实际删除数量:{len(ids)}")

    def delete_by_doc_id(self, doc_id: str):
        """
        根据doc_id删除文档
        :param doc_id:
        :return:
        """
        self.delete_by_tag_name("doc_id", doc_id)

    def search_by_query(self, query_string: str, return_fields: List[str], offset: int = None, num: int = None):
        if num is None or offset is None:
            query = (
                FilterQuery(query_string)
                .return_fields(*return_fields)
                .paging(offset=0, num=10000)  # 默认num为10000,表示查询所有文档
                .in_order()
                .dialect(2)
            )
        else:
            query = (
                FilterQuery(query_string)
                .return_fields(*return_fields)
                .paging(offset=offset, num=num)
                .in_order()
                .dialect(2)
            )

        results = self.index.query(query)
        # Prepare document results
        docs = []
        for result in results:
            content_key = self.config.content_field
            page_content = result.pop(content_key)
            docs.append(
                Document(id=result.get('id'), page_content=page_content, metadata=result)
            )
        return docs

    def count_by_doc_id(self, doc_id: str):
        """
        根据doc_id查询数量
        :param doc_id:
        :return:
        """
        query = Tag("doc_id") == doc_id

        query = (
            Query(str(query))
            .paging(offset=0, num=0)  # limit 0 0 表示统计数量
            .dialect(2)
        )
        print(query.get_args())
        results = self.config.redis().ft(self.config.index_name).search(query)
        return results.__dict__

报错信息

运行代码

python 复制代码
if __name__ == '__main__':
    embedding_name = EmbeddingType.M3E_BASE.value
    embeddings = init_embedding(embedding_name)

    vector_store = RedisSearch.from_kb(embeddings=embeddings, index_schema=chunk_index_schema, kb_id="LAW_LIBRARY")

    result = vector_store.similarity_search_with_score(query="测试", k=10, score_threshold=0.1)
    print(result)

报错信息如下Propertyvector_distance not loaded nor in schema

python 复制代码
    raise RedisSearchError(f"Error while searching: {str(e)}") from e
redisvl.exceptions.RedisSearchError: Error while searching: Property `vector_distance` not loaded nor in schema

报错原因分析

最开始使用的是 langchain_community 封装的 Redis 来操作向量数据库,但是从 langchain-community==0.3.13开始,使用from langchain_community.vectorstores.redis import Redis被标记为过时,推荐使用langchain_redis.RedisVectorStore

  1. langchain_community 封装的 Redislangchain_redis的主要区别是对redis操作使用的库不同:
    • langchain_community 基本是自己封装了操redis的各种工具(代码基本和redisvl一致)。
    • langchain_redis则是直接复用 redisvl来操作redis。
  2. langchain_community 封装的 Redis默认使用的向量字段名不同:
    • langchain_community 封装的 Redis默认使用
      • content:chunk的内容字段名
      • content_vector:chunk的向量字段名
    • langchain_redis默认使用的向量字段:
      • text:chunk的内容字段名
      • embedding:chunk的向量字段名

当从langchain_community 封装的 Redis切换到langchain_redis后,由于都是使用的默认字段,原来的向量字段是content_vector,而langchain_redis的向量字段是embedding,所以在查询的时候不一致,无法计算vector_distance,最终报错Property vector_distance not loaded nor in schema

debug排查

发现报错后,首先通过debug看看报错时参入的参数是什么,如下图所示:

从报错的地方可以看到请求的参数和报错的详细内容,请求参数使用 KNN 计算向量的距离,并将计算的结果保存到 vector_distance字段。从这里可以基本猜到是计算向量字段embedding的相似度错误导致的问题。

问大模型(推荐)

通过debug排查后,一个很自然的想法就是问一下 deepseek,再了解一些排查思路,

输入给大模型的内容如下:

plain 复制代码
在RAG应用中使用 redis 作为向量数据库,当使用results = vector_store.similarity_search_with_score(query="测试", k=10, score_threshold=0.1)查询相似的文本内容时报错Property "`vector_distance`" not loaded nor in schema

模型回答如下:

在 Redis 作为向量数据库的 RAG 应用中遇到 Property "vector_distance" not loaded nor in schema 错误时,通常是因为查询时未正确加载或定义 vector_distance 字段。以下是分步解决方案:


确认 Redis 索引 Schema 定义

确保在创建 Redis 索引时,明确包含向量距离字段 vector_distance 的定义。例如:

python 复制代码
from redis.commands.search.field import VectorField, TextField

schema = (
    TextField("content"),  # 文本字段
    VectorField("embedding",  # 向量字段名
        "HNSW",  # 索引类型 (HNSW 或 FLAT)
        {
            "TYPE": "FLOAT32",
            "DIM": 1536,  # 向量维度
            "DISTANCE_METRIC": "COSINE"  # 或 "L2"
        }
    )
)

确保查询时返回 vector_distance

在查询时,需显式要求返回 vector_distance 字段。例如使用 redis-pyFT.SEARCH 命令:

python 复制代码
from redis.commands.search.query import Query

query = (
    Query("*=>[KNN 10 @embedding $query_vector AS vector_distance]")
     .return_fields("content", "vector_distance")
     .dialect(2)
)
result = redis_client.ft(index_name).search(query, {"query_vector": query_embedding})

检查 LangChain/第三方库的配置

若使用 LangChain 的 Redis 集成,检查 similarity_search_with_score 是否支持 score_threshold 参数。部分版本可能需要手动配置返回字段:

python 复制代码
from langchain_community.vectorstores.redis import Redis

vector_store = Redis.from_existing_index(
    embedding=embedding_model,
    index_name="my_index",
    schema=schema  # 确保传入包含 vector_distance 的 schema
)

results = vector_store.similarity_search_with_score(
    query="测试",
    k=10,
    score_threshold=0.1,
    return_fields=["content", "vector_distance"]  # 显式指定返回字段
)

更新 Redis 和客户端库

确保使用的 Redis 版本 >= 7.2.0(支持向量搜索),并更新 Python 客户端库:

bash 复制代码
pip install --upgrade redis redis-py

验证索引状态

检查索引是否已正确创建并包含向量字段:

python 复制代码
print(redis_client.ft(index_name).info())
# 输出应包含向量字段定义

手动计算距离(备选方案)

如果问题仍未解决,可手动计算相似度得分:

python 复制代码
from sklearn.metrics.pairwise import cosine_similarity

query_embedding = ...  # 获取查询向量
stored_embeddings = ...  # 从 Redis 获取存储的向量

scores = cosine_similarity([query_embedding], stored_embeddings)[0]
results = sorted(zip(documents, scores), key=lambda x: x[1], reverse=True)

总结

此错误的核心是 Redis 索引未返回 vector_distance 字段。需确保:

  1. 索引 Schema 正确包含向量字段和距离度量。
  2. 查询时显式请求 vector_distance
  3. 使用的库版本和参数配置正确。

若问题持续,检查库的文档或提交 Issue 以获取版本兼容性支持。

报错原因总结

从debug和大模型的回答分析,可以初步判断可能是索引 Schema 未正确包含向量字段和距离度量,使用下面的代码查看 schema 的内容:

python 复制代码
print(redis_client.ft(index_name).info())
# 输出应包含向量字段定义

排查到这里,我们基本可以得出前面的结论,由于两次查询使用的shema不一致导致的问题。

解决方法

排查到了原因是:由于两次查询使用的shema不一致导致的问题,所以解决该问题也就比较简单了。

解决方法:

  1. 删除原来的索引

可以使用下面的方法删除索引,delete_documents参数用来控制是否删除该索引关联的key,如果为True则会删除该索引关联的所有内容。

python 复制代码
from redisvl.index import SearchIndex

# initialize the index object with schema from file
index = SearchIndex.from_yaml(
    "schemas/schema.yaml",
    redis_url="redis://localhost:6379",
    validate_on_load=True
)

# delete index and data
index.delete(drop=True)
  1. 重新创建索引
python 复制代码
from redisvl.index import SearchIndex

# initialize the index object with schema from file
index = SearchIndex.from_yaml(
    "schemas/schema.yaml",
    redis_url="redis://localhost:6379",
    validate_on_load=True
)

# create the index
index.create(overwrite=True, drop=False)

# data is an iterable of dictionaries
index.load(data)

参考文档

github.com/langchain-a...

docs.redisvl.com/en/stable/a...

docs.redisvl.com/_/downloads...

相关推荐
南雨北斗17 分钟前
8.安装laravel12和编程学习的几点思考
后端
异常君1 小时前
Java 9 特性详解:从模块系统到 API 增强的全面剖析
java·后端
程序猿chen1 小时前
《JVM考古现场(十八):造化玉碟·用字节码重写因果律的九种方法》
java·jvm·git·后端·面试·java-ee·跳槽
南雨北斗1 小时前
7.安装Laravel 12 PHP需要开启的扩展
后端
异常君1 小时前
【深度解析】Spring/Boot 核心陷阱:事务、AOP 与 Bean 生命周期的常见问题与应对策略
java·后端
福大大架构师每日一题1 小时前
2025-04-13:范围内整数的最大得分。用go语言,给定一个整数数组 start 和一个整数 d,这代表了 n 个区间 [start[i], start[i
后端
一个热爱生活的普通人1 小时前
浅谈池化思想:以 database/sql 连接池为例
后端·go
kunge20131 小时前
1.MCP入门-大模型函数调用的概念
后端
流秧1 小时前
三种方式来实现多线程连续打印abc
后端
南雨北斗1 小时前
4.composer国内镜像源推荐
后端