首页
/ LobeHub 产品级全文检索架构与运维全指南:从 FtsSearchRepo 到 Elasticsearch 迁移

LobeHub 产品级全文检索架构与运维全指南:从 FtsSearchRepo 到 Elasticsearch 迁移

2026-09-07 11:18:53作者:管翌锬

导读

本篇技术指南系统讲解 LobeHub(.agents/skills/full-text-search/SKILL.md 定义的"产品全文检索"技能域)中用于检索产品自有数据的全文搜索引擎:涵盖 agents、topics、messages、files、知识库、documents、chatGroups、memories 等全部可检索实体,以及其背后的 PostgreSQL(pg_search)与 Elasticsearch 双后端架构、数据捕获(Capture)/全量重建(Reindex)/增量同步(Sync)三大运维管线、权限与可观测性约束。读完本文,你将掌握 LobeHub 全库搜索从"一次搜索请求如何走到后端"到"如何安全切换搜索提供商"的完整链路,并理解为什么该仓库用一整套 Outbox 捕获与修订栅栏机制来保证重建与增量同步的一致性。

范围说明:本文聚焦搜索 LobeHub 产品自有数据,不涉及 Agent 的外部联网搜索(apps/server/src/services/search/)与 builtin 网页浏览工具,二者是相互独立的模块。

架构总览:一条稳定的读路径

SKILL 首先给出产品搜索的唯一稳定读路径,所有入口(router/service)都必须沿此路径执行:

router/service -> createFtsSearchRepo -> FtsSearchRepo -> selected backend -> existing result schema

整条链路在代码库中被切分为职责清晰的四层,每一层都有明确的"归属"边界:

层次 代码位置 职责
服务工厂层 apps/server/src/services/ftsSearch/ 请求级 provider 选择、Elasticsearch 配置加载、ES HTTP 客户端、后端遥测
仓储门面层 packages/database/src/repositories/ftsSearch/ provider 无关契约 + PostgreSQL / Elasticsearch 两套具体实现
共享类型层 packages/types/src/ftsSearch.ts 可搜索实体清单与搜索领域类型
文档投影层 packages/database/src/repositories/ftsSearchDocument/ ES 文档 schema、mapping、可查询字段、源表到文档的投影

服务工厂:createFtsSearchRepo

Routers 与领域服务必须调用 apps/server/src/services/ftsSearch/index.ts 中导出的 createFtsSearchRepo 来获得仓储实例,绝不允许自己 new 一个 provider。该工厂的职责是"先解析部署级 provider,再构造稳定的门面",其核心签名如下(从源码结构看,入参以 CreateFtsSearchRepoInput 为载体):

  • dbLobeChatDatabase 实例;
  • userId / workspaceId:决定搜索范围(scope);
  • callerAgentVisibility:调用方 Agent 可见性('private' | 'public' | null);
  • usage:本次搜索用途(见下文可观测性一节),贯穿整个请求用于埋点归因。

工厂内部的核心不变量是:缺失 Elasticsearch 配置或 provider 初始化失败都必须保持可见——loadElasticsearchFtsSearchConfig() 若缺少 ES_URL、索引命名空间或 API Key(非 insecure 模式),工厂会直接放弃构造;最终因 createFtsSearchBackendForProvider 返回空而抛出 FtsSearchBackendUnavailableError。该文件还显式导出了 FTS_SEARCH_PROVIDERS = { elasticsearch, pgSearch } 常量和 FtsSearchProvider 类型,作为仓库内唯一定义 provider 身份的地方。

仓储门面:FtsSearchRepo

packages/database/src/repositories/ftsSearch/index.ts 中定义的 FtsSearchRepo对外保持稳定、对后端保持中立的搜索门面:

  • 构造时若无显式 backend,默认装配 PgSearchFtsSearchBackend
  • search() 是主要入口:当 type 未指定时会并行并发地检索 agents、chatGroups、topics、messages、files、folder/page documents、userMemories、knowledgeBases 等多类实体,每类走独立的 FtsSearchBackendRequest
  • 各类实体的每类结果配额由 calculateLimits() 控制(默认每类 limitPerType = 5,上下文场景如 agent/page/resource 有不同的 3/6 配额梯度);
  • 门面永远只返回既有产品结果类型与展示顺序response.items 直接汇入既有 hydrate 结果 schema,routers 无需理解任何 provider 特有的结果形状。

