大数跨境
0
0

开发实战|为图数据库实现向量检索(中)

开发实战|为图数据库实现向量检索(中) NebulaGraph
2025-11-07
0
导读:如何支持向量属性的 DDL 和 DML 语句?

导读


刘芷溢同学,在 OSPP 2025 中,为 NebulaGraph 实现了向量数据持久化与 ANN Search(向量近似近邻检索),以赋能 AI 场景。该功能使 NebulaGraph 进一步具备知识图谱和向量检索相结合的能力,高效地用 GraphRAG 来改善传统 RAG 的痛点。

我们特别邀请刘芷溢同学将自己的项目经验整理成三篇开发实战,希望给大家的GraphRAG 相关工作一些启发。本篇,主要介绍如何在 NebulaGraph 中支持向量类型的 DDL&DML 语句。

开发实战|为图数据库实现向量检索(上)我们已经对 NebulaGraph 的整体执行流程有了认识,并概述了如何实现 Vector 数据类型以及 Vector 存储。

现在我们需要将 Vector 类型集成到 Nebula Graph 中,支持用户通过 DDL 和 DML 语句来创建和操作 Vector 类型的属性。我们需要解决几个关键问题:

  • 如何在已有的 Tag/Edge Schema 中添加 Vector 类型的属性?
  • 如何在 Insert/Update 语句中插入和更新 Vector 类型的属性值?
  • 如何保证 Storage 层获取正确的 Schema 信息,并正确存储和读取 Vector 类型的属性值?

⚠️文末附总结答案~

接下来我们一步一步来解决这些问题,我们将从 DDL 适配开始讲起,然后是 DML 适配,最后总结我们中间走了的弯路和经验教训。

 ⚠️这里为了简化,我们假设使用 Tag Schema 进行说明


DDL适配

 在 NebulaGraph 中,Tag/Edge 的 Schema 定义存储在 Metad 节点中,用户通过 DDL 语句(如 CREATE TAG/ALTER TAG)来定义和修改 Schema.

实际上 Storaged 节点在执行 DML 语句时会从 Metad 获取最新的 Schema 信息。通过自己的 cache 定期更新。

(一)DDL Syntax

我们需要在 DDL 语句中支持定义 Vector 类型的属性。

  • 定义 vector 属性时,需要指定向量的维度,例如 vector(128) 表示一个 128 维的向量。
  • 对于 vector 具体的数值常量,我们为了避免 parser 的二义性,单独设计了语法规则 vector(1.0,2.0,3.0) 来表示一个具体的向量值。
CREATE TAG IF NOT EXISTS test1(name string, embedding vector(3) DEFAULT vector(1.0,2.0,3.0)) TTL_DURATION = 100, TTL_COL = "create_time";;

(二)Schema Support

1. Schema Storage

Meta 服务使用 RocksDB 存储,将 schema 对应的 KV 对存入其中

  • Key 结构: SpaceId + TagId + Version
  • Value 是 Thrift 序列化的 Schema 定义,我们复用了原有的 Schema 结构,只是扩展了 ColumnTypeDef 来支持 Vector 类型

这里我们复用了 ColumnTypeDef 中的 type_length 字段来表示 Vector 的维度

struct ColumnTypeDef {  1required common.PropertyType    type, // add vector type  // type_length is valid for fixed_string type and vector type for dimension  2optional i16                    type_length = 0,  // geo_shape is valid for geography type  3optional GeoShape               geo_shape,}struct ColumnDef {    1required binary          name,    2required ColumnTypeDef   type,    3optional binary          default_value,    4optional bool            nullable = false,    5optional binary          comment,}struct SchemaProp {    1optional i64      ttl_duration,    2optional binary   ttl_col,    3optional binary   comment,}struct Schema {    1: list<ColumnDef> columns,    2SchemaProp schema_prop,}

2. Schema Provider

SchemaProvider 是内存中的 schema 表示,提供字段查询、偏移计算等功能,是对数据编解码的核心依赖。

