本地优先的书签检索

找回那个你只记得一半的书签。

你存过它。你大概记得它讲什么,或者记得当时为什么想要它,或者记得那个下午你还在看别的什么 —— 就是不记得标题。facetmark 把这三件事全部建成索引,一起拿来搜,全部落在你自己机器上的一个 SQLite 文件里。

Python 3.10+测试 1,514许可证 MIT存储 1 个 SQLite 文件上传
facetmark demo --size 60
$ facetmark search "sqlite-vec latency shard recall"
// 内容型查询 —— 你记得里面的词
 
1Why chromadb changes the recall story0.0776
2sqlite-vec: notes on embedding0.0829
3hnswlib-5: notes on index0.0768
4qdrant-6: notes on persistence0.0767
5Evaluating pgvector-6 for filter0.0777
 
5 条结果 · 17.0 ms · 目标在第 2
这是 facetmark demo 的真实输出,它会离线造一个 60 页的合成书签库。provider 是 mock,所以这是一次管路体检,不是质量度量 —— mock 是把文本哈希成向量的。分数列看上去没排序,是因为名次来自重排阶段,而分数是融合分,重排故意不去覆盖它。

它要解决的问题

你找一个存过的页面,只有三种找法

文件夹树能回答第一种。后两种它一句话都接不上,所以你最后总是在翻历史记录。这三类就是整套评测用的三种查询类型,后面的数字是它们在一个真实的 1,700 条书签库上真实跑出来的 Recall@5。

内容型

你记得里面的词

页面里写了 sqlite-vec、写了 shard,你想把它找回来。这种任何像样的索引都能干。

「sqlite-vec latency shard recall」

0.959
Recall@5
模糊型

你记得意思,不记得词

你知道它解决了什么问题。你从来就没记住那个产品叫什么,或者已经忘了。文件夹名字在这里一点忙也帮不上,内容面就是为这一类存在的。

「那个讲把向量和其他数据放在一起、不用再起一个服务的东西」

0.706
Recall@5
情景型

你记得时间,和旁边那一批

「就是我看 qdrant 的那个下午。」facetmark 会重建保存会话和一张链接图来回答它 —— 而它依然是短板。放在这里是因为它是真的,不是因为它好看。

「和 qdrant 那篇差不多时候存的另一个」

0.279
Recall@5

479 条查询,一个真实库,A 档。完整协议和剩下的表在实测页

快速开始

四条命令

如果你用的是 Chromium 系浏览器 —— Chrome、Edge、Brave、Vivaldi、Chromium、Opera —— 连导出都不用。facetmark import 不带参数时会自己找到浏览器配置文件并读它。

shell
pip install facetmark

facetmark import                  # 自动找浏览器,只读
facetmark index                   # 抓取、富化、嵌入、会话、边
facetmark search "那篇讲把向量存在 sqlite 里的"

手边没 API key?facetmark demo 会离线造一个 60 页的合成库并搜它,让你先看看输出长什么样,再决定要不要投入。

  1. 安装。Python 3.10 以上。唯一一个大体积的可选依赖是 sentence-transformers,而且只有你想本地跑嵌入时才需要。
  2. 导入。从不写回你的浏览器。Firefox 和 Safari 不是 Chromium 系,这两个需要先导出一次 HTML —— 怎么导
  3. 接上模型。任何 OpenAI 兼容的端点,或者一个本地嵌入模型、完全不要 key。一个模型都不接也能用,只是只剩词面和会话图。
  4. 建索引。幂等。以后新增了书签再跑一次,它只做新增的那一部分。
  5. 搜。或者 facetmark serve 之后用浏览器扩展、HTTP API、或者 MCP 客户端。

它是怎么工作的

四个面。出厂默认只开一个。

facetmark 在同一个库上建了四套互相独立的索引,可以用 RRF 把它们融合起来。然后它把这个融合实测了一遍,发现还不如单拿最好的那一个面,所以出厂默认只开一个面。另外三个还在,还有测试,一个参数就能打开 —— 只是默认不开,因为数字说不该开。

索引的是什么回答什么样的问题默认
词面
两个 FTS5 索引
标题、URL、正文的字符三元组和词段。精确字符串、ID、代码、报错信息,以及没有空格可分的中文。
融合时输了 5.4pp
内容面
稠密向量
页面正文抽取后的嵌入,不是标题的。换说法。词忘了、意思还在的那种。
W1 赢家,0.643
意图面
生成的查询
模型为这个页面写的候选问法,再用「能不能把这页捞回来」过滤一遍。你以后会怎么开口找它。
只有 38% 的意图站得住
上下文面
会话与图
保存会话聚类、域名结构,以及全库的链接图。「我存那个的时候还顺手存了哪些?」图扩展开 情景门关
+2.09pp / −18.83pp

这四个结论每一个都对应实测页上一份协议、一套查询集和一个置信区间。

管线

从一句查询到一份排名

