存储策略:Valkey vs JSON vs NumPy

本文解释了为什么我们在 SEO 流水线搜索服务 中针对不同类型的数据使用不同的存储技术。

问题所在:一刀切行不通

不同的数据具有不同的访问模式:

  • 嵌入向量 (5K x 384 浮点数):需要快速的向量操作(余弦相似度)

  • 短语映射 (2500+ 短语):需要人工编辑、版本控制

  • 查询缓存 (实时查询):需要快速的键值查找、TTL 过期

  • 产品数据 (64K 产品):需要结构化访问、兼容性规则

对所有数据使用相同的存储方式效率低下。

三种存储技术

1. NumPy 数组:向量操作

使用场景:嵌入向量(产品、查询、短语)

为何选择 NumPy

  • 快速的向量运算:针对矩阵运算优化的 C/Fortran 库

  • 内存映射文件:无需复制到 RAM 即可加载大型数组

  • 批处理操作:在毫秒级内处理数千个向量

  • 标准格式:兼容 ML 库(scikit-learn, TensorFlow)

文件格式:二进制 .npy 文件

加载方式

import numpy as np

# 内存映射(不会将整个文件加载到 RAM)
embeddings = np.load('embeddings.npy', mmap_mode='r')

# 计算余弦相似度
from sklearn.metrics.pairwise import cosine_similarity
similarities = cosine_similarity(query_embedding, embeddings)

性能:以适合实时推理的速度进行大规模相似性搜索。

2. JSON 文件:可人工编辑的数据

使用场景:配置、映射、元数据

为何选择 JSON

  • 人类可读:易于检查和调试

  • 版本控制:Git diff 能精确显示变更内容

  • 手动编辑:无需代码即可修复错误

  • 通用格式:任何语言都能解析 JSON

我们存储的内容

  • 短语到过滤器的映射:短语 → 过滤规则

  • 产品特性:产品 → 特性字典

  • 查询元数据:查询 → 点击、展示、来源

  • 流水线配置:步骤参数、阈值

文件格式:文本 .json 文件

加载方式

import json

with open('phrase_mappings.json') as f:
    mappings = json.load(f)

# 访问数据
filters = mappings['mini pc']  # ['form_factor:mini', 'category:pc']

3. Valkey (Redis):快速的键值缓存

使用场景:实时搜索、自动补全、热门查询

为何选择 Valkey

  • 内存存储:查找延迟为微秒级

  • RediSearch:带索引的向量相似性搜索

  • TTL 过期:自动缓存失效

  • 发布/订阅:跨服务器的实时更新

  • 持久化:可选的磁盘快照以确保持久性

我们存储的内容

  • 查询嵌入向量缓存:最近查询 → 嵌入向量

  • 热门查询:按流量排名的前 1000 条查询

  • 自动补全索引:前缀 → 查询建议

  • 过滤器提取缓存:查询 → 提取出的过滤器

  • 相关搜索缓存:查询 → 相关查询

数据结构

  • 字符串:简单的键值对(查询 → 嵌入向量)

  • 有序集合:排序数据(按分数排名的热门查询)

  • RediSearch 索引:向量相似性搜索

  • 哈希:结构化数据(查询元数据)

加载方式

from app.shared.valkey_cache import get_valkey

valkey = get_valkey()
# ... (实现细节已省略)

决策矩阵

何时使用 NumPy

标准

  • 数据是数值型(浮点数、整数)

  • 需要快速的向量操作(点积、余弦相似度)

  • 数据以读取为主(很少更新)

  • 数据量大(数百万个数字)

  • 批处理(一次处理多个项目)

示例

  • 嵌入向量(产品、查询、短语)

  • 特征向量

  • 相似性矩阵

何时使用 JSON

标准

  • 数据是结构化的(对象、数组)

  • 需要人类可读性

  • 需要版本控制(Git)

  • 数据偶尔变更(手动编辑)

  • 数据量小到中等(<100 MB)

