命令与接口参考

按任务查找导入、检索语法、分页、HTTP API、扩展和配置项。首次使用建议先完成快速上手,再回到这里查具体参数。

安装#

Python 3.10 以上,Windows / macOS / Linux 都行。基础安装没有任何需要编译的机器学习依赖;向量检索来自 sqlite-vec,它是一个 SQLite 扩展。

shell
pip install facetmark
# 或者用 uv:
uv pip install facetmark

facetmark version

带本地嵌入

只有你想在自己机器上算嵌入、而不走端点时才需要。它会拉 PyTorch 和 sentence-transformers,几百 MB。

shell
pip install "facetmark[local]"

从源码装

shell
git clone https://github.com/88lin/facetmark
cd facetmark
python -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"

pytest -q                 # 1,700+ 个测试
ruff check src tests scripts
不要格式化代码库

它是手写排版的。CI 跑的是 ruff check,不是 ruff format;跑后者会生成一份没人想 review 的 diff。

数据存在哪

一个目录,按平台选,里面一个 SQLite 文件。用 FACETMARK_DATA_DIR 换目录,或者用 --db 给单条命令指一个文件。

平台默认数据目录
Windows%LOCALAPPDATA%\facetmark\
Linux / macOS~/.local/share/facetmark/
设了 XDG_DATA_HOME 时$XDG_DATA_HOME/facetmark/

数据目录通常包含数据库、配对令牌及可选的配置文件。备份时分别保管数据库与密钥;路径以 facetmark stats 和 facetmark config path 的输出为准。

没 key 没网也能先试

facetmark demo 会造一个合成库,用确定性的离线 provider 建索引,然后跑三条搜索。首页那个终端就是这么录的。

shell
facetmark demo --size 60

另见: 快速上手

把书签导进来#

导入是单向只读的。facetmark 读浏览器配置或者导出文件,两者都不写。

Chromium 系:不用导出

Chrome、Edge、Brave、Vivaldi、Chromium、Opera 和 Opera GX 都把书签放在一个 JSON 文件里,facetmark 能自己找到。浏览器开着也能安全读。

shell
facetmark browsers        # 它能看到哪些
facetmark import          # 只有一个时直接导

装了多个配置时,它不猜 —— 导错人的书签比多敲一条命令糟糕得多。它会把候选列出来,你自己指:

shell
facetmark import "$HOME/.config/google-chrome/Default/Bookmarks"

Firefox 和 Safari:先导出 HTML

浏览器导出入口
Firefox书签 → 管理书签 → 导入和备份 → 将书签导出为 HTML
Safari文件 → 导出 → 书签
Chrome / Edge(手动路线)chrome://bookmarks → ⋮ → 导出书签
其他任何 Netscape 格式的 bookmarks.html 都行。这是 1994 年的格式,到今天所有人还在写它。
shell
facetmark import ~/Downloads/bookmarks.html

导入会报什么

同一条命令同时处理 Netscape HTML 和 Chrome JSON,并且报它干了什么,而不是转一个圈。在一份真实的 1.7 MB 导出上(96 个文件夹、四层嵌套):解析 1,710 条,写入 1,701 条,合并 9 条重复,1 条不可索引。

字段含义
parsed文件里找到的条目数。
inserted / updated新写入的,以及标题或文件夹变了的。
merged_duplicates同一个 URL 存了两次,取时间早的那个。
non_indexablejavascript:、place:、file: 之类。
missing_dates没有保存时间的。照样导入,但进不了保存会话。
privacy_skipped被 FACETMARK_PRIVACY_EXCLUDED_DOMAINS 挡下的。
timestamp_unit源文件用的是哪种时间戳。Chrome 和 Netscape 不一样,这里告诉你识别出的是哪种。
先排除域名,再导入

把 FACETMARK_PRIVACY_EXCLUDED_DOMAINS 设成逗号分隔的列表,这些主机就不会被写入、不会被抓、也不会被嵌入。比事后删行省事。

另见: 快速上手

模型接入#

在线模型共用一个 OpenAI 兼容的 base_url 和 api_key。确认端点同时提供需要的对话与向量能力;兼容网关、自托管服务或本地向量后端可以补足服务商缺少的能力。

对话模型生成摘要、主题、实体、要点和候选问题;向量模型把页面文本与查询转换为向量。没有正文时,部分衍生内容可能依据标题生成。

走端点

shell
export FACETMARK_API_KEY=sk-...
export FACETMARK_BASE_URL=https://api.openai.com/v1
export FACETMARK_CHAT_MODEL=gpt-6-luna
export FACETMARK_EMBED_MODEL=text-embedding-3-small
export FACETMARK_EMBED_DIM=1536
使用服务商的 API 根路径

