Karakeep(Hoarder)架构解析:Next.js 前端 + SQLite 任务队列 + 多 Worker 异步流水线
【免费下载链接】hoarderA self-hostable bookmark-everything app (links, notes and images) with AI-based automatic tagging and full text search项目地址: https://gitcode.com/GitHub_Trending/ho/hoarder
Karakeep 是一款可自托管的"收藏一切"应用(链接、笔记与图片),其系统架构采用"单体 Web 应用 + 异步任务队列 + 专用 Worker 工作进程"的分层设计:Web 应用负责数据持久化与用户交互,SQLite 任务队列承载所有后台作业,而 Crawling、OpenAI 推理与 Meilisearch 索引三类 Worker 分别完成内容抓取、自动打标签与全文检索建索引。阅读本文后,你将理解 Karakeep 从"保存一个链接"到"链接可被全文搜索"的完整数据流转链路,掌握其三大核心组件的职责边界、队列语义与部署形态,并能在源码层面定位每一步的实际实现。
架构总览:一条流水线上的三个角色
原版架构文档(docs/versioned_docs/version-v0.29.0/07-development/04-architecture.md)用三句话勾勒了系统骨架:
- Web 应用(Webapp):基于 Next.js 构建,使用 SQLite 存储数据;
- Worker 工作进程:从基于 SQLite 的任务队列中消费作业并执行,共有三种作业类型——抓取、OpenAI 推理、索引;
- 三者协同:用户通过 Web 应用产生收藏行为,系统把重活(抓网页、跑 AI、建索引)异步下沉到队列,由 Worker 逐个消化。
这套设计与 Karakeep 的"收藏一切"定位高度契合:用户保存的内容形态多样(链接、笔记、图片),而链接需要爬取正文、生成截图、推断标签、全文检索,这些都属于耗时操作,绝不能阻塞用户界面。把 Web 应用与 Worker 分离,既保证了前端响应速度,也让每一类重活可以独立扩缩容。
核心组件一:Web 应用(Next.js + SQLite)
Web 应用是整个系统的入口,负责所有用户交互与数据落盘。文档明确它的两个技术要点:
- Next.js:提供前端渲染、页面路由与 API 层。项目中的 Web 前端代码位于 apps/web,包含仪表盘(dashboard)、阅读器(reader)、设置(settings)等页面,并统一通过 tRPC 路由(packages/trpc/routers)暴露后端能力;
- SQLite:作为主数据库,承载书签、链接、标签、列表等全部业务数据。数据库的 schema 定义在 packages/db/schema.ts,迁移脚本位于 packages/db/drizzle,通过 Drizzle ORM 访问。
从部署配置(docker/docker-compose.yml)可以看到,web服务默认将数据目录挂载为/data(DATA_DIR: /data),SQLite 数据库文件即存放于此。除了业务数据,任务队列本身也落在 SQLite 中,这正是下节要展开的关键设计。
核心组件二:基于 SQLite 的任务队列
架构文档特别强调 Worker 消费的是SQLite 任务队列,而不是 Redis 等独立中间件。这一点在源码中得到印证:
- 队列抽象接口定义在 packages/shared/queueing.ts,包含
Queue(enqueue/stats/ensureInit)与Runner(run/stop)等核心契约; - 默认的 SQLite 队列实现是 liteque 插件(packages/plugins/queue-liteque/src/index.ts):它在
dataDir/queue.db中建表存任务(buildDBClient(path.join(serverConfig.dataDir, "queue.db"), ...)),并封装了createQueue、createRunner与重试语义; - 所有队列在 packages/shared-server/src/queues.ts 中统一注册,每个队列都通过 Zod schema 校验作业载荷,并配置独立的
numRetries重试次数与keepFailedJobs策略。
选用 SQLite 做队列意味着部署时无需额外引入 Redis,一个数据文件即可同时承载业务数据与后台任务,大幅降低了自托管门槛。队列插件的可替换性则由 packages/shared/plugins.ts 的插件机制保证——仓库中还提供了基于 Restate 的queue-restate插件(packages/plugins/queue-restate),满足需要更强队列能力(如分布式、持久化事件流)的部署场景。
核心组件三:Worker 工作进程
文档列出的三类作业在 apps/workers/index.ts 的workerBuilders中都有对应实现,每个 Worker 通过createRunner绑定到专属队列,并受concurrency(并发数)、pollIntervalMs(轮询间隔,默认 1000ms)与timeoutSecs(超时)三个运行参数约束。
1. Crawling Worker:无头 Chrome 抓取链接内容
抓取作业从link_crawler_queue队列消费,载荷结构为{ bookmarkId, runInference?, archiveFullPage?, storePdf? }(见 packages/shared-server/src/queues.ts)。其核心实现位于 apps/workers/workers/crawlerWorker.ts:
- 无头 Chrome:Worker 使用部署在同一容器环境中的 headless Chrome(
BROWSER_WEB_URL: http://chrome:9222)真实执行页面 JS、渲染截图,保证抓取到的是浏览器视角的完整内容,而不是简单 HTTP 拉取; - 内容探测与类型分发:抓取前先探测 URL 的 content-type 与元数据(
getContentTypeAndMetadata)。若目标为 PDF 或图片,则转为资产书签处理(handleAsAssetBookmark),否则走完整网页抓取流程crawlAndParseUrl; - 域名限流:通过
checkDomainRateLimit对目标域名做限流控制,触发限流时抛出QueueRetryAfterError,让任务延时重试且不消耗重试次数(packages/shared/queueing.ts); - 抓取后接力:抓取成功的页面会继续入队后续作业(
enqueuePostCrawlJobs)——包括 OpenAI 打标签/摘要、搜索索引重建、可选的视频下载以及crawled事件 webhook。
2. OpenAI Worker:AI 推断标签与摘要
推理作业从openai_queue队列消费,载荷为{ bookmarkId, type: "summarize" | "tag" }(packages/shared-server/src/queues.ts)。实现在 apps/workers/workers/inference/inferenceWorker.ts:
- 通过
InferenceClientFactory.build()(packages/shared/inference.ts)获取推理客户端,支持 OpenAI 兼容接口,因此可以接入各类大模型服务; - 作业成功后会把书签的
taggingStatus/summarizationStatus标记为success,失败且重试耗尽时标记为failure(attemptMarkStatus),这些状态字段由 Web 端展示; - 打标签(tagging)与摘要(summarize)分别实现在 apps/workers/workers/inference 目录下,是"AI 自动打标签"这一核心卖点的执行者。
3. Search Indexing Worker:Meilisearch 全文检索索引
索引作业从searching_indexing队列消费,载荷为{ bookmarkId, type: "index" | "delete" }(packages/shared-server/src/queues.ts)。实现在 apps/workers/workers/searchWorker.ts:
- 从数据库读取书签及其关联的链接正文、笔记、摘要、标签,组装成
BookmarkSearchDocument文档; - 通过
searchClient.addDocuments写入 Meilisearch(默认部署在http://meilisearch:7700),删除书签时则调用deleteDocuments同步清理索引; - 为提高可靠性,首次执行使用批量写入(
batch = job.runNumber === 0),重试时关闭批量、逐条写入。
得益于索引 Worker,Karakeep 的搜索可以覆盖正文全文、标题、标签、摘要与笔记,这是 Web 端全文检索功能(packages/shared/search.ts)的索引侧支撑。
从收藏到可搜索:一条完整的作业流水线
将三个 Worker 串起来,一次"保存链接"的完整生命周期是:
- 用户在 Web 应用保存链接,书签行写入 SQLite;
- Web 端将
{ bookmarkId, ... }入队link_crawler_queue; - Crawling Worker 用无头 Chrome 抓取页面正文、截图与元数据,写入书签的关联资产;
- 抓取完成后入队
openai_queue(打标签、摘要)与searching_indexing(重建索引),必要时入队视频下载队列; - OpenAI Worker 生成标签与摘要回写数据库;
- Search Indexing Worker 把最新的正文、标签、摘要组装成文档写入 Meilisearch;
- 用户随后即可在 Web 端通过全文搜索秒级检索到该链接。
这条链路在crawlerWorker.ts的enqueuePostCrawlJobs(apps/workers/workers/crawlerWorker.ts)中清晰可见,是理解整系统数据流的最佳起点。
部署形态:一个容器内三份职责
架构文档描述的组件在 docker/docker-compose.yml 中以三个服务呈现:
| 服务 | 镜像 | 职责 |
|---|---|---|
web | ghcr.io/karakeep-app/karakeep:release | Next.js Web 应用 + Worker(同一镜像按进程职责运行) |
chrome | ghcr.io/karakeep-app/karakeep-chrome:release | 无头 Chrome,供 Crawling Worker 使用 |
meilisearch | getmeili/meilisearch:v1.41.0 | 全文检索与向量存储 |
其中web服务通过MEILI_ADDR、BROWSER_WEB_URL环境变量与另外两个服务对接;Worker 的启停由环境变量WORKERS控制(见 packages/shared/config.ts 中的workers.enabledWorkers/workers.disabledWorkers配置),在 apps/workers/index.ts 的isWorkerEnabled中生效,允许按需裁剪后台任务。
架构的演进:v0.29 之后的多 Worker 生态
值得一提的另一个维度是:v0.29.0 文档聚焦的三类 Worker 只是系统的核心骨架。从当前仓库的 apps/workers/index.ts 可以看到,Worker 生态已经扩展为十余种——crawler、lowPriorityCrawler(导入等低优先级抓取)、embeddings(向量化)、inference、search、adminMaintenance、video、feed(RSS 订阅刷新)、assetPreprocessing、webhook、ruleEngine、backup以及import。它们全部遵循同一套"SQLite 队列 + 独立消费进程"的范式,印证了架构文档所描述的队列模型具备极强的扩展性——新增一类后台任务,只需注册一个队列并实现一个 Worker 即可。
整体来看,Karakeep 的架构哲学是"少依赖、可裁剪、易自托管":用 SQLite 同时承载业务数据与任务队列,用无头 Chrome + 大模型 API + Meilisearch 三个成熟组件完成"抓取-理解-检索"的闭环,最终以 Docker Compose 一键拉起,这正是它作为自托管收藏工具在部署层面广受欢迎的根本原因。
【免费下载链接】hoarderA self-hostable bookmark-everything app (links, notes and images) with AI-based automatic tagging and full text search项目地址: https://gitcode.com/GitHub_Trending/ho/hoarder
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考