有颜色的阶段是出厂默认真的会跑的。灰色的是写了、测了、然后关掉的。每一个索引阶段都是幂等的并且带指纹,所以 facetmark index 只会重做输入变了的那部分。

你的问题打出来的一句话理解语言、意图词面 · 三元组FTS5,按字符切默认关闭词面 · 分词FTS5,按词切默认关闭内容正文的向量意图生成问句的向量默认关闭RRF 融合k = 60后置阶段情景闸门冷层重排结果已排序一跳图扩展会话边 + 语义边相关单独一组默认真正走的路径写好了、接好了,默认关着
你的问题打出来的一句话
理解语言、意图

四路并行默认只开一路

词面 · 三元组FTS5,按字符切默认关闭
词面 · 分词FTS5,按词切默认关闭
内容正文的向量
意图生成问句的向量默认关闭
RRF 融合k = 60
后置阶段情景闸门 · 冷层 · 重排
结果已排序

从融合这一步分出去

一跳图扩展会话边 + 语义边
相关单独一组

默认真正走的路径写好了、接好了,默认关着

建索引

bookmarkfetchcontentenrich(摘要、主题、实体、要点)→ embedintents → 过滤 → sessionsedges

指纹

富化按正文哈希计算;嵌入按重建后的嵌入文本计算 —— 所以一个和自己文本对不上的向量会被发现,而不是被相信。--force 两个都不看。

图扩展

从融合结果往外走一跳,作为单独一组返回,不混进排名里。实测 +2.09pp,10 胜 0 负,9 ms。

你要打开的那个页面

一个搜索页,就在 127.0.0.1:8787/app

facetmark serve 会打印一个地址。打开它,你就拿到了搜索框、和扩展里同一套结果标记,外加一个告诉你索引里到底有什么的第二个视图。不用装,也不用编译 —— 这个页面就装在 Python 包里,由同一个进程发出来。

facetmark 搜索页,上面是排好序的结果,下面是当时前后一起存的那一组同一个搜索页的深色模式
搜索。第一屏是字面匹配,不花任何模型调用;排好序的答案到了就把它换掉。当时前后一起存的页面单独成一组,不会被打散混进排名里。这张图会跟着你正在读的这个页面切换深浅色。

它自己配对

令牌来自一条只在调用方请求里写的地址两者都是回环地址时才回答的路由,所以在你自己机器上没有什么要复制的。换个地方,页面会让你粘贴一次。

缺什么它会说

空的书签库会把导入命令打出来。有书签但没有向量,就打 facetmark index。搜不到东西而抓取队列还排着,它会直接告诉你,而不是甩给你一个空列表让你猜。

中文和 English

顶栏一个开关,下次来还记得。浅色、深色,或者跟随系统。/ 聚焦搜索框,上下键走结果,Esc 清空。

从零开始上手 →

在浏览器里

一个只和 localhost 说话的扩展

Manifest V3。主机权限只有 http://127.0.0.1:8787/*http://localhost:8787/*。它只访问你自己的机器,用一个配对令牌握手,并且从不写你浏览器的书签库。

facetmark 弹窗搜索结果深色模式下的同一个弹窗
弹窗。每条结果都带着命中它的面,而同一次保存会话里的页面会单独成一组,不混进排名。这个画框跟着你正在看的这个页面切主题。
facetmark 设置页深色模式下的同一个设置页
设置页。端点、配对令牌、一个可选的第二通道,一个暂停开关。四个字段,没有账号。

结果行上的每个标记是什么意思

  • about命中了内容面:正文的向量。唯一一个默认开着的面。
  • asked as命中了意图面:为这个页面生成的问句的向量。默认关闭。
  • words命中了词面 · 分词面:FTS5,按词切。默认关闭。
  • substring命中了词面 · 三元组面:FTS5,按字符切。默认关闭。
  • cold链接看起来已经死了,这一行只降权,不删。为什么,问 facetmark health
  • saved around these单独的第二组,沿会话边和语义边走一跳得到。永远不混进上面的排名。

这些是用 mock 数据渲染的界面预览,不是真实库的截图 —— 真的截一张,等于把某个人的浏览历史贴到公开网页上。

证据

四个功能被实测,四个都输了。它们现在是关的。

这个项目有意思的部分不是那些成功的功能,而是那些写完了、预注册了、测完了、然后被关掉的 —— 包括一个已经发出去的。

0.643
479 条真实查询、单一个面的 Recall@5
−5.4pp
四个面全打开的代价
−18.83pp
已经发出去的情景门的代价

W1 · 各档 Recall@5,479 条查询,一个真实库

A 只用内容向量
0.643
B +两个词面
0.589
C 四个面全上
0.635
D +上下文+图
0.639

三条标准在跑之前就写好了。三条全没达到。融合付出了 5.4 个百分点的 Recall@5,并且把查询变慢了 3.5 倍 —— p50 从 148 ms 到 526 ms。四面融合的默认当天就撤了。