  • 实际上,Schema 在 graphd 中通过 SchemaProvider 管理 field
class NebulaSchemaProvider { private:  SchemaVer ver_;  // Schema 版本号  // 字段映射: 字段名 -> 字段索引  std::unordered_map<std::string, int64_t> fieldNameIndex_;  // 字段数组,按顺序存储所有字段  std::vector<SchemaField> fields_;  // 可空字段数量  size_t numNullableFields_;  // Vector 字段相关  std::unordered_map<std::string, int64_t> vectorFieldNameIndex_;  std::vector<SchemaField> vector_fields_;  size_t numVectorNullableFields_;  // Schema 属性 (TTL等)  cpp2::SchemaProp schemaProp_;};
  • field 由 SchemaField 表示
class SchemaField {  std::string name_;           // 字段名  PropertyType type_;          // 数据类型  bool nullable_;              // 是否可空  bool hasDefault_;            // 是否有默认值  std::string defaultValue_;   // 默认值  size_t size_;                // 固定大小(字节)  size_t offset_;              // 在行中的偏移量  size_t nullFlagPos_;         // NULL 标志位位置  cpp2::GeoShape geoShape_;    // 地理类型形状};

为了保证兼容性,需要对 thrift 文件最小化修改,我们采用了原始列和向量列逻辑上分离的设计:

  • 原始列(除 VECTOR 类型外的其他类型)
  • 向量列(仅适用于 VECTOR 类型)
  • 架构属性选项(TTL、TTL_COL 等)

(三)Why Design Like This?

  • 可以兼容已有的 Schema 设计,最小化对现有代码的改动
  • 后续我们依据 Schema 进行数据读取和写入时,可以根据属性类型区分原始列和向量列,逻辑更清晰
  • 对后续向量索引的创建和 ann search,可以更方便地定位向量属性


DML 适配

(一)DML Syntax

1. Insert Vertex Sentence For Vector Type

INSERT VERTEX tag1(id, vec1, vec2) VALUES 'v5':(5, vector (0.40.50.6), vector (0.10.20.30.4)), 'v6':(6, vector (0.70.80.9), vector (0.20.40.60.8));

2. Delete Vertex Sentence For Vector Type

Delete VERTEX on tag1 "v5" [with edge]

3. Update Vertex Sentence For Vector Type

UPDATE VERTEX on tag1 'v5', 'v6' SET vec1 = vector (0.10.20.3), vec2 = vector (0.40.50.60.7);

(二)DML Processing

我们先从整个流程上看我们整个 Insert Vertex 语句的处理过程:

  • 客户端 INSERT 语句传入 Graph 层
  • Graph 层进行语法解析、语义校验、执行计划生成并执行 InsertVerticesExecutor,该处理器会将数据打包成 AddVerticesRequest 请求发送到 Storage 层
  • Storage 层接收到请求后,执行 AddVerticesProcessor
  • 获取 Schema 信息
  • 分离属性:Regular vs Vector
  • 对两种属性分别编码:
    • Regular 属性使用 RowWriterV2 的常规模式编码
    • Vector 属性使用 RowWriterV2 的 Vector 模式编码
  • 生成不同的 Key-Value 对:
    • Regular: tagKey → regularData
    • Vector: vectorTagKey → vectorData
  • 批量写入 RocksDB

1. Get Schema From Meta Service

传入的 env 中的 SchemaManager 维护 schema 缓存,该缓存会定期从 Meta 服务拉取最新的 schema 信息,保证 Storage 层获取到最新的 schema。

⚠️HOW IT WORKS?

  • Storaged 拥有 meta 客户端,当 meta service 准备就绪时,storaged 将从 metad 获取序列化后的 schema 数据

  • 取到 schema 后,storage service 中的 meta client 会解析传来的数据并重组成 NebulaSchemaProvider 供 storage service 后续使用。

    2. Encode Regular & Vector Properties Separately

    我们需要对 Regular 属性和 Vector 属性分别编码,实际上这是由 RowWriterV2 来处理的

