> 适用版本：v0.2.2

## 1. 引言

`vector.Index` 组件是 Zenfeed 系统中负责实现内容语义相似度检索的核心模块，与 `block.Block` 一一对应。它的主要目标是根据用户提供的查询向量，快速找到与之在语义上最相关的 Feed（通常是新闻资讯、文章等文本内容）。

该索引的核心设计理念是服务于**文档级别的召回 (Document-level Recall)**。与许多传统向量索引将每个文本块（chunk）视为独立节点不同，`vector.Index` 将**整个 Feed 文档作为图中的一个节点**。而 Feed 内容经过 `embedding_spliter` 切分后产生的多个文本块（chunks），它们各自的向量嵌入（embeddings）则作为该 Feed 节点的属性。

这种设计的独特性在于：

*   **搜索结果直接是 Feed ID**：用户搜索后直接获得相关 Feed 的标识符，而不是零散的文本片段。
*   **相似度聚焦于“任何部分相关即相关”**：如果一个 Feed 的任何一个 chunk 与查询向量高度相似，整个 Feed 就被认为是相关的。其最终得分为该 Feed 所有 chunks 与查询向量相似度中的最大值。
*   **为新闻资讯场景优化**：这种设计特别适合新闻资讯类应用，优先保证相关内容的召回率，确保用户不会错过重要信息，即使该信息仅是文章的一部分。

`vector.Index` 底层采用 HNSW (Hierarchical Navigable Small World) 算法来组织和搜索这些 Feed 节点，以实现高效的近似最近邻查找。

## 2. 核心概念

理解 `vector.Index` 的运作方式，需要熟悉以下核心概念：

*   **Feed (Node)**:
    *   在 `vector.Index` 的 HNSW 图中，每个**节点 (node)** 代表一个独立的 **Feed 文档** (例如一篇新闻报道)。
    *   每个 Feed 通过一个唯一的 `uint64` ID 来标识。
    *   节点存储了其对应的原始 Feed ID 以及与该 Feed 相关的多个向量。

*   **Chunk (Vector Represented by `[][]float32`)**:
    *   一个 Feed 的内容（尤其是其文本标签，如标题、正文）可能较长。如果直接将整个长文本生成单一的 embedding，可能会遇到以下问题：
        *   **LLM 输入长度限制**: 许多 embedding 模型对输入文本的长度有限制。
        *   **语义稀释 (Semantic Dilution)**: 对于包含多个主题或信息点的长文本，单一向量可能难以精确捕捉所有细微的语义，导致关键信息在整体平均化的向量表示中被“稀释”，降低了特定语义片段的表征能力。例如，一篇包含多个不同事件的综合报道，其单一向量可能无法很好地代表其中任何一个特定事件。
    *   通过 `embeddingSpliter`，一个 Feed 的文本内容可以被切分成一个或多个语义相对连贯的 **文本块 (Chunks)**。这种切分有助于每个 chunk 聚焦于更具体的主题或信息点。
    *   每个 Chunk 会被送入 LLM 生成一个 **向量嵌入 (vector embedding)**。
    *   因此，一个 Feed 节点在索引中会关联**一组向量 (vectors `[][]float32`)**，每个子向量代表其一个 Chunk 的语义。

*   **Embedding**:
    *   Embedding 是一个由浮点数组成的向量，由大语言模型 (LLM) 生成。它能够捕捉文本片段的语义信息，使得语义上相似的文本在向量空间中距离更近。
    *   `vector.Index` 存储和比较的就是这些 embeddings。

*   **HNSW (Hierarchical Navigable Small World)**:
    *   `vector.Index` 使用 HNSW 作为其底层的近似最近邻 (ANN) 搜索算法。
    *   HNSW 通过构建一个多层的图结构来实现高效搜索。上层图更稀疏，用于快速导航；下层图更密集，用于精确查找。
    *   这种结构使得索引在插入新节点和执行搜索时都能保持较好的性能。