那一轮里活下来两个,现在都在跑:图扩展作为单独一组返回(+2.09pp,10 胜 0 负,p=0.0019),以及重排对 Recall@1 的提升(+4.80pp,CI95 [+1.46, +8.35])。

然后是情景门。它在自己的 holdout 上赢了(+3.09pp,19 胜 0 负,p=3.8e−6)并且发了出去。之后另建的 361 条探针集问了另一个问题 —— 它在不该触发的查询上触发了会怎样?答案是 −18.83pp,3 胜 71 负。默认回滚了。

看完整的九个结果 →

接口

六种用法,同一个索引

本地页面

facetmark serve 会在 /app 上开一个搜索页。搜索加书签库概览,中英文、深浅色都能切。这是唯一一个除了 facetmark 本身什么都不用装的入口。

页面上有什么 →

命令行

16 条命令。search--explain 可以打印命中的是哪个面,--config 可以按名字跑任何一个消融档。

命令参考 →

HTTP API

facetmark serve 监听 127.0.0.1:8787。27 条路由,其中四条公开 —— 根路径、健康检查,以及本地页面加载自己需要的那两条;凡是碰到书签库的都要配对令牌。

路由与鉴权 →

MCP 服务器

facetmark mcp 在 stdio 上说 MCP。9 个工具、3 个资源,Claude Desktop 可以直接搜你的库、读一次保存会话。

客户端配置 →

浏览器扩展

MV3。地址栏关键字 fmCtrl+Shift+K、一键保存并进本地索引队列。

安装与配对 →

karakeep 插件

一个搜索提供者插件,把 facetmark 接到 karakeep 自己的搜索框后面。协议格式由一个回放测试钉死。

怎么接 →

常见问题

真的有人问的那六个

有东西被上传吗?

两件东西会离开你的机器,两件都归你控制。页面抓取会去你自己存的网站。富化和嵌入会去你自己配的那个 OpenAI 兼容端点 —— 可以是 OpenAI,也可以是你桌子下那台机器。

FACETMARK_EMBED_BACKEND=local 打开、FACETMARK_API_KEY 留空,除了抓页面之外就什么都不出去了。没有一个叫 facetmark 的服务器在等着收数据。也没有账号。

会动我浏览器里的书签吗?

不会。导入是单向只读。导入器打开浏览器配置里的 Bookmarks 文件或者你导出的 HTML,读完就关。代码里没有任何一处往浏览器配置里写东西。

facetmark 这边也不删。衰减层只会把陈旧页面往后排,从来不删行。

完全不用大模型能用吗?

能,会差,而且它会告诉你差在哪。不接模型,你保留两个词面、整套保存会话和域名图。你失去内容面 —— 就是测得最好的那个 —— 和意图面。

折中方案:只跑一个本地嵌入模型支撑内容面,不接 chat 模型。你失去摘要和生成意图,保住换说法搜索。

建一次索引要多少钱?

钱主要花在富化:大致每页一次小的 chat 调用。用便宜模型的话,1,700 页是几毛钱的事。嵌入更便宜,本地跑就是免费。

但壁钟时间的大头是抓页面,不是模型。facetmark 遵守 robots.txt,并且按域名给自己限速 —— 这是故意的。--no-fetch 只索引标题,几秒钟就完。

为什么默认只开一个面?

因为四面融合在 479 条真实查询上被测了,结果比单独用内容面 5.4 个百分点的 Recall@5,延迟还高 3.5 倍。

机制也写下来了:平权重的 RRF 下,两个弱面碰巧的一致(0.0279)能投赢一个强面的确定(0.0164)。四个面都还在,都还有测试。--config C 一下就全打开了,你可以自己看。

这是一个产品吗?

不是。它是一个带着评测台架的工具,而台架才是重点。每一个改过的默认值背后都有一份协议、一条预注册的标准和一个置信区间,而其中四份协议杀掉了它们自己要证明的那个功能。

最大的缺口写在实测页上:到目前为止所有查询集都是作者自己写的 —— 这是再多 bootstrap 也修不了的那一个偏差。

边界

这个东西拒绝做的事

  • 对你的浏览器只读

    导入从不写回。你的文件夹树还是你的。

  • 什么都不删

    冷层只降权。它不删行,而且 facetmark health 会告诉你它认为哪些死了、为什么。

  • 本地优先

    一个 SQLite 文件,任何 SQLite 工具都能打开。就算你不用 facetmark 了,数据也还读得出来。

  • 默认就很礼貌

    遵守 robots.txt,单域名并发封顶 2,同一主机两次请求之间有最小间隔,UA 里写清楚自己是谁。

  • 没有协议就不报数字

    没有一套提前冻结的查询集,就不改默认值。

四条命令开始,或者先把数字看完。

指南覆盖安装、四个浏览器的导出、两种模型接入方式、扩展、MCP 和 karakeep 插件。实测页覆盖项目里每一条检索结论,包括输了的那几条。