    • Regular 属性编码时,RowWriterV2 以常规模式工作,直接获取所有 Regular 属性并编码到同一个 rowstr 中。
    • Vector 属性编码时,RowWriterV2 以 Vector 模式工作,每个 Vector 属性单独编码成一个 rowstr,通过传入的 index 来获取不同的 Vector 属性。
    2.1 REGULAR ENCODING CODE
    • 分区布局:以 person(name:string, age:int, city:string) 为例
    +--------+--------+-----------+--------------+---------------+-----------+| Header | Ver    | NullFlags | Fixed Data   | Variable Data | Timestamp |+--------+--------+-----------+--------------+---------------+-----------+| 1 byte | 0-7 B  | 0-N bytes | size  bytes  | 变长          | 8 bytes   |+--------+--------+-----------+--------------+---------------+-----------+
    Fixed Data 详细布局:+----------------+----------------+----------------+| name (8 bytes) | age (8 bytes)  | city (8 bytes) || [offset, len]  | [int64_t]      | [offset, len]  |+----------------+----------------+----------------+  ↓                                  ↓  指向 Variable Data                 指向 Variable Data
    Variable Data:+-------+-------+| Alice | Tokyo |+-------+-------+
    • Regular 属性的编码我们沿用之前的逻辑不变,所有 Regular 属性会被编码到同一个 rowstr 中
    • 具体编码调用如下:
    encodeRowVal() -> RowWriterV2::setValue() -> RowWriterV2::write()
    // BaseProcessor-inl.h - encodeRowVal()StatusOr<std::string> encodeRowVal(    const meta::NebulaSchemaProvider* schema,    const std::vector<std::string>& propNames,    const std::vector<Value>& props,    WriteResult& wRet) {
      // 创建常规 RowWriterV2 (isVectorColumns=false)  RowWriterV2 rowWrite(schema);
      // 逐个设置字段值  for (size_t i = 0; i < propNames.size(); i++) {    wRet = rowWrite.setValue(propNames[i], props[i]);  }
      // 完成编码  wRet = rowWrite.finish();
      // 返回编码后的字符串  return std::move(rowWrite).moveEncodedStr();}
    • RowWriterV2::write() 对固定长度属性和变长属性分别处理,最终生成 rowstr

    • 固定长度属性:直接在预分配的缓冲区写入并通过 schema 的 offset() 直接计算位置