*   **相似度计算 (Similarity Score)**:
    *   **Feed 间相似度 (Inter-Feed Similarity)**:
        *   当评估 HNSW 图中两个 Feed 节点（例如，`nodeA` 和 `nodeB`）之间的相似度时，策略是计算 `nodeA` 的所有 Chunk 向量与 `nodeB` 的所有 Chunk 向量之间的两两余弦相似度。
        *   最终，这两个 Feed 节点间的相似度取所有这些两两 Chunk 相似度中的**最大值 (Maximal Local Similarity)**。
        *   **选择此策略的原因**: 对于新闻资讯，只要两篇报道中存在任何一对高度相关的片段（例如，都报道了同一核心事件或引用了同一关键信息），就认为这两篇报道具有强关联性。这有助于最大化召回率，确保用户能发现所有可能相关的资讯，即使它们整体侧重点不同。
        *   **潜在影响**: 这种策略对局部强相关非常敏感，但也可能因为次要内容的偶然相似而将整体主题差异较大的 Feed 判定为相关，需要在上层应用或通过重排序模型来进一步优化精度。
    *   **查询与 Feed 相似度 (Query-Feed Similarity)**:
        *   当用户使用一个查询向量 `q` 进行搜索时，计算 `q` 与目标 Feed 的每一个 Chunk 向量的余弦相似度。
        *   该 Feed 最终与查询 `q` 的相似度分数，同样取这些计算结果中的**最大值**。
        *   这样做是为了确保只要 Feed 的任何一部分内容与用户查询高度匹配，该 Feed 就会被召回。

## 3. 主要接口

`vector.Index` 提供了一组清晰的接口，用于管理和查询基于 Feed 内容语义的向量索引。

*   **`Add(ctx context.Context, id uint64, vectors [][]float32) error`**
    *   **业务目标**: 将一个新的 Feed 文档及其所有内容块（Chunks）的向量表示添加到索引中，使其能够被后续的相似度搜索发现。
    *   **核心流程**:
        1.  **接收 Feed 数据**: 接收 Feed 的唯一 `id` 和代表其所有 Chunks 的 `vectors` 列表。
        2.  **确定插入策略**: 根据 HNSW 算法的层级构建原则，为该 Feed 节点随机确定一个在多层图结构中的最高插入层级。
        3.  **查找邻近节点**: 从选定的最高层级开始逐层向下，在每一层利用该层的图结构（和 `EfConstruct` 参数指导下的搜索范围）为新 Feed 节点找到一组最相似的已有 Feed 节点（邻居）。此处的“相似”基于我们定义的“最大局部相似性”——即比较两个 Feed 所有 Chunk 向量对，取其中相似度最高的一对作为这两个 Feed 的相似度。
        4.  **建立连接**: 如果新 Feed 节点被分配到当前层级，则将其与找到的邻居建立双向连接（朋友关系），并更新其在该层级的友邻列表。
        5.  **维护图结构**: 在添加连接后，可能会触发友邻剪枝逻辑，以确保每个节点的友邻数量符合配置（`M` 或 `2*M`），并尝试维护图的良好连接性，避免产生孤立节点或过度密集的区域。

*   **`Search(ctx context.Context, q []float32, threshold float32, limit int) (map[uint64]float32, error)`**
    *   **业务目标**: 根据用户提供的查询向量 `q`，从索引中高效地检索出语义上最相似的 Feed 列表，并返回它们的 ID 及相似度得分。
    *   **核心流程**:
        1.  **接收查询**: 接收查询向量 `q`、相似度阈值 `threshold` 和期望返回的最大结果数 `limit`。
        2.  **导航至目标区域**: 从 HNSW 图的顶层开始，利用稀疏的高层图结构快速定位到与查询向量 `q` 大致相关的区域，逐层向下，每层都找到与 `q` 更近的节点作为下一层的入口。
        3.  **在底层精确搜索**: 到达最底层的图（第 0 层，包含所有 Feed 节点）后，以上一步得到的入口点为起点，进行一次更细致的扩展搜索（受 `EfSearch` 参数指导的搜索范围）。此搜索旨在找到与查询向量 `q` 的“最大局部相似性”（即 `q` 与 Feed 的所有 Chunk 向量相似度中的最大值）满足 `threshold` 且排名前 `limit` 的 Feed。
        4.  **返回结果**: 将符合条件的 Feed ID 及其对应的最高相似度分数打包返回。