这是最常见的配置失败,没之一。少了 /v1,每一次调用都 404,包括第一次;而错误是从 provider 那边回来的,看起来像是密钥问题。

不想设环境变量,可以在运行目录放一个 .env。名字一样,前缀一样。

dotenv
FACETMARK_API_KEY=sk-...
FACETMARK_BASE_URL=https://api.deepseek.com/v1
FACETMARK_CHAT_MODEL=deepseek-flash

共享端点或免费端点

备用对话模型按配置顺序尝试。请先确认每个模型在当前账户和端点可用;不要用备用链掩盖错误的接口地址或权限。

shell
export FACETMARK_CHAT_MODEL_FALLBACKS=deepseek-flash,deepseek-v4-pro

provider 会记录每一次调用到底是哪个模型答的。任何建立在降级链上的报告,都必须把这个混合比例公开。

本地嵌入,不要 key

本地向量使用 sentence-transformers。首次下载模型后可在服务所在机器计算;网页抓取仍会联网。若保留在线对话配置,摘要与综述仍可能调用该服务。

shell
pip install "facetmark[local]"

export FACETMARK_EMBED_BACKEND=local
export FACETMARK_EMBED_MODEL=bge-m3
export FACETMARK_EMBED_DIM=1024
export FACETMARK_LOCAL_EMBED_PATH=BAAI/bge-m3   # 或本地模型目录
export FACETMARK_LOCAL_EMBED_MAX_SEQ=1024
为什么序列长度默认是 1024

同一篇文档嵌入两次,必须落在同一个地方。bge-m3 在 1024 token 下,一个固定的 64 篇探针集上最小自余弦是 0.999976,64/64 全部自匹配。降到 512 token,最小值掉到 0.9769 —— 因为截断开始从同一段文本上剪掉不同的量。所以默认是 1024,而且调低它是一笔真的交易。

改维度等于作废所有向量

FACETMARK_EMBED_DIM 在第一次建索引时写进 meta 表。之后对不上就报错,而不是默默把不兼容的向量混在一起。换嵌入模型或换维度,请用 facetmark index --force 重算。

一个模型都不接

照装照跑。你保留两个词面、保存会话、域名与链接图、链接健康。你失去内容面和意图面。facetmark search --quick 是明确的纯词面路径,一次模型调用都不发。

另见: 模型与配置

建索引#

shell
facetmark index

索引按阶段执行,并通过输入指纹复用已完成的工作。新增书签或正文、模型配置变化时,相应阶段需要更新;请查看实际任务输出。

阶段干什么要模型吗?
fetch抓页面,遵守 robots.txt 和单域名限速,抽取可读正文。不要
enrich摘要、主题、实体、要点 —— 每页一次小的 chat 调用。chat
embed_content把重建后的文本嵌入。嵌入
intents为每页生成候选查询。chat
filter_intents只留下能把这页搜回来的那些。典型情况下不到一半能活。不要
embed_intents把存活下来的意图嵌入。嵌入
sessions按时间间隔把保存行为聚成一次次会话,间隔选哪个由「覆盖率 × 相对打乱对照的纯度提升」决定。不要
edges建会话边、语义边、同域名边和替代边。不要

常用参数

参数作用
--no-fetch完全不抓网,只索引标题。几秒钟而不是几小时,效果也弱很多。
--limit N每阶段只处理 N 条。适合先看一眼这一跑到底要多少钱。
--force不看指纹,已经做过的也重做。
--mock确定性离线 provider。不要 key、不联网、也没质量。
--json每个阶段的机器可读报告,含各阶段耗时。

指纹是怎么算的

  • 富化按正文哈希。正文没变,就不发第二次 chat 请求。
  • 嵌入按重建后的嵌入文本,而不是按正文。所以富化变了、嵌入文本跟着变了,那个陈旧向量会被发现 —— karakeep 往返的损伤就是这么插出来的。
  • 会话和边每次重建;它们便宜,而且依赖整个库。

facetmark reindex 把所有衍生产物丢掉,从书签本身重建。facetmark migrate 把旧库升到当前 schema,默认先快照一份,除非你加 --no-backup。

这一跑的代价

费用由文本长度、模型和服务商定价决定,建议先处理小样本。耗时还受网页抓取和站点限流影响;默认单主机并发为 2,并保留请求间隔。