门面还提供两个专用入口:ftsSearchCandidates()(仅取候选 ID,供遗留/混合路径自行做 PG 水合,需 ftsSearchCandidateEnabled 开启,而该开关只在 provider 为 elasticsearch 时打开)与 searchKnowledgeBaseDocuments()(在指定知识库集合内检索文档,返回 FtsSearchKnowledgeBaseDocumentHit[])。

共享领域类型

packages/types/src/ftsSearch.ts 维护着全文搜索"能搜什么"的权威清单:

export const FTS_SEARCH_DOCUMENT_ENTITIES = [
  'agents', 'topics', 'files', 'knowledgeBases', 'userMemories', 'chatGroups',
  'memoryContexts', 'memoryPreferences', 'memoryActivities', 'memoryIdentities',
  'memoryExperiences', 'personaDocuments', 'documents', 'messages',
] as const;

同时该文件定义了重建运行状态机:每个实体的 FtsSearchReindexEntityStatus'pending' | 'backfilling' | 'completed';整个重建任务 FtsSearchReindexRunStatus'backfilling' | 'ready_for_incremental_sync' | 'completed' | 'failed'——这四种整体状态对应下文 fts-search:reindex --status 的输出语义。

Provider 与权限不变量(Invariants)

SKILL 用整节强调了一组部署级不变量,理解它们才能安全地改动任何一层:

  1. FTS_SEARCH_PROVIDER 是部署级选择器,当前取值仅 pg_searchelasticsearch,它不是功能开关、更不是用户灰度(rollout)。只有当某 provider 被端到端实现后,才允许增加新的枚举值。该语义在 packages/env/src/ftsSearch.ts 中落实为 FTS_SEARCH_PROVIDER: z.enum(['elasticsearch', 'pg_search']).default('pg_search')——即默认回落到 PostgreSQL 实现。

  2. 绝不静默降级:ES 报错、配置缺失、候选行为不受支持时错误必须暴露给调用方;禁止"静默重试 PostgreSQL"或加 ilike 兜底。SKILL 特别强调"候选检索不得扩大调用方作用域"。

  3. 每实体路由禁止:在引入 ES 之前,必须有覆盖测试证明 ES 后端支持 provider 中立契约中的每一个实体,且不得在 provider 之间做"按实体分流"(per-entity routing)。

  4. 权限边界始终归 PostgreSQL:即便 ES 只回传候选 ID,PostgreSQL 水合(hydration)与父级校验(parent checks)仍是权威的权限裁决点;当既有结果路径需要数据库授权或水合时,不得直接把裸 ES hit 返回给用户。这也是 packages/database/src/repositories/ftsSearch/elasticsearch/ 下同时存在 candidates.tshydration.ts 的原因——从源码结构看,ES 后端负责 candidate 召回,PG 水合与作用域过滤(userId / workspaceId / callerAgentVisibility)随后把关。

新增/修改一个可搜索实体:必须作为跨层变更处理

把一个实体加进全文索引(或修改其文档投影),在 LobeHub 中是一个贯穿六层的单一变更,任何一层遗漏都会造成搜索行为与数据不一致。SKILL 给出逐项核对清单:

  1. packages/types/src/ftsSearch.tsFTS_SEARCH_DOCUMENT_ENTITIES 与共享请求/结果类型;
  2. packages/database/src/repositories/ftsSearchDocument/ 下的文档 schema、mapping、文本字段、过滤器、fixtures 与 mapping parity(映射一致性)测试——目录内可看到配套的 schema.tsmappings.tsbuilder.ts 及其 schema.test.ts / mappings.test.ts / builder.test.ts
  3. FtsSearchDocumentBuilder(builder 层),包括软删除与来自相关源行的 fanout(扇出);
  4. PostgreSQL 与 Elasticsearch 两个后端的候选行为、权限水合、分页、排序;
  5. 捕获函数/触发器或 captureInfrastructure.ts 中的 fanout 查询;
  6. 重建 checkpoint、Outbox 排空、指标与自托管文档。