    • 变长属性:
      • 间接存储:固定区域只存 8 字节的 [offset, length]
      • 后追加:所有变长数据按顺序追加到末尾
    // 写入 INT64 示例WriteResult RowWriterV2::write(ssize_t index, int64_t v) {  auto field = schema_->field(index);  auto offset = headerLen_ +           // Header + Version                numNullBytes_ +        // NULL 标志                field->offset();       // 字段在固定区域的偏移  // 3. 原地写入(in-place)  memcpy(&buf_[offset], &v, sizeof(int64_t));  // 4. 清除 NULL 标志  if (field->nullable()) {    clearNullBit(field->nullFlagPos());  }  isSet_[index] = true;  return WriteResult::SUCCEEDED;}
    WriteResult RowWriterV2::write(ssize_t index, const std::string& v) {  auto field = schema_->field(index);  // 1. 计算固定区域的偏移(存储 offset + length)  auto fixedOffset = headerLen_ + numNullBytes_ + field->offset();  // 2. 字符串实际存储在缓冲区末尾  int32_t strOffset = buf_.size();  int32_t strLen = v.size();  // 3. 在固定区域写入"指针"  memcpy(&buf_[fixedOffset], &strOffset, sizeof(int32_t));  memcpy(&buf_[fixedOffset + 4], &strLen, sizeof(int32_t));  // 4. 追加实际数据  buf_.append(v.data(), v.size());  if (field->nullable()) {    clearNullBit(field->nullFlagPos());  }  isSet_[index] = true;  return WriteResult::SUCCEEDED;}
    2.2 VECTOR KEY DESIGN
    • 在 Type 中新增 VECTOR 类型,便于后面直接根据 rowstr 的第一字节判断写入 RocksDB 的哪个 CF。
    • 增加 PropId 来区分不同的 Vector 属性。
    2.3 VECTOR ENCODING CODE
    • 与变长属性处理类似,Vector 属性在 rowstr 中也采用间接存储的方式:

    • 固定区域存储 8 字节的 [offset, length]

    • 实际的向量数据追加到 rowstr 的末尾
    +--------+--------+-----------+--------------+---------------+-----------+| Header | Ver    | NullFlag  | Offset + Len | Vector Data   | Timestamp |+--------+--------+-----------+--------------+---------------+-----------+| 1 byte | 0-7 B  | 0-1 byte  | 8 bytes      | 变长(N*4 B)   | 8 bytes   |+--------+--------+-----------+--------------+---------------+-----------+
    示例:128 维向量+-----------+------------------+-----------------------------+| [offset]  | [length]         | [f1, f2, ..., f128]        || (4 bytes) | (4 bytes)        | (128 * 4 = 512 bytes)      |+-----------+------------------+-----------------------------+    ↓             ↓                      ↓  指向数据      数据长度            原始 float 数组
    • Vector 属性的编码我们新增了一套逻辑,每个 Vector 属性会被单独编码成一个 rowstr
    bool vectorPropNamesEmpty = vectorPropNames_.empty();
    // 遍历每个 Vector 属性for (size_t i = 0; i < vectorProps_.size(); ++i) {  // 获取 vector 字段索引  int64_t vectorFieldIndex = vectorPropNamesEmpty      ? static_cast<int64_t>(i)      : schema->getVectorFieldIndex(vectorPropNames_[i]);
      // 生成 Vector 专用 Key  auto vectorKey = NebulaKeyUtils::vectorTagKey(      spaceVidLen_, partId, vid, tagId,      static_cast<int32_t>(vectorFieldIndex));
      // 编码 Vector 值  auto vectorValue = encodeVectorRowVal(      schema, vectorProps_[i],      static_cast<size_t>(vectorFieldIndex), wRet);
      // 存储  data.emplace_back(std::move(vectorKey), std::move(vectorValue.value()));}

    3. Write to RocksDB

    完整的通过 raft-wal 写入 RocksDB 流程如下图:

    • 为了确保 DML 操作的一致性,每个请求仅使用一个 MergeableAtomicOp 异步操作

      一个 INSERT/UPDATE/DELETE 请求对应一个原子操作单元

    • 写入真正的执行在 Raft commitLog
    • 实际上真正写入的时候,我们通过 Key 的 type 字段区分 Regular 和 Vector 数据,写入不同的 Column Family

      以 batch 写入为例:

    case OP_BATCH_WRITE: {    auto data = decodeBatchValue(log);    for (auto& op : data) {        if (op.first == BatchLogType::OP_BATCH_PUT) {            if (NebulaKeyUtils::isVector(op.second.first.str())) {                // Vector 数据写入专用 Column Family                code = batch->put(                    NebulaKeyUtils::kVectorColumnFamilyName,                    op.second.first,                    op.second.second                );            } else {                code = batch->put(op.second.first, op.second.second);            }        }        // ... 其他操作类型    }    break;}

    Update Special Processing

    Update 语句在 Storage 层的处理,相比 Insert 语句有一些特殊之处:

    • Update 在 Storage 层生成的计划树包含 TagNode 节点,该节点负责读取当前顶点的属性值
    • 为了支持 Vector 属性,我们需要确保 TagNode 能正确读取多个 Vector 属性
    (1)完整流程
    ┌─────────────────────────────────────────────────────────────────┐│ 1. TagNode::doExecute(partId, vId)                            │├─────────────────────────────────────────────────────────────────┤│   • 读取常规 Tag 数据: tagKey -> value_                       ││   • 检查 Schema 是否包含 Vector 字段                          ││   • 遍历所有属性,筛选出 Vector 类型                           ││   • 为每个 Vector 属性:                                       ││     - 获取字段索引: getVectorFieldIndex(propName)             ││     - 构造 Vector Key: vectorTagKey(vId, tagId, index)        ││     - 读取 Vector 数据: kvstore_->get() -> vectorValues_      ││   • 初始化 RowReader:                                         ││     - reader_.reset(*schemas_, value_)                        ││     - vectorReaders_[i].reset(*schemas_, vectorValues_[i], true, index) │└─────────────────────────────────────────────────────────────────┘                              ↓┌─────────────────────────────────────────────────────────────────┐│ 2. FilterNode::doExecute()                                     │├─────────────────────────────────────────────────────────────────┤│   • 检查过滤条件                                              ││   • 传递 reader_ 和 vectorReaders_ 给下游                     │└─────────────────────────────────────────────────────────────────┘                              ↓┌─────────────────────────────────────────────────────────────────┐│ 3. UpdateTagNode::doExecute()                                  │├─────────────────────────────────────────────────────────────────┤│   • 获取上游的 reader_ 和 vectorReaders_                      ││   • collTagProp():                                             ││     - 从 reader_ 读取常规属性                                 ││     - 从 vectorReaders_[i] 读取 Vector 属性                   ││     - 所有属性统一存储到 props_                               ││   • updateAndWriteBack():                                      ││     - 计算更新表达式,更新 props_                              ││     - 区分写入:                                               ││       * 常规属性 -> rowWriter_->setValue()                    ││       * Vector 属性 -> vectorRowWriters_[index]->setValueVec()││     - 完成编码:                                               ││       * rowWriter_->finish() -> nVal                          ││       * vectorRowWriters_[i]->finishVector() -> vectorVals[i] ││     - 批量写入:                                               ││       * batchHolder->put(key_, nVal)                          ││       * batchHolder->put(vectorKeys_[i], vectorVals[i])       │└─────────────────────────────────────────────────────────────────┘                              ↓┌─────────────────────────────────────────────────────────────────┐│ 4. UpdateResNode::doExecute()                                  │├─────────────────────────────────────────────────────────────────┤│   • 根据 returnPropsExp_ 提取返回属性                         ││   • 构造 DataSet 返回给客户端                                 │└─────────────────────────────────────────────────────────────────┘
    (2)TAGNODE
    • 独立读取 Vector 数据到 vectorValues_
    • 我们实际上为了支持多 Vector 属性的读取,新增了 vectorReaders_ 成员变量来存储多个 Vector Reader

      每个 Vector 属性对应一个 ReaderVector 属性初始化需要传入 Vector 所在的索引

    // 一个主 Reader,N 个 Vector ReaderRowReaderWrapper reader_;                     // 读取常规属性std::vector<RowReaderWrapper> vectorReaders_; // 读取 Vector 属性
    // 初始化方式不同reader_.reset(*schemas_, value_);           // 常规模式vectorReaders_[i].reset(*schemas_, vecVal, true, idx); // Vector 模式
    (3)UPDATETAGNODE
    • 从多个 Vector Reader 收集所有属性
    • 常规属性从 reader_ 读取
    • Vector 属性从 vectorReaders_ 读取
    • 统一到 props_map 进行表达式计算
    • 使用多个 vectorRowWriters_ 编码回写
    // 1. 收集常规属性for (auto index = 0UL; index < schema_->getNumFields(); index++) {    auto propName = std::string(schema_->getFieldName(index));    auto retVal = QueryUtils::readValue(reader_, propName, schema_);    if (!retVal.ok()) {      return nebula::cpp2::ErrorCode::E_TAG_PROP_NOT_FOUND;    }    props_[propName] = std::move(retVal.value());  }
    // 2. 收集 Vector 属性for (auto index = 0UL; index < schema_->getVectorNumFields(); index++) {  auto vecPropName = std::string(schema_->getVectorFieldName(index));
      auto retVal = QueryUtils::readVectorValue(vectorReaders_[index], vecPropName, schema_);  if (!retVal.ok()) {    return nebula::cpp2::ErrorCode::E_TAG_PROP_NOT_FOUND;  }  props_[vecPropName] = std::move(retVal.value());}
    // 3. 初始化 Regular 属性的 RowWriterrowWriter_ = std::make_unique<RowWriterV2>(schema_);val_ = reader_->getData();
    // 4. 为每个 Vector 属性创建独立的 RowWriterfor (auto index = 0UL; index < schema_->getVectorNumFields(); index++) {  vectorVals_.emplace_back(vectorReaders_[index]->getData());  vectorRowWriters_.emplace_back(      std::make_unique<RowWriterV2>(schema_, truestatic_cast<int32_t>(index))  );}


    踩过的坑

    在实现过程中,我们也遇到了一些设计上的挑战和坑,下面列举几个典型的例子:

    1. Parser 对 Vector 语法的支持

    v1: 最初我尝试直接使用 [1.0, 2.0, 3.0] 来表示向量值,但发现这样会与list类型产生二义性,导致进行 tck 测试时对 list 类型的复杂查询出现解析错误。

    v2: 后来我尝试使用 (1.0;2.0;3.0) 的语法,但这种方式会与计算表达式中的括号冲突,导致解析器无法正确识别。

    v3: 后来经过和 mentor 的讨论,最终我们设计了 vector(1.0,2.0,3.0) 的语法,避免了上面的解析冲突。

    2. Schema 设计的兼容性

    v1: 最初我尝试直接在 ColumnTypeDef 中添加一个新的字段来表示 Vector 的维度,但这样会导致对现有代码的改动过大,影响了系统的稳定性。

    v2: 后来我尝试将 Vector 属性和 Regular 属性混合存储在同一个字段列表中,但这样会导致在数据读取和写入时难以区分不同类型的属性,增加了实现的复杂度。而且也会影响后面向量索引的创建和查询。

    v3: 最终我们采用了将 Vector 属性和 Regular 属性逻辑上分离的设计

    3. DML 处理中的多属性支持

    v1: 最初我的想法是将 Regular 和 Vector 属性写入分为两个原子操作,但是这样会破坏操作的原子性。

    v2: 后面我尝试在 RowWriter 和 RowReader 中对不同类型属性进行处理,但是对 RocksEngine,batch 等存储层接口改动过大,增加实现的复杂度。

    v3: 最终我们通过在 Key 中区分不同类型属性,并在写入时根据类型选择不同的 Column Family,既保证了操作的原子性,又简化了存储层的实现

    我们只需要在 RaftPart::commitLogs() 中根据 Key 的类型选择不同的 CF 即可。

    4. Update 语句中多 Vector 属性的读取和写入

    v1: 最初我的想法是一个 filterNode 可以有多个 TagNode 来分别读取不同的 Vector 属性,实际实现时发现这种方案实现复杂度过高,且不易维护。

    v2: 最终我们选择在一个 TagNode 中读取所有 Vector 属性,并在 UpdateTagNode 中统一处理所有属性的更新和写入,简化了实现。

    5. Schema 缓存的一致性问题

    在测试中,我们发现 vector 属性在 Storage 层进行操作时会崩溃,经过排查发现是因为 Storage 层的 Schema 缓存没有及时更新,导致读取到的 Schema 信息不一致。

    解决方案:我们确保 Storage 层的 Schema 缓存可以拉取到 vector 类型的属性,保证操作时使用的是最新的 Schema。


    总结

    现在我们来回答一下开篇提出的三个问题

    1. 如何在已有的 Tag/Edge Schema 中添加 Vector 类型的属性?

    • 在 DDL 层扩展类型定义,新增 VECTOR 类型并且在定义时指定维度(dimension),例如 vector(128)。
    • 在 schema 的序列化结构中复用现有 ColumnTypeDef 的 type_length 字段表示 Vector 的维度(最小化 Thrift 文件改动)。
    • 在 meta(Meta 服务)中保存 schema 的序列化表示(Key: SpaceId + TagId + Version,Value: Thrift 序列化的 Schema
    • Storage 从 Meta 拉取该序列化结果并构建内存 SchemaProvider
    • 在内存 SchemaProvider 中对 Vector 字段单独维护(与常规列逻辑上分离),比如额外的 vectorFieldNameIndex_vector_fields_numVectorNullableFields_ 等。

    2. 如何在 Insert/Update 语句中插入和更新 Vector 类型的属性值?

    • 在 Parser/语义层加入对 vector 常量的支持,采用专门语法 vector(0.1, 0.2, 0.3) 来表示向量常量,避免与 list 等冲突。
    • Graph 层在语义校验通过后生成请求(例如 AddVerticesRequest / Update 请求),将 vector 属性与 regular 属性区分并打包到请求中发送给 Storage 层。
    • Storage 层处理 DML 时,先从 SchemaProvider 获取 schema,然后“分离属性”:Regular vs Vector。
    • Regular 属性统一使用已有的 RowWriterV2 常规编码,一个 rowstr 包含所有 regular 字段。
    • 每个 Vector 属性单独使用 RowWriterV2 的 Vector 模式编码(每个 vector 属性生成独立的 rowstr)。
    • Key 设计:对 Vector 数据使用区分 type 的 Key(并带上 PropId 或 vector field index),以便写入不同 Column Family(CF)。
    • 写入:在组装写入 Batch(或 Raft 提交)时,根据 Key 判断写入 CF,保证 Regular 与 Vector 数据可以在同一原子请求中写入不同 CF,从而保持操作的整体原子性(通过 raft-wal + batch)。
    更新(UPDATE):
    • Update 的计划会读取当前 regular row(tagKey)以及所有 vector 行(vectorTagKey)——TagNode 负责读取常规和 vector 数据并构造主 reader_ 与多个 vectorReaders_
    • UpdateTagNode 把常规字段与多个 vector 字段合并到 props_map,计算表达式后分别用 rowWriter->setValue() 和 vectorRowWriters[index]->setValueVec() 编码,并一起 batch 写回(regular key + 所有 vector keys)。

    3. 如何保证 Storage 层获取正确的 Schema 信息,并正确存储和读取 Vector 类型的属性值?

    Schema 存储与传递:
    • Meta 服务以 RocksDB 存储 schema(Key: SpaceId + TagId + Version;Value: Thrift 序列化的 Schema)。
    • Storage 端有 meta client,定期或在事件触发时从 Meta 拉取序列化的 schema 数据,然后解析并重建为 NebulaSchemaProvider(内存表示)。
    SchemaProvider 设计:
    • 在内存 SchemaProvider 中区分 regular 字段数组与 vector 字段数组(fieldNameIndex_ vs vectorFieldNameIndex_)、提供方法 getFieldIndex/getVectorFieldIndexgetNumFields/getVectorNumFields 等。
    RowReader/RowWriter:
    • Regular 的读写沿用 RowReader/RowWriter 既有逻辑(单个 rowstr)。
    • Vector 的读写采用 RowWriterV2 的 Vector 模式:
      • 固定区域存放 [offset, length](8 bytes,或两个 4-byte),实际 float 数组追加在 rowstr 尾部(N * 4 bytes for float)。
    • 每个 vector 字段对应单独的 rowstr,因此读取时需要单独发起 kv get(使用 vectorKey)。
    TagNode:
    • 读取正常 tagKey 的值并初始化主 reader_
    • 如果 schema 有 vector 字段,逐个构造 vectorKey 并读取对应值,初始化对应的 vectorReaders_

    下期预告:分享如何在 NebulaGraph 中支介绍如何实现向量索引和向量搜索功能~

    🔗PR 链接

    https://github.com/vesoft-inc/nebula/pull/6083

    https://github.com/vesoft-inc/nebula/pull/6074

    https://github.com/vesoft-inc/nebula/pull/6099

    https://github.com/vesoft-inc/nebula/pull/6076

    https://github.com/vesoft-inc/nebula/pull/6087

    https://github.com/vesoft-inc/nebula/pull/6068

    https://github.com/vesoft-inc/nebula/pull/6104

    https://github.com/vesoft-inc/nebula/pull/6090


    ⚠️NebulaGraph 的向量相关功能,在 OSPP 2025 结束后,将统一收录于 nebula-contrib(收录了许多来自社区开发者贡献的生态工具)
    https://github.com/nebula-contrib/nebula-vsearch/


    💡本文首发于刘芷溢同学 Blog,点击「阅读原文」即可跳转。

    🥳欢迎大家在评论区交流关于向量和 GraphRAG 的疑问/开发经验。


    O2O 计划火热进行中,免费为你提供技术支持

    📧来论坛,GraphRAG 产品反馈一键直达 NebulaGraph 产品团队。

    https://discuss.nebula-graph.com.cn/t/topic/17256


    如果你觉得 NebulaGraph 能帮到你,或者你只是单纯支持开源精神,可以在 GitHub 上为 NebulaGraph 点个 Star!

    每一个 Star 都是对我们的支持和鼓励✨

    GitHub:https://github.com/vesoft-inc/nebula

    官网:https://www.nebula-graph.com.cn/

    论坛:https://discuss.nebula-graph.com.cn/



    扫码添加

     可爱星云

    技术交流

    资料分享


    NebulaGraph 用户案例

    Why Graph Database?⬇️

    复杂关系场景,图数据库为何是首选?

    风控场普适智能|中证数智BlockSec携程Airwallex众安保险中国移动Akulaku邦盛科技360数科BOSS直聘金蝶征信快手青藤云安全

    平台建设:博睿数据携程众安科技微信OPPOvivo美团百度爱番番携程金融普适智能BIGO

    知识图谱:普适智能|中证数智中医药大学企查查腾讯音乐中科大脑泰康在线苏宁微澜同花顺携程酒店

    数据血缘:波克城市微众银行携程金融

    智能运维BOSS直聘|58同城中亦安图

    供应链:京东物流震坤行

    营销推荐:阿里妈妈

    GraphRAG:中科数睿

    ✨ NebulaGraph 推荐阅读

    【声明】内容源于网络
    0
    0
    NebulaGraph
    一个开源的分布式图数据库
    内容 731
    粉丝 0
    NebulaGraph 一个开源的分布式图数据库
    总阅读521
    粉丝0
    内容731