给个量级感:刚才那个真实的 1,700 条书签库,用 --no-fetch 建索引,得到 322 个保存会话、9,132 条边、1,386 个域名、1,775 个向量。

另见: 快速上手

查询语言#

所有检索入口用的是同一套语法:网页搜索框、facetmark search、/search 与 /quick、MCP 工具、karakeep 插件。它是一门过滤语言,不是第二个排序器——过滤器只决定哪些页面有资格,从不改动幸存页面的分数。完整参考:docs/query-language.md。

shell
facetmark search "postgres domain:github.com -title:tutorial"
facetmark search "kafka added:<7d"          # 这周存的
facetmark search 'title:encryption (signal|matrix)'
facetmark search "tag:work sort:date"       # 一次浏览,按时间倒序
字段匹配什么例子
domain: 别名 site:站点,精确或通配domain:github.com
host:完整主机名host:news.ycombinator.com
url:地址的一部分url:*/docs/*
title:只在标题里title:encryption
text:抓下来的正文里text:"GDPR compliance"
folder:来自哪个浏览器文件夹folder:study
tag:你自己的标签,精确匹配tag:work
topic:富集写出来的主题topic:postgres
lang:检测到的页面语言lang:zh
opened:你打开过几次opened:10..

否定、短语、多选、通配

shell
-facebook                    排除一个词
-domain:pinterest.com        排除整个站
"consumer group rebalancing"  精确短语
(security|privacy)           两个词任一
domain:(github.com|gitlab.com)   两个值任一
domain:*.github.io           * 是任意长度的一段字符

日期

写时长时比的是书签的年龄,所以 added:>90d 是「存了 90 天以上」。写绝对日期时直接比时间戳,所以 added:>=2026-04-01 是「那天及之后存的」。两者方向相反,因为对各自的写法来说,这都是唯一自然的读法。

写法含义
added:<7d最近一周存的
added:>90d存了 90 天以上
added:2026-04那个月存的
added:2026-04-01..2026-09-01一个显式区间
before:2026-05-01 after:30dadded: 的别名;这两个上的时长按年龄读,所以 after:30d 是最近 30 天

排序,以及什么叫一次浏览

sort:date 最新在前,sort:-date 最旧在前;也支持 title、domain、url 和 opened。只有过滤器或排序指令、没有自由文本时,查询直接浏览书签,不调用模型。纯词面搜索和本地向量搜索也不产生在线向量费用。

不带语法的查询行为不变

解析器只在一个 token「不可能是纯文本」时才把它当语法。note: something 不是过滤器,因为 note 不是字段;state-of-the-art 不是三个否定;https://example.com/x 是一个完整的词。而解析不了的值——比如 added:90d,一个没有比较符的时长——会出现在响应的 filters.ignored 里,既不悄悄生效,也不悄悄丢掉。

你自己的标签

Netscape 与 pinboard 导出带着 TAGS 属性,现在它被保留下来:存在书签上、随每条命中返回、并且可以用 tag:work 查询。匹配的是列表里完整的一个元素,所以 tag:work 不会扩到 workshop;要匹配多个用 tag:(work|rust)。POST /bookmark 和 MCP 的 save_bookmark 也接受 tags;重复导入同一个文件时标签取并集而不是覆盖。

起服务:HTTP API 与配对令牌#

shell
facetmark serve        # 127.0.0.1:8787
只听回环地址不算鉴权

除了 /、/health,以及本地页面加载自己用的 /app 和 /app/boot,每条路由都要令牌,在 localhost 上也要 —— 因为你机器上任何一个进程都能访问 127.0.0.1。--host 不是回环地址时,facetmark serve 会告警:这个索引里是你整个浏览兴趣图谱。

令牌

令牌首次运行时生成,保存在数据目录的 pairing-token.txt。请求可使用 Authorization: Bearer <token> 或 x-facetmark-token。不要把令牌放入公开链接或日志。

shell
facetmark token             # 打印
facetmark token --rotate    # 作废旧的
shell
TOKEN=$(facetmark token)

curl -s http://127.0.0.1:8787/health

curl -s -X POST http://127.0.0.1:8787/search \
  -H 'content-type: application/json' \
  -H "x-facetmark-token: $TOKEN" \
  -d '{"q":"vectors inside sqlite","limit":5}'

POST /search

字段类型含义
qstring查询。必填。
limitint返回条数。
configstringprofile 或档位名。"" 和 "full" 都走 default_config。
assistbool允许模型参与的理解阶段。
expandbool同时返回一跳图扩展那一组。

全部路由

分组路由
公开GET / · GET /health
本地页面 —— 同样公开GET /app · GET /app/static/* · GET /app/boot
搜索GET /stats · GET /quick · POST /search · POST /suggest · POST /synthesize
记录GET /bookmark/{id} · GET /bookmark/{id}/related · POST /bookmark · POST /open
会话GET /sessions · GET /session/{id}
索引队列GET /queue/next · POST /queue/complete · GET /queue/stats
链接健康GET /link-health/summary · GET /link-health/{id} · POST /link-health/check · GET /graveyard
karakeep 桥POST /karakeep/documents · POST /karakeep/documents/delete · POST /karakeep/search · POST /karakeep/clear · GET /karakeep/stats
管理接口

管理接口还检查连接来源,仅允许回环连接,并可通过 FACETMARK_ADMIN_API=false 关闭。远程管理请使用 SSH 转发步骤。

本地页面#

facetmark serve 同时提供 /app 应用和 HTTP API。页面包含搜索、综述、书签库、浏览批次与系统状态,设置从齿轮入口进入。

shell
facetmark serve
# facetmark 2.0.0  http://127.0.0.1:8787
# open the search page:     http://127.0.0.1:8787/app
# pairing token written to the data directory
#   (facetmark config show lists the effective data_dir)

Python 包里的纯 HTML、CSS 和 ES 模块:没有 Node,没有打包器,也就没有会和服务端对不上的构建产物。页面和 API 由同一个进程发出,所以是同源的 —— 这也是它没法托管到别处去的原因:这个服务的 CORS 只对浏览器扩展的来源开放。

五个标签页与管理入口

视图地址干什么用
搜索/app#/search输入确认后先显示不调用模型的词面结果,再更新完整检索结果;中文选字期间暂停查询。用加载更多继续浏览。
综述/app#/ask输入问题并提交,根据已存摘要或片段生成带编号引用的回答。沿引用查看来源并核对原文。
书签库/app#/library查看收藏活动、正文与向量覆盖、浏览批次和关联规模。搜不到内容时,先确认书签是否导入、正文与索引是否齐全。
浏览批次/app#/sessions查看同一时间段保存的书签,进入某个批次继续阅读。
系统/app#/system查看服务与模型状态、抓取队列、链接健康和冷层清单。
设置(齿轮)/app#/settings测试并保存模型配置,调整索引选项,启动或取消索引任务。
首次设置/app#/setup按导入书签、选择模型、建立索引三步完成初始化;已有书签时也可再次进入。
哪些操作需要管理入口

配对后可以搜索、生成综述和查看书签。导入文件、保存配置与控制索引任务还要求本机连接或 SSH 转发。从搜索结果打开原文会记录阅读活动;搜索和详情页不提供书签编辑、删除操作。

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

和扩展弹窗用的是同一套词。在页面里,每个标记鼠标悬停都有一行解释;这张表是为了让你一次看全。

标记意思默认
内容相关命中了内容面 —— 已存内容的向量,可能来自正文或标题推断的摘要。开
提问方式命中了意图面 —— 给这个页面生成的问题的向量。关
词语命中了字面 · 分词面 —— 对标题、文件夹、网址里完整词的 FTS5。关
子串命中了字面 · 三元组面 —— 对字符的 FTS5,中文查询和只打了一半的词能命中,靠的就是它。关
已冷却很久以前存的,一直没打开过,而且有更新的东西看起来把它取代了。排名压低,绝不删除。开
当时前后一起存的第二组:从上面某条结果出发,在链接图上走一跳。绝不混进排名里。开

第二组里每一行都带着走到它的那条边 —— 同一次浏览(同一次上网时存的)、语义相近、已被取代、同一页面、同一站点。这些名字背后的权重在配置表里。

页面怎么拿到令牌

它去问 GET /app/boot。这是唯一一条能把配对令牌交出去的路由,而且只在调用方和请求里写的地址两者都是回环地址时才交。在你自己机器上两条都成立,页面就自己配对好了,没有什么要复制的。

第二个条件是干什么的

公网上的一个页面可以把某个域名解析到 127.0.0.1,然后让你的浏览器去发这个请求 —— 调用方确实是回环地址。但它改不了 Host 头,那里面还写着攻击者的域名。查这一项,才是拦住一个网站读走你令牌的东西;这也是为什么它是一条单独的路由,而不是挂在现有路由上的一个开关。

在反向代理后面,或者用局域网地址访问时,这个检查会不通过 —— 这是故意的:页面这时给你一个输入框,把 facetmark token 粘一次就行。它存在那个浏览器的本地存储里,不在页面里。

键盘

按键作用
/未在输入框内打字时,切换到搜索并聚焦查询框。
Enter提交查询;有选中建议时采用该建议。中文选字的确认回车不会提交。
↑ ↓建议列表打开时选择建议;否则在搜索结果之间移动。输入法选字期间保留给输入法。
Esc先关闭建议或详情;搜索页没有弹层时清空查询并聚焦搜索框。

语言和主题

中英文和主题偏好保存在当前浏览器的当前站点。官网与应用使用同名偏好键,但不同域名的浏览器存储不会自动同步。界面尊重系统的减少动态效果设置。

另见: 应用使用指南

翻页:limit、offset 和 depth#

每个搜索入口都收 limit、offset 和 depth,而每个搜索响应报的是它实际给出的那个窗口,不是把你要的原样回显。

shell
facetmark search "kafka rebalance" -n 20
facetmark search "kafka rebalance" -n 20 -o 20 --depth 60

只要还有下一页,CLI 就会把下一页的 --offset 和 --depth 打出来。走 HTTP 时,同样这三个字段放在 POST /search 的请求体里:

json
{
  "hits": [ ],
  "limit": 20,          // 实际给的,已经夹过
  "offset": 20,
  "depth": 60,          // 这次排名跑的深度
  "total": 137,         // 已经排过的条数;封顶时是下界
  "has_more": true,
  "depth_capped": false
}
字段含义
limit这一页的条数。会夹到 MAX_PAGE_SIZE,默认 200。
offset跳过的条数。会夹在 MAX_CANDIDATE_DEPTH 以下。
depth融合之前每个面各读多深。不填就按窗口推算;把上一页报的值原样送回来,这一页就接着同一次排名往下走。
total融合这一步排过的文档数。是个下界,不是书签库的总数;depth_capped 为真时更是明确只当下界看。
has_more这个窗口后面还有东西。在出厂的单面默认档下是准的;开了好几个面时是上界 —— 多出来的那一条有可能是候选池里已经有的文档。
depth_capped后面确实还有,而且停下来的原因是撞到了深度上限,不是你的窗口 —— 这是“点下一页”和“把深度调大,或者把查询收窄”之间的区别。

为什么 depth 是个参数,而不是实现细节

页面大小决定一次展示多少条,检索深度决定候选池范围。下一页沿用响应中的 depth,可以在同一个候选池上继续读取,避免翻页时改变排名依据。

钉住 depth,否则第二页会和第一页打架

只有在一个面的时候,RRF 才在候选池变大时保持名次稳定。一个文档的分数,是它在“深度以内排到了它”的那些面上求和,所以更深的池子可能凭空给某个文档补上一项 —— 而这一项可能压过对手的整个分数。在一个面上排第 2、在另一个面上排第 40,合起来赢过只在一个面上排第 1 的(1/62 + 1/100 对 1/61),但在深度 30 时后面那一项根本不存在。

所以开了好几个面时,为了翻到第 2 页而把深度加大,会让第 2 页对“第 1 页是什么”这件事和第 1 页产生分歧。解法不是加大它:把上一页报的 depth 原样送回来,每一页就都是同一次排名的一个切片。本地页面和浏览器扩展都是这么做的。

两个上限

MAX_PAGE_SIZE(200)限住一页。MAX_CANDIDATE_DEPTH(2000)限住它们背后的整个候选池,撞上它就是 depth_capped 被置上的原因。两个都在同一个地方、在任何查询开跑之前夹好,所以一个超大的请求不花什么代价,回给你的就是实际给出的那个窗口。

浏览器扩展#

Manifest V3,Chromium 系浏览器。它只和 127.0.0.1:8787 说话 —— 那就是它全部的必需主机权限。

  1. 从发行页下载 facetmark-extension.zip 并解压。
  2. 打开 chrome://extensions,开启开发者模式,选加载已解压的扩展,指向刚才那个目录。
  3. 在终端跑 facetmark serve 并保持运行。
  4. 跑 facetmark token,打开扩展的设置页,把令牌粘进去。
  5. 按 Ctrl+Shift+K(macOS 是 Cmd+Shift+K)就能搜了。

它能干什么

功能说明
地址栏关键字地址栏输 fm 再敲空格,不用开弹窗就能搜。
快捷键Ctrl+Shift+K / Cmd+Shift+K。
保存当前标签页一键。页面进本地索引队列,弹窗底部显示还剩几个。
右键菜单右键一个链接或页面就能存。
分组结果同一次保存会话里的页面单独成组,不混进排名。
面标签每条结果显示命中了哪些面 —— 关于、可能会问、词、子串、关联、冷。

设置项

字段含义
endpointfacetmark 监听在哪。默认 http://127.0.0.1:8787。
tokenfacetmark token 的输出。
channelB可选的第二个端点,用于同时跑两个库。
paused不卸载的前提下让扩展停止和服务通信。
不在应用商店里

扩展以 zip 形式放在发行页,需要解压后加载。它没有提交到 Chrome 应用商店或 Edge 加载项目录。

另见: 连接工具与备份

MCP 服务器#

facetmark mcp 在 stdio 上跑一个 FastMCP 服务器,所以 Claude Desktop 这类 MCP 客户端可以搜你的库、读一次保存会话、存一个页面。

json
{
  "mcpServers": {
    "facetmark": {
      "command": "facetmark",
      "args": ["mcp"]
    }
  }
}

在 args 里加 "--db", "/path/to/facetmark.db" 可以指定库,加 "--mock" 可以没 key 先试。环境变量的读法和其他命令完全一样。

9 个工具

工具作用
search_bookmarks完整管线,和 facetmark search 一样。
get_bookmark一条记录,可选带正文。
list_sessions最近的保存会话。
get_session一次会话里存的全部。
find_related在链接图里往外走一跳。
synthesize基于检索到的页面写一份回答。
suggest_from_context你正在看的这段文字,库里有什么相关。
check_link_health一个存过的 URL 还活着吗。
save_bookmark加一个 URL 并排队索引。

3 个资源

  • bookmark://{id} —— 一条记录的 JSON。
  • session://{id} —— 一次保存会话。
  • facetmark://stats —— 索引规模与覆盖率。

另见: 连接工具与备份

karakeep 插件#

karakeep 是一个可自托管的书签管理器,搜索提供者可插拔。这个插件把 facetmark 接到它的搜索框后面:karakeep 管界面,facetmark 管检索。

  1. 把插件拷进 karakeep 的插件包。
  2. 在 exports 映射里注册它。
  3. 在 meilisearch 之后加载,因为插件管理器发出去的是最后注册的那个提供者。
  4. 把它指向一个在跑的 facetmark 服务。
shell
cp -r integrations/karakeep/search-facetmark \
  /path/to/karakeep/packages/plugins/search-facetmark
json
// packages/plugins/package.json — exports 映射
"./search-facetmark": "./search-facetmark/index.ts"
ts
// packages/shared-server/src/plugins.ts 的 loadAllPlugins()
await import("@karakeep/plugins/search-meilisearch");
await import("@karakeep/plugins/search-facetmark");  // 必须在后面
shell
export FACETMARK_URL=http://127.0.0.1:8787
export FACETMARK_TOKEN=$(facetmark token)
facetmark serve

协议是怎么钉住的

  • karakeep 上游的类型按 blob SHA 钉在 integrations/karakeep/typecheck/upstream-pins.json,CI 会对着它跑 tsc --noEmit。
  • 报文格式存在 integrations/karakeep/contract/wire.json,由 tests/test_karakeep_contract.py 回放。
  • 这个回放测试真的接住了一个:只有一条命中时的 offset 1,正确答案是 hits: [] 配 totalHits: 1。空的 hits 不等于没有结果。
指望它之前要知道两件事

第一,没有针对真实在跑的 karakeep 实例的测试 —— 只有对钉死协议的。第二,把库推进 karakeep 再读回来,排名会变:karakeep 的 tag 就是你浏览器的文件夹名,所以关键词从 19,016 个不同词塌到 13 个。指标层面的结论能过往返,名次层面的不能,除非重建索引。完整实测。

想卸掉这座桥,把 karakeep_doc 表 drop 了就行。enrichment.source_hash == 'karakeep' 是保留值,意思是这行桥可以覆写;其他任何值都意味着是真模型写的,桥不碰。

另见: 连接工具与备份

数据库里有什么#

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

表存什么
bookmarkURL、标题、文件夹路径、保存时间、来源。
content抓回来的正文和抽取结果。
enrichment摘要、主题、实体、要点,以及 source_hash 指纹。
intent生成的候选查询,以及它有没有过了「能搜回来」的过滤。
vec_content / vec_intentsqlite-vec 虚拟表,存稠密向量。
fts_tri / fts_seg两个 FTS5 索引:字符三元组和词段。
session / bookmark_session重建出来的保存会话及其成员。
edge带类型的边:session、semantic、same_domain、supersession。
health链接健康结论:ok、gone、drifted、soft_gone。
karakeep_doc桥的状态。drop 掉就是卸载。
meta嵌入模型、维度、后端,第一次建索引时写入,之后强制校验。

链接健康与冷层

shell
facetmark health                       # 已知的
facetmark health --check               # 真的去探网络
facetmark health --check --no-save-recovered   # 只读扫描

扫描可以用 DNS-over-HTTPS、Wayback 可用性 API 和一个阅读代理,来区分「页面没了」和「你 DNS 坏了」。在拿这个库做任何测量之前,请加 --no-save-recovered,让扫描除了健康日志之外保持只读。

一个已知的、而且承重的 bug

冷层把「URL 死了」当成「存下来的副本没用了」,这是错的:facetmark 存了正文。URL 死了恰恰是本地快照最值钱的时候。现在还没修,因为在出厂 profile 下另一个意外让这个降权根本没机会执行,而只拆掉其中任一个,结果会实测变差 1.46pp。完整的故事。

全部配置项#

作为环境变量时,每个名字前面加 FACETMARK_;放在 .env 里也一样。下面的默认值就是出厂值。

存储

配置项默认说明
DATA_DIR按系统见安装。
DB_NAMEfacetmark.db
PRIVACY_EXCLUDED_DOMAINS空不导入、不抓、不嵌入。

模型接入

配置项默认说明
API_KEY空空是合法的,代价是失去内容面和意图面。
BASE_URLhttps://api.openai.com/v1通常以 /v1 结尾;智谱使用 /api/paas/v4。
CHAT_MODELgpt-6-luna
CHAT_EXTRA_BODYJSON 对象字符串;可设置思考和输出上限。留空不强制采样参数。
EMBED_SEND_DIMENSIONSfalsedimensions
EMBED_BATCH_SIZE64百炼使用 20。
CHAT_MODEL_FALLBACKS空逗号分隔。默认为空是故意的。
EMBED_MODELtext-embedding-3-small
EMBED_DIM1536写进 meta,对不上就报错。
EMBED_BACKENDendpoint或 local。
REQUEST_TIMEOUT60.0秒。
MAX_RETRIES3
USE_MOCK_PROVIDERfalse确定性离线 provider。

本地嵌入

配置项默认说明
LOCAL_EMBED_PATH空空就下载。
LOCAL_EMBED_DEVICEcpu
LOCAL_EMBED_BATCH8
LOCAL_EMBED_MAX_SEQ1024调低会损失可重现性 —— 见模型接入。

抓取

配置项默认说明
FETCH_CONCURRENCY30全局。
FETCH_PER_HOST_CONCURRENCY2礼貌,不是性能。
FETCH_PER_HOST_MIN_INTERVAL0.5同一主机两次请求的秒数间隔。
FETCH_TIMEOUT15.0
RESPECT_ROBOTStrue
ROBOTS_ON_ERRORallowrobots.txt 读不到时怎么办。
ROBOTS_MAX_CRAWL_DELAY5.0对对方声明的 crawl delay 封顶。
MIN_BODY_CHARS200低于此数算无正文。
BODY_TRUNCATE_CHARS6000
USER_AGENT写明自己是 facetmark

富化与意图

配置项默认说明
ENRICH_CONCURRENCY4
INTENT_GENERATE_N8每页生成多少候选。
INTENT_KEEP_N4每页最多留多少。
INTENT_PROBE_TOP_K10「能不能搜回来」看多深。

会话、检索与衰减

配置项默认说明
SESSION_EPS_MINUTES自动不设时,间隔由覆盖率 × 纯度提升在网格上选。
SESSION_EPS_GRID_MINUTES5…240搜索的网格。
RRF_K60w / (k + rank) 里的 k。
CANDIDATES_PER_FACET50
GRAPH_EXPAND_HOPS1
GRAPH_EXPAND_FACTOR0.6
DECAY_FACTOR0.5
DECAY_AGE_DAYS365
DECAY_RESCUE_THRESHOLD0.02改它之前先看衰减层实测。

链接健康与服务

配置项默认说明
HEALTH_ENABLE_EXTERNALtrue网络探测总开关。
HEALTH_ENABLE_DOHtrueDNS-over-HTTPS。
HEALTH_ENABLE_WAYBACKtrue
HEALTH_ENABLE_READERtrue
HEALTH_SOFT_GONE_LENGTH_RATIO0.30正文缩到这个比例 ⇒ soft_gone。
HEALTH_GONE_CONFIRM_DAYS7
HEALTH_PROXY_URL未设
HOST127.0.0.1
PORT8787

另见: 模型与配置

全部命令#

使用 facetmark COMMAND --help 查看每条命令支持的参数。数据库相关命令通常支持 --db,许多命令提供 --json 便于脚本处理。

命令作用值得一提的参数
version打印版本。
browsers列出可导入的活浏览器配置。--json
import [PATH]导入 Netscape HTML 或 Chrome JSON。不带路径时自动找活配置。从不写回。
migrate把 schema 升到当前构建需要的版本。--check、--no-backup
index抓取、富化、嵌入、意图、会话、边。--no-fetch、--limit、--force、--mock
reindex从书签重建所有衍生产物。--mock
search QUERY搜库。-n、--quick、--config、--explain
show ID把一条书签打成 JSON。--body
sessions列出保存会话。-n
health链接健康,以及衰减层到底看不看得见它。--check、--no-external、--no-save-recovered
stats索引规模与覆盖率。
export [FILE] [QUERY]把库、或者一条过滤查询选中的那部分,写成 import 能读回来的 JSON。只接受过滤器——排序结果的前几条不是备份。派生数据不写进去,index 会重建。--full
doctor诊断这套装置——配置以及每一项设置来自哪里、schema 版本、索引有没有建起来、已存的向量和设置对不对得上。不修任何东西,也不调用模型;每一条发现都写清楚该跑哪条命令。--json
token打印扩展要的配对令牌。--rotate
serve跑本地 HTTP 服务。--host、--port、--mock
mcp在 stdio 上跑 MCP 服务器。--mock
crawl URL礼貌地把一个站点走进库里:遵守 robots.txt,隐私排除名单上的主机完全不碰,抓到的每一页都是一条普通书签。--max-pages、--off-domain
update报告 PyPI 上有没有更新的 facetmark。只在你运行它的时候查——没有后台检查、没有遥测——而且它自己从不执行升级。--json
demo离线造一个合成库并搜它。--size、--keep
config path / config show`config.toml` 在哪,以及生效的设置和每一项的来源。
eval跑检索评测,可以是 A–E 消融。--ablation、--rungs、--queries、--bootstrap、--out

跑你自己的评测

评测命令接受包含 {text, qtype, target_url} 的 JSONL 查询集,在指定书签库上比较检索配置,并输出置信区间与配对检验。先固定数据集和协议,再解释结果。

shell
facetmark eval --no-build \
  --queries my-queries.jsonl \
  --rungs A,C,full \
  --bootstrap 10000 --concurrency 4 \
  --out report.json
并发会摧毁延迟数字

--concurrency > 1 会让 p50 和 p95 失去意义。要质量数字时用它,要延迟就抽一个子集在并发 1 下重跑。

另见: 连接工具与备份

排错#

每次模型调用都返回 404

核对服务商要求的完整 base URL,包括路径前缀(常见为 /v1),并确认模型名称可用。404 可能来自地址或模型名,不能单凭它判断 API Key 无效。

建索引或搜索时报「维度不匹配」

数据库记录的向量维度与 FACETMARK_EMBED_DIM 不一致。先确认模型的实际输出维度,修正配置并重启服务,再重建索引。修改前保留数据库备份。

富化静悄悄地什么也没做

存着的 source_hash 已经等于当前正文哈希,指纹认为活干完了。这是正确行为,facetmark index --force 可以覆盖它。

向量有,但结果很差

通常是向量写完之后嵌入文本又变了 —— 比如富化被一座桥覆写了。用 facetmark index --force 重算。如果是一个全新的索引就差,先确认自己是不是不小心跑在 mock provider 上:facetmark stats 会报当前用的嵌入模型。

SQLite 报 disk I/O error

SQLite 在某些网络文件系统和 FUSE 上跑不稳。用 FACETMARK_DATA_DIR 把数据目录换到本地磁盘。

抓得很慢,或者页面是空的

两个通常都是故意的。遵守 robots.txt,单主机并发封顶 2,两次请求之间还有最小间隔。有些站就是不给。没正文的页面照样建索引 —— 管线会退到只用标题的指纹 —— 只是弱一些。想要快而浅的索引,用 --no-fetch。

扩展连不上服务

按顺序查三件事:facetmark serve 真的在跑吗;设置里的 endpoint 和它实际监听的主机端口对得上吗;设置里的令牌和 facetmark token 一致吗。轮换过令牌的话,扩展要拿新的。

其他

facetmark stats 和 facetmark health 会把索引里到底有什么打出来,大部分困惑到这就解了。实在不行就提个 issue —— 最有用的是把出错那条命令的 --json 输出贴上来。

另见: 快速上手