一致性硬性要求:schema 字段、mapping、builder、固定 fixtures 必须完全一致——不在文档 schema 中的字段,绝不能出现在 mapping 或查询字段列表里。

查询预算:multi_match 的叶分句上限

SKILL 特别提醒一个易踩坑的约束:Elasticsearch multi_match 查询长度由"共享叶分句预算 ÷ 所选查询字段数"决定。新增一个查询字段会降低该实体的安全查询长度;而如果更换查询分析器导致"每个 Unicode 码点产生多个 term",就必须重估预算及其字段数回归测试。从服务层遥测指标(见可观测性一节)也能侧面印证这一设计:存在 executedQueryCharsoriginalQueryChars 两组直方图,分别记录"预算截断后实际发出的查询"与"原始查询"的字符数,用于监控截断行为。

数据捕获、全量重建与增量同步

这是 SKILL 中实操价值最高的一节,也是自托管用户最需要小心编排的部分。

职责划分:migration 与 runtime installer 的边界

  • 数据库迁移(regular migrations)负责"持久 schema":修订序列(revision sequence)、Outbox 表与索引、schema 托管的源表索引(例如 Memory fanout 的 GIN 索引)。它们随版本升级自动安装。
  • 捕获函数与触发器(capture functions/triggers)只在算子显式启用 ES 路径时才安装。入口脚本为 scripts/installFtsSearchSyncCapture/index.ts,它要求 DATABASE_URL 就绪,通过 runWithLockRetry 包装仓库方法 installCaptureInfrastructure()(该方法的持久化实现位于 packages/database/src/repositories/ftsSearchSyncOutbox/),安装成功/失败分别打印明确的 ✅ ... / ❌ ... 前缀日志。SKILL 要求其语义为:事务性、definition-checked(定义校验)、对完全一致的安装幂等、对部分安装或定义被改动时 fail-closed(关闭即失败)
  • 反向约束同样重要:不要为纯 PG 实例安装捕获设施,也不要把普通 schema 索引挪进 runtime installer 仅仅因为它恰好支撑捕获。

Outbox 并发与顺序保证

Outbox(变更日志表,schema 见 packages/database/src/schemas/ftsSearchSyncOutbox.ts)是整个增量链路的持久化基石,SKILL 明确其语义:

  • (entity, document_id) 合并(coalesce);更新的捕获在锁定冲突行之后重置重试/死信状态并分配新修订号,从而保证同一文档的提交顺序不被打乱。
  • 序列分配是非事务的:必须使用既有写入栅栏(write fences)与"已提交修订边界",绝不能仅凭 last_value 就断言更早的 Outbox 行都已可见
  • 认领(claim)使用精确租约时间戳作为 fencing token:被其他 worker 回收的工作,陈旧 worker 不得再 ack / fail / release。
  • 死信:永久失败与重试耗尽变成持久死信;任何"创建或观察到死信"的排空过程必须失败,而不是继续发布一个虚假的成功切换信号。

支持算子入口(可直接运行的命令)

SKILL 给出的全部运维入口如下(均可从仓库根目录执行,且都能在根 package.json 中找到对应 script):

# 1) 安装 ES 路径的捕获设施(触发器/函数,事务性、幂等)
bun run db:install-fts-search-capture
#    实际映射: cross-env MIGRATION_DB=1 tsx ./scripts/installFtsSearchSyncCapture/index.ts

# 2) 查看可恢复的全量回填状态(重建到哪个实体/进度)
bun run fts-search:reindex -- --status
#    实际映射: node scripts/elasticsearchReindex/runner.mjs

# 3) 执行/确认全量回填(apply 模式,--yes 免二次确认)
bun run fts-search:reindex -- --apply --yes

# 4) 执行一段增量同步(最多 8 步,--yes 免确认)
bun run fts-search:sync -- --max-steps=8 --yes
#    实际映射: cross-env MIGRATION_DB=1 bun run scripts/elasticsearchSync/cli.ts