示例

  • 配置文件

  • 短语映射

  • 产品元数据

  • 流水线参数

何时使用 Valkey

标准

  • 需要快速查找(微秒级)

  • 数据频繁变更(实时查询)

  • 需要 TTL 过期(缓存失效)

  • 需要发布/订阅(实时更新)

  • 需要向量搜索(RediSearch)

示例

  • 查询缓存

  • 自动补全

  • 热门查询

  • 会话数据

  • 速率限制

混合方法

我们结合使用所有三种技术以获得最佳性能:

流水线(离线处理)

NumPy:计算嵌入向量、相似性矩阵

JSON:存储映射、元数据、配置

Valkey:不使用(流水线离线运行)

搜索服务(实时查询)

NumPy:从磁盘加载嵌入向量(内存映射)

JSON:从磁盘加载映射(缓存在内存中)

Valkey:缓存实时查询、自动补全、热门查询

数据流

graph LR
    Pipeline[SEO 流水线
离线] subgraph Storage NP[NumPy 文件
embeddings.npy] JS[JSON 文件
mappings.json] end Search[搜索服务
实时] VK[Valkey
缓存] Pipeline --> NP Pipeline --> JS NP --> Search JS --> Search Search --> VK VK --> Search

性能比较

NumPy(内存映射):

  • 加载时间:瞬时(零拷贝映射到虚拟内存)

  • 查找时间:接近零(CPU 直接指针指向数组索引)

  • 内存:最小(操作系统管理的页缓存,不驻留在进程 RAM 中)

JSON

  • 加载时间:高延迟(顺序解析和对象实例化)

  • 查找时间:高效(标准哈希映射开销)

  • 内存:占用大(整个序列化结构驻留在堆中)

Valkey

  • 加载时间:持久化(驻留在后台服务中)

  • 查找时间:中等(包括网络往返和协议序列化)

  • 内存:外部化(隔离在数据库进程内)

胜出者:NumPy 用于高吞吐量批处理,Valkey 用于分布式单键访问

向量相似性搜索

NumPy(cosine_similarity):

  • 批处理操作:快速(通过 SIMD/向量化线性代数优化)

  • 可并行化:高(可扩展到所有可用的物理 CPU 核心)

  • 内存:线性(随嵌入维度直接扩展)

Valkey(RediSearch):

  • 搜索速度:优越(利用专门的 HNSW/向量索引)

  • 可并行化:有限(受引擎线程模型限制)

  • 内存:密集(需要原始嵌入向量加上索引元数据)

胜出者:Valkey 用于亚感知延迟的实时搜索,NumPy 用于繁重的离线分析处理

配置查找

JSON

  • 加载时间:可变(与配置复杂性成正比)

  • 查找时间:快速(原生字典访问)

  • 编辑时间:无摩擦(人类可读的文本修改)

Valkey

  • 加载时间:即时(连接时即激活)

  • 查找时间:受网络限制(取决于请求开销)

  • 编辑时间:编程式(需要 CLI 或客户端 SET 命令)

胜出者:JSON 用于静态配置,Valkey 用于动态共享状态

参考文献

技术

  • NumPy - 数组计算库

  • JSON - 数据交换格式

  • Valkey - 内存数据存储(Redis 分支)

  • RediSearch - 向量搜索模块

技术概念

相关文章

总结

我们使用三种针对不同用例优化的存储技术:

NumPy(嵌入向量):

  • 快速的向量操作(余弦相似度)

  • 内存映射

  • 批处理

  • 标准 ML 格式

JSON(配置):

  • 人类可读(易于调试)

  • 版本控制(Git diff)

  • 手动编辑(无需代码)

  • 通用格式

Valkey(实时缓存):

  • 快速查找(微秒级)

  • 向量搜索(RediSearch)

  • TTL 过期(自动失效)

  • 发布/订阅(实时更新)

混合方法:为每项任务使用合适的工具,组合使用以获得最佳性能。


← 返回文档索引