LatticeDB:融合图、向量与全文索引的嵌入式数据库探索
LatticeDB 最值得关注的不是“又出了一个数据库”,而是它把嵌入式属性图数据库、原生向量索引、原生全文索引这三件事组合到了一起。也就是说,同一个数据对象既可以表达复杂的点边关系,又可以被语义向量召回,还可以被关键词命中,数据只写一份,查询却不只一种。这个组合对做知识图谱、本地语义搜索、RAG 应用、离线数据管理的人来说很实用,尤其是那些不想为一个小工具再单独部署图数据库、向量数据库和 Elasticsearch 的人。
我写这篇文章不是要复述官网功能清单,而是按正常项目评审和动手实验的路径,把 LatticeDB 这类嵌入式属性图数据库的适用场景、运行条件、数据建模思路、向量与全文索引的使用方式、批量任务验证和排查链路拆一遍。有一点要提前说清楚:这个项目是在 Hacker News 上以 Show HN 形式出现的,属于相对新的项目,原始资料没有给出完整的 API 文档和版本信息,所以下面的操作思路更多是结合嵌入式图数据库的通用开发方式来补全的。真正落地前,你需要以手头版本的 README 和 SDK 文档为准。
1. 先理解 LatticeDB 的定位:它不是“又一款图数据库”,而是“把三种检索方式合进一个文件”
很多人看到 LatticeDB 的第一反应是拿它和 Neo4j、DuckDB、SQLite、pgvector 做对比。这样比可以,但容易误判。LatticeDB 的定位不是替代大数据量的在线事务库,而是覆盖中小数据量、单机、进程内嵌入、需要混合检索这类场景。
1.1 嵌入式数据库和 C/S 数据库的差异
嵌入式数据库的意思是数据库以库的形式被引入应用进程,不需要单独启动一个数据库服务进程,数据通常落在一个本地文件里。应用关闭,数据库跟着退出;应用升级,数据库文件仍然保留。和它相对的是客户端/服务器模式,比如 PostgreSQL、MySQL、Neo4j Server 这类独立服务。
这个差异直接决定了适用边界:
- 嵌入式适合桌面工具、本地分析脚本、边端设备、离线环境、单用户应用。
- C/S 模式适合多客户端并发、数据权限复杂、需要独立扩容、需要跨进程访问的场景。
很多人误以为嵌入式数据库性能弱。其实不是性能弱,而是并发模型不同。嵌入式数据库在单进程内访问时速度往往很快,因为它省掉了网络和上下文切换。但一旦多个进程同时打开同一个数据库文件,就需要数据库本身实现文件锁和并发控制,稍不注意就会出现锁冲突或数据文件损坏。所以使用 LatticeDB 前,先想清楚你的应用到底是单进程访问,还是多个进程同时读写。
1.2 “属性图”到底是什么意思
传统关系数据库用表存数据,表之间的关联靠外键。LatticeDB 用的是属性图模型,核心单元是节点、边和属性。
- 节点表示实体,比如人、组织、设备、文件、交易。
- 边表示节点之间的关系,方向很重要,比如“A 关注了 B”和“B 关注了 A”是两条不同语义的边。
- 节点和边都可以挂键值对属性,比如姓名、时间、权重、状态。
图模型的优势在于:当你关心的是多跳关系、路径、网络结构时,用图查询往往比关系数据库多次 JOIN 更直观,索引和遍历也更有针对性。
1.3 原生向量索引和原生全文索引意味着什么
“原生支持”这四个字需要拆开看。很多数据库都可以在字段里塞一段二进制数据,但只是能存,不等于能高效检索。原生支持向量索引,通常意味着存储引擎内置了向量索引结构,例如 HNSW、IVF 这类近似最近邻结构,查询时能按向量相似度返回 top_k 结果,而不是把全表数据读出来挨个算距离。
原生全文索引同理。没有全文索引的数据库只能用 LIKE 做字符串模糊匹配,数据量一上去就容易全表扫描。有原生全文索引的数据库,内部会建倒排索引,把关键词到文档的映射直接存下来,查询时先通过索引定位候选集合,再用相关性算法排序。
LatticeDB 把图和这两种索引放在同一个存储引擎里,最直接的收益是:你不需要在应用层做数据同步了。以前做混合检索,很多人是先把业务数据写进关系数据库,再把文本字段同步到 Elasticsearch,把 embedding 同步到向量数据库,最后在应用层做多路召回再合并。每一层同步都意味着字段映射、增量更新、删除同步、一致性问题。LatticeDB 这种组合方式,至少在中小数据量场景里,可以把这套架构从四层简化成一层。
2. 什么场景真正需要 LatticeDB 这种组合能力
不是所有项目都需要嵌入式属性图数据库,更不是所有项目都需要“图 + 向量 + 全文”三合一。这一节说清楚哪些场景受益明显,哪些场景还是建议用独立数据库。
2.1 知识图谱 + 语义搜索
知识图谱的核心是实体和关系。实体之间往往还有大量文本描述,比如公司介绍、产品说明、工单记录。传统做法是图数据库管关系,向量数据库管语义,ES 管关键词。三个系统各自维护一份数据,甚至内容都不一样。
用 LatticeDB 这种方案,可以这样做:
- 每个实体是一个节点,节点属性里有文本字段。
- 把文本字段用 embedding 模型转成向量,存到节点的向量字段中。
- 在全文索引里给文本字段建立关键词索引。
- 查询时可以直接写一个包含“找到某个节点的所有邻居节点中,语义最接近某个问题的节点”这类逻辑。
这种场景下,LatticeDB 组合能力的价值非常突出,因为它让“关系遍历”和“向量召回”在同一个引擎内完成,减少了数据搬运。
2.2 本地 RAG 应用
RAG 应用通常需要把文档切片、向量化、存储,然后在提问时做向量召回。如果只做简单的文档问答,lancedb、sqlite-vec、chroma 这类方案已经够用。但如果你的 RAG 不是简单的“答案都在一个文档切片里”,而是需要信息在多个实体之间关联,图模型就有优势了。
举个例子:你问“某个项目的负责人最近处理了哪些和高风险设备相关的工单”,这里有实体关系(项目 -> 负责人 -> 工单 -> 设备),有文本语义(工单描述),还有关键词(高风险设备)。用纯向量数据库,很难表达这段跨实体的关系路径;用纯图数据库,又很难按语义相似度召回“描述最接近的工单”。LatticeDB 的价值就在这里。
2.3 嵌入式设备上的本地数据组织
最近嵌入式方向的热度很高,特别是在嵌入式 Linux 板卡上做本地数据采集、关联分析和简单语义检索。这类设备通常资源有限,网络不稳定,不可能一直连接一个独立数据库服务。嵌入式数据库能解决的问题是:数据随应用一起分发,启动后直接读写本地文件,不上云也能完成必要的查询。
如果你的板卡内存比较紧张,存储介质是 eMMC 或 TF 卡,需要关注的就是数据库文件大小、索引构建时的内存占用、写入频率。向量索引和全文索引都是典型的空间换时间结构,索引越小,构建越快,但召回精度可能会有变化。所以做嵌入式部署时,不要一上来就导入全量数据,先在开发机上用小样本确认索引构建时间、数据库文件大小和启动耗时,再移植到目标板卡。
2.4 什么时候不该选 LatticeDB
边界同样重要。以下几种情况,我建议你用成熟方案:
- 数据量大到几十 GB 以上,需要分布式存储和水平扩展,嵌入式数据库不适合。
- 并发用户非常多,比如线上业务系统同时几百人写数据,应该用 PostgreSQL、MySQL 这类独立服务。
- 团队已经很熟练地使用 Neo4j + pgvector + Elasticsearch,且运维成本可以承受,没必要迁移。
- 需求只是简单的 key-value 或关系查询,多引入图和向量索引反而增加概念成本。
- 项目刚发布,API 不稳定,文档不全,不适合作为生产环境的唯一数据源。
LatticeDB 这类数据库最适合的是“单机、中小规模、开发效率优先、要混合检索”的场景。用它,你获得的是开发心智上的统一。
3. 运行条件与最小验证流程:先用一次“初始化 + 写点 + 写边 + 查询”跑通
不管什么嵌入式数据库,第一次使用都应该按最小可运行步骤来验证,而不是先研究所有功能。下面给出一套通用流程,具体 API 名称要以 LatticeDB 实际版本为准。
3.1 环境与依赖前置条件
嵌入式数据库通常以库文件或语言扩展形式提供。从 “嵌入式” 这个定位判断,你大概率会看到以下几种交付形式之一:
- C/C++ 库,通过 cmake 或源码编译集成。
- Python 包,通过 pip 安装。
- Rust crate,通过 cargo 引入。
- 其他语言绑定,需要你确认项目是否已经发布。
无论哪种形式,我建议先确认三件事:
- 操作系统支持。是只支持 Linux,还是 Windows、macOS 都支持?不同架构的预编译包是否齐全?
- Python 版本或工具链版本。如果你的环境太新或太旧,依赖编译可能失败。
- 存储路径。嵌入式数据库需要指定数据库文件目录,注意目录是否有写权限,是否在临时目录里。我经常遇到 Demo 能跑,但把数据库放进系统目录后报权限错误的情况。
3.2 设计最小验证样例
不要一开始就设计复杂的数据模型。最小验证样例只需要覆盖四个动作:
- 初始化数据库文件。
- 创建两个节点。
- 在两个节点之间创建一条边。
- 执行一次图查询或属性查询,验证结果能按预期返回。
如果项目同时开发了向量和全文索引能力,最小样例增加两个动作:
- 给某个节点添加一个文本属性和一个向量属性。
- 执行一次文本搜索和一次向量搜索,确认索引能返回结果。
设计这个样例的目标是验证 API 是否稳定、数据是否真正落盘、索引是否真的生效。
3.3 伪代码框架参考
为了避免误导大家,我这里不写真实 API,而是用伪代码表达这一类库通常的使用思路。你拿到实际 SDK 后,只需要把函数名替换成对应版本即可。
# 伪代码框架,只是表达使用思路,不是 LatticeDB 的真实 API lat = LatticeDB.open("data/lattice_demo.db") graph = lat.get_graph() alice = graph.add_vertex("person", {"name": "Alice", "title": "Engineer"}) bob = graph.add_vertex("person", {"name": "Bob", "title": "Manager"}) graph.add_edge(alice, bob, "reports_to", {"since": 2024}) alice.set_text("bio", "Alice is a database engineer who focuses on graph index.") alice.set_vector("embedding", [0.21, -0.54, 0.88, ...]) results = graph.search_text("index engineer") vector_results = graph.search_vector("embedding", target_vector, top_k=5) print(results) print(vector_results) lat.close()看到错误的不要慌,先看四样东西:初始化路径、语法版本、字段类型、索引名称。九成问题出在这里。
3.4 验证成功后的下一步
跑通最小样例之后,不要立刻接入全量业务。建议先做三个压力测试:
- 写 100 个节点、200 条边,观察写入耗时。
- 在同一批数据上建全全文索引,跑一次包含关键词的查询。
- 在向量字段上构建索引,跑一次 top_k 查询,看返回的排序是否合理。
这三个测试完成后,你对 LatticeDB 的资源占用和查询表现就有一个基本判断了。如果连这个规模都慢得离谱,那问题可能出在 API 用法不对,或索引没有真正创建。
4. 图数据建模:把“表思维”切换成“图思维”
使用属性图数据库,最大的门槛不是 API,而是建模方式。很多人用图数据库写出来的查询又绕又慢,并不是数据库不行,而是数据模型仍然按照关系表的思路设计。
4.1 节点、边和属性的建模原则
建模时可以参考几个很实际的原则:
- 实体放节点,关系放边,描述性字段放属性。
- 如果一个字段将来会被单独检索,就应该作为属性暴露出来,而不是塞进一个 JSON 字符串里。
- 如果两个节点之间有多种关系,应该使用不同边类型,比如“关注”“拉黑”“转发”是三种边,不要合成一条带类型字段的边。
- 边也可以有属性。比如“任职”这条边可以带“入职时间”“离职时间”“职位”。
举一个知识图谱的简单例子:你有一份产品文档,文档中提到了多个负责人。此时“文档”和“人”都是节点,文档和负责人之间是“负责人”边,边属性可以记录“负责起始时间”“负责模块”。而文档正文内容则建议放成文档节点的文本属性。这样既可以通过图关系找到负责人,又可以对文档正文做全文和向量检索。
4.2 实体是节点还是属性
这是新手最容易纠结的问题。判断标准很简单:如果这个信息需要作为关系中的端点,就应该是节点;如果只是描述某个节点的状态,就应该是属性。
举个例子:“员工姓名”通常是属性,因为一般不会有人查询“所有名字叫张三的员工之间的关系”。但“项目”必须是一个节点,因为项目连接了多个员工,项目本身挂载了文档、排期、风险信息。
4.3 标签与多跳查询
属性图通常允许给节点和边打标签或类型。建议把标签设计成稳定的枚举值,不要过于零散。查询多跳关系时,可以使用递归或路径查询。比如“找出 Alice 所有下属的下属”就是一个两跳查询。在图模型里这种查询可以很直接,但前提是边的方向一致,同一类关系不能一会儿从 A 指向 B,一会儿从 B 指向 A。
这也是我实际测图数据库时经常遇到的坑:数据写入时没有统一边的方向,查询时返回结果不稳定。LatticeDB 即使支持无向边,我也建议在业务层约定好方向,否则建出来的索引和业务语义可能不一致。
5. 原生向量索引:别把“能存向量”当成“能检索向量”
5.1 向量字段和向量索引的结构差异
在向量索引字段里,你能存储一组浮点数,但不代表所有向量条目都会被高效检索。真正决定检索效率的是索引结构。常见的向量索引算法包括 HNSW、IVF、PQ 等。HNSW 的特点是召回率高、查询快,但内存占用比较高,构建也相对慢。IVF 的特点是构建快、内存占用低,但需要训练聚类中心,召回率取决于参数设置。
对于嵌入式场景,我的建议是:
- 数据量在几万条以内,直接用暴力精确检索可能都比向量索引快,不需要过早建索引。
- 数据量到几十万甚至百万级别,再考虑 HNSW 这类近似最近邻索引。
- 向量数据的维度要统一。经常有人把不同模型输出的 embedding 混在一起存,查询时维度不一致,直接报错或返回空结果。
5.2 距离度量怎么选
向量相似度最常用的三个度量:余弦相似度、欧氏距离、点积。
- 一般文本 embedding 用余弦相似度比较多,因为它对向量模长不敏感,强调方向。
- 如果向量经过归一化处理,点积和余弦相似度在排序上等价。
- 欧氏距离更关注绝对差异,适合图像特征等场景。
实际使用时不要只盯距离绝对值,更值得关注的是 top_k 排序是否稳定。我一般会先把 sample 数据的相似度分数打印出来,看看同一类文本的距离分布,再决定选择哪个阈值。
5.3 向量索引的正确使用流程
向量索引不是无脑建越多越好。正确的使用流程可以这样拆:
- 确认向量来源。比如文本经过哪个 embedding 模型转换,维度是多少,是否需要归一化。
- 写入测试数据。先用少量数据验证字段名和类型。
- 构建向量索引。注意构建时的内存占用,特别是嵌入式设备。
- 执行 top_k 查询。查看召回结果是否包含相关项。
- 调整参数。比如 HNSW 的 M 和 efConstruction,以及查询时的 efSearch。
这里最容易踩的坑是:向量文本写进去了,但没有调用索引构建接口,查询时数据库返回全部数据或者直接报错。先看日志,再看索引名称是否和字段绑定,不要一上来就怀疑向量算法有问题。
6. 全文索引:关键词搜索不是简单的 LIKE
6.1 全文索引到底解决了什么
LIKE '%关键词%' 的查询无法利用普通 B-Tree 索引,数据量上去后就是全表扫描。全文索引采用倒排索引,把每个词映射到包含它的文档列表。查询时先在倒排列表里定位候选文档,再做相关性排序,所以在文本检索场景里,全文索引通常远快于 LIKE。
LatticeDB 原生支持全文索引,意味着你可以在图的节点属性上直接做关键词检索。这对很多本地应用来说非常实用。比如你有一批工单节点,每个工单节点有描述文本,你可以直接索引“描述”字段,然后查询包含“数据库”“崩溃”“权限”等关键词的工单,同时还能借助工单节点之间的关联关系做筛选。
6.2 分词、停用词和多语言问题
全文索引的效果非常依赖分词器。英文分词相对简单,按空格和标点处理即可,但中文分词需要词典或统计模型。LatticeDB 对中文的支持情况,需要你在实际版本中验证。如果项目底层内置了全文检索库,比如 Tantivy、Lucene、SQLite FTS5 这类组件,中文分词能力会不一样。建议你专门用中文文本做一个检索测试,看看“数据库”是否能被正确切分,是否会出现检索不到的问题。
文本字段比较长时,还会涉及停用词、词干化、大小写归一化等问题。如果你的内容是英文技术文档,词干化和停用词处理会影响召回率;如果是中文内容,最需要关心的是分词是否靠谱。
6.3 全文索引和向量索引的分工
在 LatticeDB 中,你可以同时给同一个文本字段建全文索引和向量索引。全文索引负责精确词命中,向量索引负责语义近似。两种检索方式可以互相补充:
- 适合全文索引的场景:产品型号、错误码、固定短语、人名、规范名词。
- 适合向量索引的场景:同义改写、自然语言问题、没有固定关键词的模糊表达。
做一个混合检索时,简洁的方法是先执行一路召回,再在结果上做另一路排序。比如先用全文索引找到包含“数据库错误”的节点,再按向量的语义相似度对这些候选节点排序。这样做比直接把全部数据都做向量检索更稳定,也更可控。
7. 混合查询和批量任务:如何从 Demo 走向真实使用
很多人在 Demo 阶段一切正常,一旦进入批量导入和混合查询,问题就集中爆发。这一章讲清楚从单任务到批量任务的验证节奏和注意事项。
7.1 混合查询的典型结构
你可以把混合查询理解成多路条件同时命中一个图。假设我要找“与开发者小明相关,且语义上接近‘数据库性能问题’的文档”,逻辑上至少有三个条件:
- 图条件:小明节点通过“负责”边关联到的文档节点。
- 向量条件:文档节点的 embedding 与“数据库性能问题”的向量接近。
- 可选全文条件:文档标题或正文命中“性能”。
理想情况下,LatticeDB 能把这些条件组合到同一次查询里,先做图操作缩小候选范围,再做向量或全文检索。具体有多少能力是“同时过滤”还是“先召回再合并”,要看数据库查询体的实现。我的建议是先用小数据手动验证一下组合查询的结果,看看是否和分步查询一致。很多数据库组合查询的语义不同,容易让人误以为逻辑正确。
7.2 批量导入:事务、命名和输出一致性
从单条任务变批量任务,核心要处理的不再是查询逻辑,而是事务边界和输出一致。
批量写入向量和文本时,有一个常见问题:同一批数据写入后,全文索引没有自动更新,导致查询结果缺失。不同数据库对索引更新的策略不同,有的是写时同步更新,有的是延迟合并。建议你在批量导入后执行一次 force merge 或 refresh 操作,并且用查询结果验证索引是否生效。
另一个常见问题是输出顺序不稳定。向量检索本身是近似最近邻,top_k 结果存在一定不确定性。如果你的业务对结果顺序要求严格,建议把相似度分数保存到业务层,再按业务逻辑二次排序。全文检索的相关性分数同理,不同版本的分词和评分算法都可能变化。
7.3 批量任务失败重试
批量导入时,如果中间发生网络错误、权限异常、端口冲突或文件占用,任务可能中断。对嵌入式数据库来说,大批量写入失败最常见的两个原因,一是磁盘空间不足,二是单事务内写入量过大导致内存飙升。
建议这样处理:
- 把批量导入拆成小批次,比如每 100 条一个事务。
- 为每条记录设计稳定的 ID,失败后重试时不会产生重复节点。
- 日志里记录每次写入的批次范围和失败原因。
- 先跑一个 1% 的样本批次,验证输出数量和字段完整性,再全量导入。
如果你要做的数据量极大,比如几百万条文本,建议不要全部通过脚本一条条写入。先看 LatticeDB 是否提供 CSV 或 JSON 批量导入接口,如果有,优先使用批量导入,性能通常远高于逐条调用。
8. 常见坑与排查链路:先看日志,再改参数
最后把我在实际测试嵌入式数据库时经常遇到的问题整理成排查链路。这些经验对 LatticeDB 同样适用。
8.1 启动失败或初始化失败
启动阶段的问题通常是环境和路径问题。排查顺序如下:
- 数据库文件路径是否存在,目录是否有写权限。
- 依赖版本是否匹配,比如 Python 库不同版本 API 是否兼容。
- 数据库文件是否被其他进程占用。Windows 下文件锁问题尤其突出。
- 日志有没有输出堆栈,比如缺少某个 native 库或编译工具链。
8.2 查询返回空结果或结果无关
这类问题最容易让人怀疑索引失效。实际排查顺序是:
- 先查数据是否真的写进去了,执行一次不带过滤条件的全量查询。
- 再查字段名是否写对,特别是向量字段和全文索引字段的绑定关系。
- 如果数据存在但全文检索无结果,先确认索引是否构建,分词器是否适合你的语言。
- 如果向量检索无结果,先检查向量维度和查询向量是否一致,再检查相似度阈值是否设置过高。
8.3 查询慢或资源占用过高
查询慢不一定是数据库性能弱,有可能你的查询设计导致大量数据被扫描。排查顺序:
- 看日志或 profile 输出,确认查询有没有走索引。
- 看条件字段是否离散度足够。如果一个字段每个节点都一样,索引很难发挥作用。
- 看向量查询的候选集范围。如果 filter 条件太宽,等价于全量计算相似度。
- 看并发设置。进程内同时候多任务写入,容易产生锁等待。
8.4 给新手的决策建议
如果你正在评估 LatticeDB,我的建议是把评估分成四个阶段:先在开发机跑通最小样例,再用自己的业务数据构建一个 5 万条左右的中等数据集验证检索效果,然后测试批量导入和索引重建,最后才考虑是否引入到实际业务。
整体来看,LatticeDB 这类嵌入式属性图数据库,最让人心动的是把图、向量、全文三种能力统一到一套数据模型里,省掉了数据同步和应用层多路召回的处理。但它毕竟是一个需要实际验证的新项目,落地时最该盯住的不是功能列表,而是输入格式、资源占用、索引更新、失败重试和查询稳定性。先把单任务跑稳,再考虑批量化和生产化,这是最稳妥的路径。
如果只是学习,或者做一个单机本地小工具,默认配置通常够用。如果要长期使用,我建议你把数据库文件路径、日志目录、索引构建策略和备份方案提前整理好,避免数据越来越复杂之后难以调整。踩过几次之后你会发现,很多问题不是工具能力不够,而是前置环境和数据模型没有处理干净。
