大数跨境
0
0

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

开发实战|为图数据库实现向量检索(下) NebulaGraph
2025-11-14
0
导读:如何支持 ANN Index 和 ANN Search?

导读


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

我们特别邀请刘芷溢同学将自己的项目经验整理成三篇开发实战,希望给大家的GraphRAG 相关工作一些启发。本篇,主要介绍如何在 NebulaGraph 中支持 ANN Index 和 ANN Search.
开发实战|为图数据库实现向量检索(上)开发实战|为图数据库实现向量检索(中),我们已经实现了 Vector 类型的存储以及对 DDL 和 DML 的适配。
现在我们需要实现 ANN Index 构建和 ANN Search,这里分三个步骤来实现:
  • 构建 ANN Index Adapter,将 HNSWlib 和 faiss 封装成统一的接口
  • 实现 ANN Index 的 DDL,支持创建和删除 ANN Index
  • 利用 ANN Index 进行 ANN Search
这里面也有一些关键问题需要解决:
  • ANN Index 的生命周期管理谁来负责?
  • ANN Index 的数据存储在哪里?
  • ANN Search 生成的计划如何使用 Ann Index 进行搜索?
我们会在下面的章节中结合三个步骤逐一进行说明,并且分享我们在实现过程中的一些经验与教训。

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


ANN Search Interface

(一)ANN Index Lifecycle
向量索引的生命周期由存储服务器管理。执行创建索引命令后,向量索引将被插入;执行删除索引命令后,向量索引将被删除。在其他情况下,存储服务器将继续维护此索引。

在 ANN Index DDL 章节中会详细介绍创建和删除索引的实现。实际上是通过 VectorIndexManager 来管理向量索引的生命周期,这个单例会维护一个内存中的向量索引映射表,Key 是 GraphSpaceID + PartID + IndexID,Value 是具体的向量索引实例。


这个 VectorIndexManager 在存储守护进程启动时初始化(加载持久化的向量索引),在进程退出前持久化所有的向量索引到磁盘。

(二)ANN Index Persistence
我们这里暂时没有考虑到分布式一致性和宕机重启情况,所以只是简单的调用 faiss 和 hnswlib 的序列化 ann index 接口,以二进制形式写入本地文件。

实际上这些 ann search 库持久化底层使用的也是 C++ IOStream 序列化机制。