*   **`EncodeTo(ctx context.Context, w io.Writer) error` / `DecodeFrom(ctx context.Context, r io.Reader) error`**
    *   **业务目标**: 提供索引的持久化能力，允许将内存中的索引状态完整地保存到外部存储（如文件），并在需要时恢复。
    *   **核心流程 (`EncodeTo`)**:
        1.  **写入元数据**: 保存索引的配置参数（如 `M`, `Ml`, `EfConstruct`, `EfSearch`）和版本信息。
        2.  **写入节点数据**: 遍历所有 Feed 节点，依次保存每个节点的 ID、其所有 Chunk 向量（经过量化处理以压缩体积）、以及它在 HNSW 各层级上的友邻关系（友邻 ID 和相似度）。
        3.  **写入层级结构**: 保存每个层级所包含的节点 ID 列表。
    *   **核心流程 (`DecodeFrom`)**:
        1.  **读取元数据**: 恢复索引配置。
        2.  **重建节点数据**: 读取并重建所有 Feed 节点，包括其 ID、反量化后的 Chunk 向量、以及友邻关系。
        3.  **重建层级结构**: 恢复 HNSW 的多层图。

## 4. 内部实现细节补充

### 4.1 核心数据表示

*   **Feed 节点 (`node`)**: 每个 Feed 在内存中表示为一个 `node` 对象，它不仅存储了 Feed 的 ID 和其所有 Chunk 的向量 (`vectors [][]float32`)，还关键地维护了它在 HNSW 图各个层级上的“友邻列表” (`friendsOnLayers`)。这个友邻列表是图连接性的基础。
*   **分层图 (`layers`)**: 索引内部维护一个 `layers` 列表，代表 HNSW 的多层结构。高层图节点更少、连接更稀疏，用于快速跳转；底层图（尤其是第0层）节点最多、连接最密集，用于精确搜索。
*   **全局节点池 (`m`)**: 一个从 Feed ID 到 `node` 对象的映射，方便快速访问任何已索引的 Feed。

### 4.2 索引构建的关键机制

*   **概率性分层 (`randomInsertLevel`)**: 新加入的 Feed 节点会被随机分配到一个最高层级。这种概率机制（受 `Ml` 参数影响）形成了 HNSW 的金字塔式层级结构。
*   **动态邻居选择 (`insertAndLinkAtLevel` 中的搜索逻辑)**: 当一个新 Feed 节点加入某一层时，它会基于“最大局部相似性”在该层搜索一定数量（受 `EfConstruct` 影响）的最近邻居。
*   **连接维护与剪枝 (`makeFriend`, `tryRemoveFriend`)**: 与邻居建立双向连接后，为保证图的性能和结构（避免节点拥有过多邻居），会有一套剪枝逻辑。这套逻辑不仅考虑移除相似度最低的连接，有时还会考虑被移除连接的另一端节点的连接状况，试图避免制造“孤岛”节点，甚至在必要时（通过 `tryRemakeFriend`）为连接数过少的节点尝试从“邻居的邻居”中寻找新的连接机会。

### 4.3 存储效率：向量量化

*   为了显著减少索引在持久化存储时占用的空间，`float32` 类型的向量在写入磁盘前会通过 `vectorutil.Quantize` 被转换为 `int8` 类型，并记录下转换所需的最小值和缩放比例。读取时再通过 `vectorutil.Dequantize` 进行有损恢复。这是在存储成本和表示精度之间的一种实用权衡。