# 5) 查看 PG 侧清理任务状态
bun run scripts/pgSearchCleanup/index.ts --status

# 6) 执行 PG 侧清理(apply 模式)
bun run scripts/pgSearchCleanup/index.ts --apply --yes

fts-search:reindex 的可恢复(resumable)全量回填实现在 scripts/elasticsearchReindex/(含 runner.mjsoptions.tspreparation.tsruntime/ 子目录,以及 __tests__),其任务状态机 'backfilling' | 'ready_for_incremental_sync' | 'completed' | 'failed' 与共享类型层定义严格对齐;回填期间按实体维护 'pending' | 'backfilling' | 'completed' 的 checkpoint,配合 --status 支持断点续跑。持续增量排空则分别存在于服务端 apps/server/src/services/ftsSearchSync/ 与独立 CLI scripts/elasticsearchSync/(SKILL 还提示同步常驻受 FTS_SEARCH_SYNC_ENABLED 环境变量门控,该变量定义于 packages/env/src/ftsSearch.ts)。

上线顺序(cutover)红线

全量回填 ≠ 增量同步,回填本身不切换产品流量。 必须让应用继续跑在 PostgreSQL 上,直到:ES 别名(aliases)就绪 Outbox 已排空且保持稳定,随后才显式切换 provider。

在执行任何流程编排之前,请先通读自托管文档 docs/self-hosting/advanced/elasticsearch-migration.mdx(或其中文版 elasticsearch-migration.zh-CN.mdx)。当涉及数据库发布或索引成本影响设计时,SKILL 要求配合 db-migrations skill,并在真实 Dev 数据库上实测相关操作后再决定是否引入手工/延迟发布步骤。

Elasticsearch 环境变量与配置语义

SKILL 指明通用 ES 环境变量的归属在 packages/env/src/ftsSearch.ts。结合该文件与 apps/server/src/services/ftsSearch/index.tsloadElasticsearchFtsSearchConfig 加载逻辑,可整理出完整的配置矩阵:

变量 类型/默认 说明
FTS_SEARCH_PROVIDER elasticsearch / pg_search,默认 pg_search 部署级 provider 选择器
ES_URL URL(可选) ES 端点;缺失则 ES 后端不可用
ES_API_KEY 字符串(可选) ES API Key;除 insecure 模式外必填,绝不通过明文 HTTP 传输
ES_INDEX_NAMESPACE 字符串(可选) 索引命名空间;开发环境(NODE_ENV=development)缺省为 lobehub-dev,生产必须显式配置
ES_ALLOW_INSECURE_HTTP 'true' / 'false'(可选) 显式放行私有容器网络上禁用安全的 ES 节点:允许对非 loopback 主机使用明文 http:// 并可省略 ES_API_KEY
FTS_SEARCH_SYNC_ENABLED 'true' / 'false'(可选) 门控持续增量同步

配置加载的决策逻辑(来自 loadElasticsearchFtsSearchConfig):缺少 ES_URL 或索引命名空间即返回空;缺少 ES_API_KEY 且未开启 insecure 模式同样返回空——任一情况都会令 createFtsSearchRepo 在 provider 为 elasticsearch 时抛出 FtsSearchBackendUnavailableError,把"配置缺失"这一事实如实暴露给调用方,而不是悄悄回退。

可观测性与产品分析

SKILL 对"埋点放什么、放哪层"有严格边界,仓库实现集中在 apps/server/src/services/ftsSearch/observability.ts

服务端指标:成本与延迟

服务端通过 OTel Meter/Tracer 采集,指标命名带 fts-search-backend / fts.search.backend 前缀,覆盖:

  • 操作级fts_search_backend_operations_totalfts_search_backend_operation_duration,按 provider × entity × operation × result 分组;operation 区分 candidate_querypg_hydrationproduct_path
  • 结果漏斗fts_search_backend_result_count 记录 requested / candidate / product 三阶段数量,用于观察"召回候选 → PG 水合后实际结果"的收敛;
  • ES 请求级fts_search_elasticsearch_requests_total 及请求耗时、请求/响应字节、ES 服务端 took、返回 hits 数等直方图,并记录 error_code(如 index_not_foundtoo_many_clausesrate_limitedtimeout 等十余种);
  • 记忆检索决策fts_search_user_memory_lexical_decisions_total 与查询字符数,来源限定为 api | memory_extraction | tool | topic_retrieval

