Feast SQLite Online Store:从 feature_store.yaml 配置到源码实现的本地在线特征服务
【免费下载链接】feastThe Open Source Feature Store for AI/ML项目地址: https://gitcode.com/GitHub_Trending/fe/feast
Feast 的 SQLite Online Store 是最轻量的在线存储后端:它把特征值物化到一个磁盘上的 SQLite 数据库文件中,供 Python/Go SDK 在线读取。本文基于 SQLite Online Store 官方参考文档 展开,结合 SqliteOnlineStore 源码实现、Go 端实现 与 单元测试,完整讲解其配置项、底层数据模型、读写路径、plan/update/teardown 基础设施管理,以及向量与全文检索扩展能力,帮助你在本地开发、示例演示或轻量级部署中正确配置并理解它的行为边界。
一、定位与核心特征
按照 官方文档 的定义,SQLite Online Store 提供将特征值物化(materialize)到 SQLite 数据库以进行在线特征服务的能力,其核心特征是:
- 所有特征值存储在磁盘上的 SQLite 数据库中——整个后端就是单个
.db文件,无需额外安装数据库服务; - 仅持久化每个 key 的最新特征值——写入采用 upsert 语义,历史值不保留,这与文档功能矩阵中"不支持 TTL、不支持过期数据删除"是同一枚硬币的两面。
源码层面同样明确了它的定位边界。SqliteOnlineStore 类的 docstring 直接写明:"SQLite implementation of the online store interface. Not recommended for production usage."也就是说,它是 Feast 核心在线存储实现之一(与 Redis、DynamoDB、Snowflake、Datastore 并列,见 在线存储总览),但官方建议将其用于本地开发、快速上手、演示与测试场景,而非生产环境。这一判断也体现在它的功能短板上(见第七节功能矩阵):不支持同 key 并发写、不支持 TTL、Java SDK 无法读取。
二、配置:在 feature_store.yaml 中启用 sqlite online store
启用方式非常直接。在特征仓库根目录的feature_store.yaml中把online_store.type设为sqlite并指定数据库文件路径即可:
project: my_feature_repo registry: data/registry.db provider: local online_store: type: sqlite path: data/online_store.db仓库中的 MCP Feature Store 示例配置 就是一个真实用例:type: sqlite、path: data/online_store.db,并搭配type: file的离线存储和type: mcp的 Feature Server,构成一个完全本地化的运行形态。快速入门文档 与 feature-store-yaml 参考文档 中给出的默认仓库模板也采用同样的 SQLite 在线存储。
2.1 配置项全集
文档指出完整配置项见SqliteOnlineStoreConfig。对照 源码中的 Pydantic 模型定义 及其混入的 VectorStoreConfig,配置项全集如下:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
type | Literal["sqlite"] | "sqlite" | 在线存储类型选择器,也接受完整类名feast.infra.online_stores.sqlite.SqliteOnlineStore(见_get_db_path中的断言,L232-L243) |
path | StrictStr | "data/online.db" | SQLite 数据库文件路径;若为相对路径,会基于特征仓库路径(repo_path)解析为绝对路径 |
vector_enabled | Optional[bool] | False | 是否启用向量相似检索(依赖sqlite_vec扩展,见第六节) |
similarity | Optional[str] | "cosine" | KNN 检索的向量相似度度量;主要服务于"不支持在检索时动态配置度量"的向量库 |
text_search_enabled | bool | False | 是否启用基于 SQLite FTS5/BM25 的全文检索 |
enable_openai_compatible_store | bool | False | 是否以"OpenAI 兼容存储"模式运行:写入时为每行物化value_text数值化列value_num,使检索支持基于特征数值的元数据过滤 |
注意一个容易踩坑的细节:path的解析逻辑在_get_db_path中——只有当配置了repo_path且path是相对路径时,才会拼接成repo_path / path,否则原样使用。这意味着feast materialize、feast apply等操作在不同工作目录下执行时,实际读写的.db文件可能位于不同位置,排障时应以特征仓库根目录为基准确认数据库文件位置。
三、底层数据模型:每个 Feature View 一张表
SQLite Online Store 的数据模型是按 Feature View 共置(collocated by feature view)——这与功能矩阵中 "collocated by feature view: yes" 一致。每个启用在线服务的 Feature View 对应一张物理表,表名由compute_table_id(即源码中的_table_id)基于 project 名与 Feature View 名计算,因此测试中可以看到test_project_driver_dropoffs_stream这类表名(test_sqlite_plan.py)。
3.1 表结构
update()方法 与SqliteTable.update()中的 DDL 揭示了完整的表结构:
CREATE TABLE IF NOT EXISTS "<table_name>" ( entity_key BLOB, -- 序列化后的实体 key(EntityKeyProto -> 二进制) feature_name TEXT, -- 特征名 value BLOB, -- ValueProto 的 protobuf 序列化结果,读取时反序列化 value_text TEXT, -- 值的人读文本形式(过滤辅助列) value_num REAL, -- 可选列:仅 enable_openai_compatible_store 时创建,数值化形式 vector_value BLOB, -- 可选列:vector_enabled 时写入 f32 序列化向量 event_ts timestamp, -- 事件时间戳(UTC naive) created_ts timestamp, -- 物化/写入时间戳 PRIMARY KEY (entity_key, feature_name) )并会为每张表额外创建entity_key索引:CREATE INDEX IF NOT EXISTS "<table>_ek" ON "<table>" (entity_key)。
几个值得注意的实现细节:
- 复合主键
(entity_key, feature_name)是"仅保留最新值"的根本保证。写入使用INSERT ... ON CONFLICT(entity_key, feature_name) DO UPDATE SET ...的 upsert 语句(online_write_batch L324-L328),同一实体同一特征的新值会整体覆盖旧值,而不是追加。 entity_key以 BLOB 存储:写入前调用serialize_entity_key按entity_key_serialization_version版本序列化(L332-L335),读取时再反序列化。这也解释了为什么 SQLite 在线存储与 Go SDK 的 key 序列化版本必须保持一致。value存的是ValueProto的 protobuf 字节(val.SerializeToString(),L345),读取时用ValueProto.ParseFromString还原(online_read L408-L409)。这一"protobuf 透传"方式使 Python 与 Go 两个 SDK 可以无损地共享同一份数据。- 时间类型适配:模块顶部通过
sqlite3.register_adapter注册了date/datetime的 ISO 8601 与 Unix epoch 两种适配,以及对应的 converter(L74-L111),配合建连接时的detect_types=PARSE_DECLTYPES | PARSE_COLNAMES(_initialize_conn L911-L929),使event_ts、created_ts列能以原生 Pythondatetime对象读出。 - 安全加固:所有表名/列名拼接前都会经过
_quote_id的双引号转义,注释明确说明其目的是防止 SQL 注入。
四、写路径与读路径
4.1 写入:online_write_batch
online_write_batch 的写入流程是:
- 通过
_get_conn懒加载连接(首次访问时才sqlite3.connect,且check_same_thread=False); - 解析 Feature View 的特征类型字典、表名,并根据
enable_openai_compatible_store检查表中是否已存在value_num列(结果会被缓存在_table_has_value_num中); - 逐行执行参数化 upsert:
entity_key序列化、时间戳统一转为 UTC naive(to_naive_utc),每个(entity_key, feature)对写入一行,value为 protobuf 字节、value_text为文本形式,可选写入value_num(数值)与vector_value(若该特征是向量类型,则按 Feature View 的vector_length元数据做serialize_f32序列化,默认长度 512); - 整个批次包在单个
with conn:事务中提交,并对每个实体调用progress(1)回调,供上层展示物化进度。
一个实用的行为差异:如果配置了enable_openai_compatible_store: true但表里还没有value_num列,源码不会报错,而是打印警告"Runfeast applyto add it. Writing without value_num."(L304-L309),即降级为不带数值列的写入。因此修改该开关后必须执行一次feast apply让 schema 变更落地。
4.2 读取:online_read
online_read 是一次典型的批量点查:
- 把所有
EntityKeyProto序列化成二进制后,用一条SELECT entity_key, feature_name, value, event_ts FROM <table> WHERE entity_key IN (?, ?, ...) ORDER BY entity_key一次性取回; - 用
itertools.groupby按entity_key分组,逐实体重建feature_name -> ValueProto字典,并统一把时间戳规整为 UTC; - 未命中的实体返回
(None, None),由上层在线服务将其解析为空特征值,而不是抛错。
Go SDK 侧的SqliteOnlineStore.OnlineRead采用完全一致的读法:mattn/go-sqlite3驱动打开同一文件,按 feature view 逐个执行IN (...)查询,用proto.Unmarshal把value字节还原为types.Value,再按实体 key 的 hash 索引回填到结果矩阵。这也印证了功能矩阵中 "readable by Go: yes" 且 "readable by Java: no"(仓库 Go 目录存在独立实现,Java serving 模块则没有对应存储)。
五、基础设施生命周期:plan / update / teardown
SQLite Online Store 完整实现了OnlineStore接口的五个方法,功能矩阵中对应的四行 "update / teardown / plan: yes" 都能从源码逐一对应:
update()(L423-L458):对保留的表执行CREATE TABLE IF NOT EXISTS与索引创建;对需要删除的表执行DROP TABLE IF EXISTS。由于 SQLite 的ALTER TABLE ADD COLUMN不支持IF NOT EXISTS,源码封装了_alter_table_add_column_if_missing——捕获 "duplicate column name" 的OperationalError并静默忽略,其他错误照常抛出,以此实现幂等的列升级(value_text、value_num)。teardown()(L500-L522):比单纯 drop 表更彻底——先关闭连接,然后直接os.unlink删除整个.db文件,并对PermissionError做最多 10 次、间隔 0.25 秒的重试。这意味着feast teardown后数据库文件本身就不存在了。plan()(L460-L498):从 registry proto 反解出全部 Feature View / Stream Feature View / 在线 Label View,为每一个生成一个SqliteTable基础设施对象(含path、name、include_value_num),序列化为SqliteTableProto(对应 protos/feast/core 下的SqliteTable.proto),供feast diff类命令做基础设施变更规划。值得注意的是,plan()必须区分FeatureView.from_proto与StreamFeatureView.from_proto——回归测试 专门验证了"registry 中只有流特征视图"与"批、流视图混合"两种场景都能正确产出SqliteTable列表,这是一个曾经因类型检查导致崩溃的真实缺陷。
SqliteTable作为InfraObject的实现(L966 起),把CREATE TABLE / CREATE INDEX / ALTER逻辑收敛到update()中,并附带同样受 Python 版本约束的sqlite_vec加载逻辑,是feast apply在 SQLite 后端上的实际执行单元。
六、向量检索与全文检索扩展
虽然文档主体描述的是常规在线服务,但当前源码中 SQLite 后端还内置了两种检索扩展,对应配置项vector_enabled与text_search_enabled:
6.1 向量相似检索(sqlite_vec)
开启vector_enabled: true后:
- 写入时,向量类型的特征会被
serialize_f32序列化为定长 float32 BLOB 存入vector_value列(L350-L365); - 检索入口
retrieve_online_documents会动态创建vec0虚拟表、把目标向量字段的数据灌入,再用WHERE vector_value MATCH ? ORDER BY distance LIMIT ?做 KNN,最后 join 回主表取回entity_key、原始值与时间戳; - 从源码结构看,
sqlite_vec扩展的加载被限制在Python 3.10(sys.version_info[0:2] == (3, 10),L246-L252 与 L1014-L1021),且扩展缺失时仅告警降级,其他 Python 版本无法启用向量路径。 - Feature View 中必须声明且仅声明一个带向量索引的字段,否则
_get_vector_field会直接断言失败。
6.2 全文检索(FTS5 + BM25)与元数据过滤
- 开启
text_search_enabled: true后,retrieve_online_documents_v2会基于fts5创建search_table(tokenize="porter unicode61"),将 Feature View 的全部 STRING 类型特征列灌入,用bm25(search_table, ...)计分并ORDER BY distance LIMIT top_k完成关键词检索; - 若检索时携带数值条件过滤(filters),则必须同时开启
enable_openai_compatible_store: true并运行feast apply生成value_num列,否则抛出明确异常(L688-L715)。过滤条件由SqliteFilterTranslator翻译成entity_key IN (SELECT entity_key FROM ... WHERE feature_name = ? AND value_num <op> ?)形式的子查询,支持eq/ne/gt/gte/lt/lte/in/nin及AND/OR复合条件。
这些扩展能力使 SQLite 后端在本地场景下甚至可以承担轻量的"向量/全文 + 元数据过滤"型文档检索,但请注意其仍受单文件数据库的并发与容量限制约束。
七、功能矩阵与适用边界
以下是 官方文档 给出的完整功能矩阵(可直接对照 在线存储功能总览 与其他实现比较):
| 能力 | Sqlite |
|---|---|
| 写入特征值到在线存储 | yes |
| 从在线存储读取特征值 | yes |
| 更新基础设施(如表) | yes |
| 拆除基础设施(如表) | yes |
| 生成基础设施变更计划(plan) | yes |
| 支持 on-demand transforms | yes |
| 可被 Python SDK 读取 | yes |
| 可被 Java 读取 | no |
| 可被 Go 读取 | yes |
| 支持无实体(entityless)Feature View | yes |
| 支持同 key 并发写入 | no |
| 支持检索时 TTL | no |
| 支持删除过期数据 | no |
| 按 Feature View 共置 | yes |
| 按 Feature Service 共置 | no |
| 按实体 key 共置 | no |
结合源码,可以把这几项 "no" 理解为工程取舍而非遗漏:
- 无并发写:SQLite 的写锁与单文件模型决定了同一 key 的高并发更新不是它的设计目标;源码中连接仅懒加载一次并长期持有(
_conn),面向单进程物化/服务场景。 - 无 TTL / 无过期删除:因为 upsert 只保留最新值,时间维度信息仅体现在
event_ts中,存储层不做过期清理。 - 无 Java 读取:仓库的
java/serving模块没有 SQLite 存储实现,只有 Python 与 Go(go/internal/feast/onlinestore/sqliteonlinestore.go)两条读取路径。 - plan 能力独有优势:在总览矩阵中,支持 "generate a plan of infrastructure changes" 的核心实现只有 SQLite 与 Cassandra,这意味着 SQLite 后端可以完整地配合
feast diff/feast apply做声明式基础设施演进。
适用建议:本地开发与学习(feast init默认形态)、快速演示与 notebook 实验(如 MCP Feature Store 示例、credit-risk 端到端示例 均使用 sqlite 在线存储)、CI 测试与离线评估;生产环境请换用总览矩阵中的其他在线存储。
八、参考文件索引
- 官方参考文档:docs/reference/online-stores/sqlite.md;在线存储对比总览:docs/reference/online-stores/overview.md
- Python 实现:sdk/python/feast/infra/online_stores/sqlite.py;向量配置基类:sdk/python/feast/infra/online_stores/vector_store.py
- plan 回归测试:sdk/python/tests/unit/infra/online_store/test_sqlite_plan.py
- Go 实现:go/internal/feast/onlinestore/sqliteonlinestore.go
- 实际配置示例:examples/mcp_feature_store/feature_store.yaml;配置参考:docs/reference/feature-repository/feature-store-yaml.md
【免费下载链接】feastThe Open Source Feature Store for AI/ML项目地址: https://gitcode.com/GitHub_Trending/fe/feast
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考