(三)Memory Tracked
我的实现暂时是使用 NebulaGraph 内置的 MemoryTracker 定期查询内存索引的大小。如果超过限制,则无法插入新的 Vector.
(四)ANN Index Interface
为了支持不同的向量索引库,我们需要定义一个统一的向量索引接口 AnnIndex,并且实现不同的向量索引适配器。这个接口主要包含以下方法:
class AnnIndex { public:  AnnIndex() = default;
  AnnIndex(GraphSpaceID graphID,           PartitionID partitionID,           IndexID indexID,           const std::string &indexName,           bool propFromNode,           size_t dim,           const std::string &rootPath,           MetricType metricType,           size_t minTrainDataSize = 3);
  virtual ~AnnIndex() = default;  AnnIndex(const AnnIndex &) = delete;  AnnIndex &operator=(const AnnIndex &) = delete;
  [[nodiscard]] virtual Status init(const BuildParams *params) 0;  // add data to index incrementally  [[nodiscard]] virtual Status add(const VecData *data) 0;  // upsert data to index  [[nodiscard]] virtual Status upsert(const VecData *data) 0;  // soft delete data from index, return number of deleted vectors  [[nodiscard]] virtual StatusOr<size_tremove(const IDSelector &sel) 0;
  // ann search  [[nodiscard]] virtual Status search(const SearchParams *params, SearchResult *res) 0;  // reconstruct vector by id  [[nodiscard]] virtual StatusOr<Vector> reconstruct(VectorID id) 0;
  // load index file from disk  // flush index to disk  [[nodiscard]] virtual Status read(const std::string &file) 0;  [[nodiscard]] virtual Status write(const std::string &dir, const std::string &file) 0;  virtual AnnIndexType indexType() const 0;  virtual std::string toString() const 0;};
(五)Concurrent ANN Index
通过实现 AnnIndex 接口,我们构建了两个向量索引:一个底层使用 Faiss IVF 索引,另一个底层使用 HNSWlib HNSW 索引。
为了实现并发,我们在向量索引中使用了读写锁。这允许多个查询执行 ANN Search,但只允许执行一个查询执行对索引的 DML 操作(添加或删除)。
(六)ANN Index Utils
  • 为了简化向量索引的使用,我们定义了一些辅助数据结构和枚举类型:MetricType
  • MetricType:表示向量距离度量类型,如 L2 距离和内积。
  • AnnIndexType:表示向量索引类型,如 IVF 和 HNSW。
  • IDSelector:用于选择要删除的向量 ID 列表。
  • VecData:表示向量数据,包括向量数量、维度、数据和 ID。
  • BuildParams:表示向量索引构建参数的基类,以及其派生类 BuildParamsIVF 和 BuildParamsHNSW。
  • SearchParams:表示向量搜索参数的基类,以及其派生类 SearchParamsIVF 和 SearchParamsHNSW。
  • SearchResult:表示向量搜索结果,包括向量 ID、距离和向量数据。
enum MetricType : int8_t { L2, INNER_PRODUCT };enum AnnIndexType : int8_t { IVF, HNSW };
// faiss usedstruct IDSelector {  size_t cnt;  VectorID* ids;  // vector of IDs to select};
struct VecData {  size_t cnt;     // number of vectors  size_t dim;     // dimension of each vector  float* fdata;   // float type vector data source  VectorID* ids;  // int64 identifier of each vector};
struct OwnedVecData {  std::vector<float> flat;  std::vector<VectorID> ids;  VecData view;};
// ANN index build parametersstruct BuildParams {  MetricType metricType{MetricType::L2};  AnnIndexType indexType{AnnIndexType::IVF};};
struct BuildParamsIVF final : public BuildParams {  size_t nl{3};  // number of lists  size_t ts{3};  // train size};
struct BuildParamsHNSW final : public BuildParams {  size_t maxDegree{16};      // the maximum degrees  size_t efConstruction{8};  // expansion in construction time  size_t capacity{10000};    // capacity of the index};
struct SearchParams {  size_t topK{10};        // number of nearest neighbors to search  float* query{nullptr};  // query vector data  size_t queryDim{0};     // dimension of query vector};
struct SearchParamsIVF final : public SearchParams {  size_t nprobe{10};  // number of lists to probe};
struct SearchParamsHNSW final : public SearchParams {  size_t efSearch{16};  // expansion factor at search time};
// ANN search resultstruct SearchResult {  std::vector<VectorID> IDs;  // distances of the result vectors  std::vector<float> distances;  // result vectors  std::vector<float> vectors;};


ANN Search DDL

ANN Index DDL 功能的主要设计目标是支持对多个共享同名属性的标签进行索引。这需要新的 DDL 语法、元数据结构以及 Graphd 、 Metad 和 Storaged 之间协调的执行流程。
(一)Create ANN Index Syntax
  • Tag Ann Index Creation Syntax
CREATE TAG ANNINDEX <index_name> ON <tag_name_list>::(<field_name>) [IF NOT EXISTS] ann_index_params}[COMMENT '<comment>']
  • ANN Index Parameters
  1. ANNINDEX_TYPE: Index type, support IVF and HNSW
  2. DIM: Vector dimension
  3. METRIC_TYPE: Metric type, support L2 and INNER_PRODUCT
  4. NLIST: Number of lists, only for IVF index
  5. TRAINSIZE: Training size, only for IVF index
  6. MAXDEGREE: Maximum degree, only for HNSW index
  7. EFCONSTRUCTION: Expansion factor at construction time,only for HNSW index
  8. MAXELEMENTS: Capacity of the index, only for HNSW index
{ANNINDEX_TYPE: "IVF", DIM:128, METRIC_TYPE:"L2", NLIST:3, TRAINSIZE:3}{ANNINDEX_TYPE: "HNSW", DIM:128, METRIC_TYPE:"L2", MAXDEGREE:15, EFCONSTRUCTION:200, MAXELEMENTS:10000}
(二)Thrift Structure
1. Thrift ANN Index Item
通用索引项定义了单个模式中多个字段的索引,其索引参数仅包含 s2_max_level 和 s2_max_cells ,这无法满足 ANN 索引对跨多个模式的同名属性创建索引的要求。因此,我们设计了一个新的 ANN 索引项。
struct IndexItem {    1: common.IndexID       index_id,    2: binary               index_name,    3: common.SchemaID      schema_id    4: binary               schema_name,    5: list<ColumnDef>      fields,    6: optional binary      comment,    7: optional IndexParams index_params,}
ANN 索引项定义了在多个模式中同名属性之间创建索引的过程,包括所有需要建立索引的模式的信息。同时,它还使用一个列表来存储创建 ANN 索引所需的参数。
ann index params:[IVF/*ann type*/, 128 /*dim*/, L2/*metric type*/, 3/*nlist*/, 3/*train size*/][HNSW/*ann type*/, 128 /*dim*/, L2/*metric type*/, 16/*max degree*/, 200/*ef construction*/, 100000/*max elements*/]
ann index params:[IVF/*ann type*/, 128 /*dim*/, L2/*metric type*/, 3/*nlist*/, 3/*train size*/][HNSW/*ann type*/, 128 /*dim*/, L2/*metric type*/, 16/*max degree*/, 200/*ef construction*/, 100000/*max elements*/]
2. Thrift Request & Response Structure
DDL 在 Graphd 接收到创建 ANN Index 请求后,会将请求转换为 Thrift 请求发送到 Metad. 我们定义了以下 Thrift 结构:
struct CreateTagAnnIndexReq {    1: common.GraphSpaceID      space_id,    2: binary                   index_name,    3: list<binary>             tag_names,    4: IndexFieldDef            field,    5bool                     if_not_exists,    6: optional binary          comment,    7: optional list<binary>    ann_params,}
Storaged 会定期向 Metad 请求 ANN Index 元数据,并根据元数据创建向量索引实例。所以我们需要新的 request 将所有的 ANN Index 元数据发送到 Storaged:
struct ListTagAnnIndexesResp {    1: common.ErrorCode         code,    2: common.HostAddr          leader,    3: list<AnnIndexItem> items,}struct ListEdgeAnnIndexesResp {    1: common.ErrorCode         code,    2: common.HostAddr          leader,    3: list<AnnIndexItem>     items,}
(三)Core Component
我们的设想是在 Storaged 中维护我们的向量索引实例,为了与其他功能模块解耦,我们设计了一个核心组件 VectorIndexManager 来管理所有的向量索引实例。
  • 在存储守护进程中,设计一个 VectorIndexManager 单例来管理 Storaged 中的所有 ANN Index.
  • ANN Index 的生命周期:
  1. 创建 :通过 CreateTagAnnIndex 请求创建 Ann 索引。> 除非删除,否则它将始终保存在内存中。退出时需要将所有 Ann Index 持久化到磁盘,并在系统重启后从磁盘重新加载。
  2. 访问 :通过 GetTagAnnIndex 请求访问 Ann 索引用于加速 Ann Search。
  3. 删除 :通过 DropTagAnnIndex 请求删除 Ann Index。
  4. 更新 :通过 UpdateTagAnnIndex 请求更新 Ann Index。
实际上,VectorIndexManager 维护了一个内存中的向量索引映射表,Key 是 GraphSpaceID + PartID + IndexID,Value 是具体的 ANN Index 的智能指针。
class VectorIndexManager final { public:  static VectorIndexManager& getInstance();  Status init(meta::IndexManager* indexManager, std::string annIndexPath);  Status start();  Status stop();
  // Create & Rebuild  Status createOrUpdateIndex(GraphSpaceID spaceId,                             PartitionID partitionId,                             IndexID indexId,                             const std::shared_ptr<meta::cpp2::AnnIndexItem>& indexItem);  Status rebuildIndex(GraphSpaceID spaceId,                      PartitionID partitionId,                      IndexID indexId,                      const std::shared_ptr<meta::cpp2::AnnIndexItem>& indexItem);
  // Access & Search  StatusOr<std::shared_ptr<AnnIndex>> getIndex(GraphSpaceID spaceId,                                               PartitionID partitionId,                                               IndexID indexId);  StatusOr<SearchResult> searchVectors(GraphSpaceID spaceId,                                       PartitionID partitionId,                                       IndexID indexId,                                       const SearchParams& searchParams);
  // Update & Delete  Status addVectors(GraphSpaceID spaceId,                    PartitionID partitionId,                    IndexID indexId,                    const VecData& vecData);  Status removeIndex(GraphSpaceID spaceId, PartitionID partitionId, IndexID indexId);
  // Utility  std::vector<std::shared_ptr<AnnIndex>> getIndexesByPartition(GraphSpaceID spaceId,                                                               PartitionID partitionId);  bool hasIndex(GraphSpaceID spaceId, PartitionID partitionId, IndexID indexId) const;
  // ... private members for lifecycle management};
(四)Create Index Execution Flow
创建过程是一个多步骤操作,涉及 Graphd 、 Metad 和 Storaged .
1. Graphd -> Metad -> Storaged
  • Graphd: 解析 CREATE ANNINDEX DDL 并生成生成计划 Start->CreateTagAnnIndex->SubmitJob.
  • Metad: 执行 CreateTagAnnIndex 步骤。Metad 会在内部创建 AnnIndexItem 元数据条目。创建成功后,提交一个 AdminJob.
  • Metad: 该 AdminJob 将 ANN Index 参数打包成 AdeminTask 发送给 Storaged。Storaged 通过其内部的 AdminTaskManager 处理这些任务以完成作业。这涉及到一个分布式时序问题:
Meta Client 会因缓存更新看不到新加入的索引。因此,我们在 Storaged 中使用了重试机制。如果多次重试失败,我们会直接从元服务器请求数据以强制刷新缓存。

Meta Client 缓存更新机制: 存储节点的 IndexManager 通过 MetaClient 获取索引信息,但 MetaClient 的缓存通过心跳周期进行更新:


1.时序问题: 元服务创建索引后,存储节点需要等到下一个心跳周期才能看到新索引。

2.同步问题: getTagAnnIndex 直接从缓存读取数据。如果缓存尚未更新,则会返回 IndexNotFound 错误。

2. Storaged ANN Index Creation
存储节点在接收到创建 ANN Index 的 AdminTask 后,会生成真正的向量索引实例并将其存储在 VectorIndexManager 中。
对于每个分区,Storaged 会执行以下步骤:
  • 扫描 KVStore(RocksDB)中与属性名称匹配的所有 Vector 属性数据。
  • 将这些向量数据批量添加到新创建的向量索引实例中。
  • 将顶点 ID/边类型和向量 ID 之间的映射关系存储到 KVStore 的 id-vid 列族中。

顶点 ID 的类型为 std::string。这里 VectorID 是通过哈希计算得到的,因此需要存储两个映射:VectorID->VertexID 和 VertexID->VectorID.


ANN Search 

为了在 Graphd 中支持 ANN Search,我们必须修改查询执行流程。当查询的 Tag/Edge 上存在 ANN Index 时,执行计划将优先扫描 ANN Index 以获取 limit * K 条数据,然后再进行过滤。
这需要对优化规则、Graphd 执行计划以及 Storaged 中的执行计划进行修改,最主要的是在 Storaged 中添加一个新的 AnnIndexScan 节点。
(一)ANN Search Syntax
为了轻松区分 ANN Search 和原始的 MATCH 语句,我们设计了新的 APPROXIMATE LIMIT 语法。这使我们无需在 yacc 层面做过多改动。
语法示例:
MATCH (v:v1)RETURN v, euclidean(vector(0.90, 0.85, 0.88), v.embedding) AS distanceORDER BY euclidean(vector(0.90, 0.85, 0.88), v.embedding)APPROXIMATE LIMIT 1OPTIONS {ANNINDEX_TYPE:'IVF', METRIC_TYPE:L2, NPROBE:2};
(二)Overview of ANN Search Execution Flow
  1. Graphd (优化):优化规则识别出 APPROXIMATE LIMIT 语法,并生成一个包含 TagAnnIndexCcan 节点的物理执行计划。
  2. Storaged (索引扫描):Storaged 收到 TagAnnIndexScan 请求。它会生成一个内部计划,在底层的 ANN Index(例如 IVF)上执行搜索。
  3. Storaged (返回 VIDs):索引扫描返回匹配的 VectorID。Storaged 随后将 VectorID 映射回 vid,并将 vid 返回给 Graphd.
  4. Graphd (获取属性):Graphd 调用 Storaged 的 GetProp 操作符,为上一步返回的 vid 批量获取其余的非向量属性。
  5. Graphd (收尾):Graphd 执行最终的 Limit 和 Project 操作,整理数据后返回给客户端。
(三)Graphd Details
1. ANN Search Optimization Rule
新的优化规则用于识别 ANN Search 模式并将其转换为 TagAnnIndexScan 计划。规则需要覆盖以下两种情况:
(1)情况 1:不返回向量距离
MATCH (v:tag5:tag6)RETURN vORDER BY euclidean(vector(1.02.03.0), v.vec)APPROXIMATE LIMIT 3OPTIONS {ANNINDEX_TYPE:'IVF', METRIC_TYPE:L2, NPROBE:3}
(2)情况 2:返回向量距离——ApproximateLimit->Sort->Project->ScanVertices 转换为 Limit->TagAnnIndexScan
MATCH (v:tag5:tag6)RETURN v, euclidean(vector(1.02.03.0), v.vec)ORDER BY euclidean(vector(1.02.03.0), v.vec)APPROXIMATE LIMIT 1OPTIONS {ANNINDEX_TYPE:'IVF', METRIC_TYPE:L2, NPROBE:3}
2. Attribute Expression Modification
为了支持在多个标签上对同名向量属性进行 ANN Search,我们必须修改 AttributeExpression 的处理方式。
MATCH (v:coach_vector:player_vector)RETURN v, euclidean(vector(0.90, 0.85, 0.88), v.embedding) AS distanceORDER BY euclidean(vector(0.90, 0.85, 0.88), v.embedding)APPROXIMATE LIMIT 1OPTIONS {ANNINDEX_TYPE:'IVF', METRIC_TYPE:L2, NPROBE:2};
  • 原有限制:原始表达式强制要求使用 v.tag.prop 的形式来获取顶点属性,这样的话 v.embedding 会被识别成对 Tag 的获取直接返回 NULL 给用户。
  • 修改后:对于向量属性,我们放宽了这一限制。系统现在支持 v.embedding 这种形式来获取 Vertex 的 Vector 属性。
(四)Storaged Details
Graphd 的执行器会通过 RPC 调用 Storaged 上的服务。Storaged 内部会启动自己的执行器并生成 Storaged 计划。而我们主要需要修改的是 Storaged 计划中的 AnnIndexScan 节点和 GetProp 节点。
1. AnnIndexScan Node
AnnIndexScan 的内部流程与 IndexScan 非常相似:
  • 初始化: 执行 doExecute,它会调用 resetIter 来准备数据。
  • 获取 VectorID: resetIter 负责调用 Ann Index(例如 IVF)的搜索接口。这一步返回的结果仅仅是 VectorID(ANN 索引内部的 ID),而不是实际的 vid.
  • 迭代与映射: doNext 负责遍历 resetIter 获取的 VectorID 结果。
  • 获取 VID: 在 doNext 内部,调用 AnnIndexVertexScan::getVidByVectorid() 函数。此函数通过获取 RocksDB 的 id-vid 列族数据,得到 VectorID 对应的实际 vid.
  • 返回数据: Storaged 将 vid 返回给 Graphd.
2. GetProp Node
为了配合 Multi-Tag ANN Search,Storaged 中 AppendVerticesExecutor 执行计划里的 GetProp 节点也需要修改。
我们设计了一个新函数 collectAllVertexProps,用于收集单个顶点的所有非向量属性和向量属性。
  • 目的:当我们对 tag5 和 tag6 进行 ANN Search 时,一个顶点可能只匹配了 tag5(即它只有 tag5),但没有 tag6。
  • 行为:collectAllVertexProps 函数能正确处理这种情况,将未匹配标签的属性设置为 EMPTY(空),确保返回给 Graphd 的数据结构始终一致。
GetProp 返回的属性示例:
 _vid|tag5.id|tag5.vec|tag5._tag|tag6.num|tag6.vec|tag6._tag "v7"|7|vector(1.9,2.0,2.1)|2|__EMPTY__|__EMPTY__|__EMPTY__ "v8"|8|vector(2.2,2.3,2.4)|2|__EMPTY__|__EMPTY__|__EMPTY__ "v6"|6|vector(1.6,1.7,1.8)|2|__EMPTY__|__EMPTY__|__EMPTY__

踩过的坑 

实现这个功能过程中,一般我们遇到的问题都是设计不够周全导致的。这里总结几个比较典型的:
1. ANN Index 元数据设计不合理
  • 问题:最初我们尝试使用通用索引项 IndexItem 来表示 ANN Index 元数据,但它无法支持在多个标签上对同名属性创建索引的需求。
  • 解决方案:我们设计了新的 AnnIndexItem 结构,能够包含多个 Tag 的信息,并且使用列表来存储创建 ANN Index 所需的参数。
2. ANN Index 创建的时序问题
  • 问题:实现过程中并未考虑 Metad 和 Storaged 的时序问题,在测试时发现无法获取到 ANN Index
  • 原理:在 Metad 创建 ANN Index 后,Storaged 可能无法立即看到新创建的索引,因为 Meta Client 的缓存需要通过心跳周期更新。
  • 解决方案:我们在 Storaged 中实现了重试机制。如果多次重试失败,我们会直接从元服务器请求数据以强制刷新缓存。
3. ANN Search 优化规则不完善
  • 问题:最初的优化规则只覆盖了返回向量距离的情况,忽略了不返回向量距离的场景。
  • 解决方案:我们扩展了优化规则,确保它能够识别两种情况,并正确生成包含 TagAnnIndexScan 节点的执行计划。
4. Multi-Tag ANN Search 属性获取问题
  • 问题:在 Multi-Tag ANN Search 场景下,v.embedding 这种形式无法正确获取向量属性,导致返回结果总是为空。
  • 原因:原始的 AttributeExpression 实现强制要求使用 v.tag.prop 的形式来获取顶点属性。
  • 解决方案:我们修改了 AttributeExpression 的处理逻辑,允许对向量属性使用 v.embedding 这种形式进行访问。同时为了简化逻辑,我们设计了 collectAllVertexProps 函数,确保即使某些标签未匹配,其属性也会被设置为 EMPTY,从而保持数据结构的一致性。

总结

本文详细介绍了在 NebulaGraph 中实现向量索引和相似度搜索的完整过程,这是本系列的下篇。在前两篇文章中,我们已经完成了 Vector 类型的存储支持以及 DDL/DML 的适配,而本篇则聚焦于 ANN Index 的构建和 ANN Search 的实现。

实在是没有时间,分布式下的功能还没有完成,希望以后有机会再来填坑。

(一)核心成果
通过本次开发,我们实现了以下核心功能:
  1. 统一的 ANN Index 接口: 设计了 AnnIndex 抽象接口,成功封装了 HNSWlib 和 Faiss 两种主流向量索引库,为后续扩展更多索引类型奠定了基础。
  2. 完整的索引生命周期管理: 通过 VectorIndexManager 单例实现了向量索引的创建、访问、更新和删除的完整生命周期管理,包括索引的持久化和重启后的自动加载。
  3. Multi-Tag ANN Index 支持: 突破了传统索引只能作用于单个 Schema 的限制,实现了跨多个 Tag 对同名向量属性建立索引的能力,这是本次实现的一大亮点。
  4. 高效的 ANN Search 执行流程: 设计了从 Graphd 到 Storaged 的完整执行链路,包括新的优化规则、TagAnnIndexScan 执行节点,以及 VectorID 到 VertexID 的映射机制。
(二)回顾关键问题
在文章开头,我们提出了三个关键问题。现在让我们来回顾一下这些问题的答案:
问题 1:ANN Index 的生命周期管理谁来负责?
答案:由 Storaged 通过 VectorIndexManager 单例来管理。
这个单例维护了一个内存中的向量索引映射表(Key 为 GraphSpaceID + PartID + IndexID,Value 为具体的向量索引实例)。VectorIndexManager 在存储守护进程启动时初始化并加载持久化的索引,在进程退出前将所有索引持久化到磁盘。这种设计将索引实例维护在存储层,既保证了数据局部性,又简化了分布式环境下的一致性问题。
问题 2:ANN Index 的数据存储在哪里?
答案:ANN Index 以两种形式存储:
  • 内存存储向量索引实例(IVF、HNSW)常驻内存,通过 VectorIndexManager 管理,以支持高性能的向量搜索。
  • 磁盘持久化:调用 Faiss 和 HNSWlib 的序列化接口,将索引以二进制形式写入本地文件系统。这些库底层使用 C++ IOStream 序列化机制。
  • 映射关系存储:VectorID 与 VertexID/EdgeType 之间的映射关系存储在 RocksDB 的 id-vid 列族中,确保能够将向量搜索结果转换回实际的顶点或边。
问题 3:ANN Search 生成的计划如何使用 ANN Index 进行搜索?
答案:通过专门的优化规则和执行节点实现:
  • Graphd 优化阶段:新的优化规则识别 APPROXIMATE LIMIT 语法,将 ApproximateLimit->Sort->Project->ScanVertices 转换为 Limit->TagAnnIndexScan 执行计划。
  • Storaged 执行阶段:AnnIndexScan 节点通过 VectorIndexManager 获取对应的向量索引实例,调用其 search 接口返回 VectorID 列表。
  • ID 映射转换:通过查询 RocksDB 的 id-vid 列族,将 VectorID 转换为实际的 VertexID。
  • 属性补全:Graphd 使用返回的 VertexID 列表调用 GetProp 获取完整的顶点属性,通过 collectAllVertexProps 函数处理 Multi-Tag 场景。
(三)关键技术决策
在实现过程中,我们做出了几个重要的技术决策:
  • 索引生命周期由 Storaged 管理: 将向量索引实例维护在存储层,既能保证数据局部性,又能简化分布式环境下的一致性问题。
  • 新的元数据结构 AnnIndexItem: 摆脱了通用 IndexItem 的限制,专门为 ANN  Index 设计的元数据结构,能够更好地支持 Multi-Tag 场景。
  • 重试机制解决时序问题: 通过在 Storaged 中引入重试机制,解决了 Meta Client 缓存更新延迟带来的时序问题。
(四)经验与教训
回顾整个开发过程,我们总结出以下几点经验:
  1. 充分考虑分布式系统的时序问题: 在分布式系统中,不同组件之间的状态同步是异步的。我们在 ANN Index 创建流程中遇到的 Meta Client 缓存更新问题就是一个典型案例。设计时应该充分考虑这类时序问题,并设计相应的重试或强制刷新机制。
  2. 接口设计要考虑可扩展性: AnnIndex 接口的设计使得我们能够轻松支持多种向量索引库。统一的接口不仅简化了上层调用逻辑,也为后续引入新的索引类型(如 DiskANN、ScaNN 等)预留了空间。
  3. 优化规则要覆盖所有场景: 在实现 ANN Search 优化规则时,我们最初只考虑了返回距离的场景,导致不返回距离的查询无法被优化。这提醒我们在设计优化规则时,需要全面梳理所有可能的查询模式。
  4. 测试先行: 在开发过程中,充分的测试帮助我们发现了许多设计上的疏漏,如 Multi-Tag 场景下的属性获取问题。完善的测试用例不仅能帮助发现 bug,还能驱动我们思考更多的边界情况。
(五)展望
当前的实现已经基本满足了向量搜索的核心需求,但仍有一些可以优化和扩展的方向:
  1. 索引更新策略优化: 当前的索引更新是实时的,对于大规模数据插入场景,可以考虑引入批量更新或异步更新机制。
  2. 更多索引类型支持: 可以引入更多类型的向量索引,如基于磁盘的 DiskANN,以支持超大规模向量数据。
  3. 混合查询优化: 探索向量搜索与图遍历的更深度融合,如在图遍历过程中动态执行向量过滤。
  4. 分布式索引: 当前索引是按分区独立管理的,未来可以考虑实现跨分区的分布式索引,提升大规模场景下的查询性能。
通过这个项目,我深刻体会到在复杂的分布式系统中添加新功能的挑战性。它不仅需要对系统架构有深入的理解,还需要细致地考虑各种边界情况和性能优化。希望这个系列的分享能给正在开发类似功能的同学一些参考和启发。
最后,感谢 NebulaGraph 社区和开源之夏项目组提供的学习和实践机会!感谢我的项目曹志鹏导师的指导和 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 的疑问/开发经验。


🔥日本 nMeetUp 火热报名中

🔥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 一个开源的分布式图数据库
总阅读597
粉丝0
内容731