标签有界原则:SKILL 明确 entity、provider、operation、outcome、粗粒度错误类型可以作标签;原始查询、用户 ID、文档 ID、索引内容一律不得作为标签(observability.ts 中 buildFtsSearchBackendMetricAttributes 的实现正是只取这五个有界维度)。usage(如 home_searchunified_searchknowledge_basememory_toolmessage_search)作为成本归因维度贯穿各计数器。

前端事件:用户体验

用户实际感知的搜索行为归口到开源的 Command Menu 埋点文件 src/features/CommandMenu/analytics.ts:产品分析可覆盖端到端时长、渲染结果数、空结果、结果点击与放弃(abandonment)。SKILL 给出的分工很明确——后端指标解释 provider 成本与延迟,前端事件解释用户体验,二者不可互相替代

遥测失败的黄金法则

无论服务端还是前端,测量或分析失败绝不能改变所选 provider 的结果或错误。这一法则在代码中有双重落实:withFtsSearchBackendObservability 把埋点包装在业务调用之外,任何 recordSafely 捕获到的埋点异常只走 diag.error 日志;FtsSearchRepo.recordMeasurement 中 hook 的异常被 try/catch 后仅 console.error,且返回 Promise 的 hook 用 catch 吞掉拒绝——业务结果不受影响。

验证与测试策略

改动任何 provider、mapping、builder、capture、sync 或分析模块,SKILL 要求"在变更模块旁补充或更新聚焦测试",并长期保留三类边界测试:

  • 调用方必须走 FtsSearchRepo(禁止绕过门面直连 provider);
  • provider 不得泄漏裸结果形状(混合/遗留路径必须经过 PG 水合与权限检查);
  • 遥测失败不影响搜索行为(对应 apps/server/src/services/ftsSearch/observability.test.ts 与仓储层的 boundary 测试)。

每当以下路径被改动,必须补测:授权裁剪候选之后的分页、删除/作用域变更的优先级、Outbox 并发认领与陈旧结算、重试/死信行为、重建断点续跑的标识一致性、捕获定义不匹配(capture-definition mismatch)。仓储层的既有测试骨架可参考 packages/database/src/repositories/ftsSearch/tests/elasticsearch.test.tsboundary.test.tspackages/database/src/repositories/ftsSearch/elasticsearch/ 下成组的 *.test.ts

本地验证命令(SKILL 原文):

# 从仓库根目录对改动文件运行全量静态检查(类型/lint 等)
bun run check <changed-files...>

涉及 migration 或数据库运行时变更时,SKILL 要求遵循 db-migrationstesting skill 的流程,并在真实 Dev 数据库上验证,而不是只在本地空库上想当然。

小结:给运维与开发者的三条行动准则

  1. 改实体 = 跨六层变更:从 packages/types/src/ftsSearch.ts 的实体清单、ftsSearchDocument 的 schema/mapping/builder、双后端行为、captureInfrastructure 的捕获函数,到重建 checkpoint 与自托管文档,任何一跳缺失都会造成索引与结果不一致;schema、mapping、fixtures 必须逐字段对齐。
  2. 切 ES 前先满足全部前置:跑 db:install-fts-search-capture 安装定义校验过的捕获设施 → fts-search:reindex --status / --apply --yes 完成可恢复全量回填 → 在 Outbox 稳定排空且别名就绪后,才通过 FTS_SEARCH_PROVIDER=elasticsearch 显式切换,全程不得静默回退 PG、不得在 provider 间做实体分流。
  3. 让错误与观测都"诚实":ES 配置缺失、provider 失败保持可见;埋点标签永远有界(entity/provider/operation/outcome/usage),遥测永远不影响搜索结果;遇到数据库/索引成本相关的取舍时,先实测再上线。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391