中文 EN

系统设计 Lab

Job Board 先用 PostgreSQL 完成全文检索与结构化过滤;只有候选打分、搜索功能或独立扩容成为主导约束时,Search Service 才成为单独的 component。

调节 active jobs、搜索 QPS、可复用 query 的实测 cache hit、keyword candidates、职位更新率与 freshness 预算。观察 PostgreSQL 何时足够、热门结果或 search session 何时值得 cache,以及 Elasticsearch / OpenSearch 何时才应成为独立 component。

分步讲解

一步一步把它想清楚

常规演进场景

从左到右点击,就是预设的演示路径。每张卡片都会改变 workload 输入。

Workload

这些是输入,不是预设好的架构阶段。

推荐形态

当前架构路径
Job Board 搜索架构图 白板风格 Job Board 架构:Cache 不是固定层,只在热门 normalized query 或稳定 search session 有证据时出现;PostgreSQL 保存 truth,CDC pipeline 更新派生 Search Index。 Clients APIs PostgreSQL Derived search Job seeker search + apply Employer publish + close Search API retrieve + rank Conditional cache hot results / session Job API source writes Application API validate active Job table source of truth PG indexes B-tree + GIN + GiST CDC / Outbox capture commits Event log / Queue buffer + replay Indexer versioned upsert Search service BM25 + filters
Clients
Job seeker 提交 keyword、filters,并申请一个当前仍 active 的职位
Employer 创建、编辑、暂停或关闭 Job
APIs
Search API 查询 PostgreSQL indexes,或在独立 Search Service 中执行 BM25 + filters
Conditional cache 只缓存热门 first-page result 或 bounded Top 1,000 session;long-tail miss 继续进入搜索后端
Job API 把职位变化写入 PostgreSQL source of truth
Application API 提交申请前回 PostgreSQL 校验最新 Job.status
PostgreSQL
Job table 保存 Job、status、version 和 typed search fields
PG indexes 用 active-only partial indexes 检索、过滤并缩小 ranking candidates
Derived search
CDC / Outbox CDC 读取 WAL,或 Relay 读取 transaction 内提交的 outbox event;只捕获已经 commit 的变化
Event log / Queue Kafka 一类的 durable log 负责缓冲、重试和 replay;它不是 CDC,也不是 source of truth
Indexer 消费事件并生成幂等、带 version 的 SearchDocument upsert
Search service Elasticsearch/OpenSearch 独立保存和扩展派生的 inverted index

瓶颈

每次查询需要 ranking 的 candidates

PostgreSQL search CPU 压力

Search latency vs p95 预算

Cache 后的 backend search traffic

Search freshness lag

Top-K / total count 工作量

为什么会变

    决策权衡

    Typed filters

    PostgreSQL FTS

    Active-only partial indexes

    Conditional result / session cache

    独立 Search Service

    CDC / Outbox / Queue

    申请时回源校验

    有出处支撑的规则

    这些是模型背后那些经得起时间考验的 system design 论断。而 slider 的具体阈值,则被刻意标注为教学用的假设。

    已验证的规则

    Partial index 只保存满足 predicate 的 rows

    active-only GIN、B-tree 和 GiST 可以排除多年 closed history,但 query predicate 必须能明确推出 index predicate。

    PostgreSQL — Partial Indexes
    已验证的规则

    OpenSearch keyword search 默认使用 BM25

    专用 Search Service 把全文 relevance、non-scoring filters 和 Top-K retrieval 当作核心 workload,而不是关系数据库中的附加能力。

    OpenSearch — Keyword search
    已验证的规则

    Elasticsearch 默认不会 cache 带 hits 的完整搜索结果

    Shard request cache 默认只 cache size=0 的 totals、aggregations 和 suggestions;热门 Top 20 result 需要 Search API 明确使用 Redis 一类的外部 cache。

    Elasticsearch — Shard request cache
    已验证的规则

    CDC 捕获数据库已经提交的 row-level changes

    CDC 是 change capture mechanism:connector 读取数据库 transaction log 并产生 change-event stream。Queue 是可选的传输与缓冲层,Indexer 才负责更新 SearchDocument。

    Debezium — CDC Architecture
    已验证的规则

    Outbox 让业务状态与 event 在同一个 transaction 提交

    Job API 同时写 Job row 与 outbox row;CDC connector 再捕获 outbox change。这样避免 PostgreSQL 成功、消息发送失败造成的同步双写分叉。

    Debezium — Outbox Event Router
    已验证的规则

    独立 Search Index 是 near real-time,不是事务内立即可见

    document changes 经过 refresh 才进入可搜索 segments;所以搜索 freshness 与 Application API 的业务正确性必须分开。

    Elasticsearch — Near real-time search

    教学用假设

    • 候选数、latency 和 throughput 公式只是展示拐点的保守教学模型,不是 PostgreSQL、Elasticsearch 或 OpenSearch benchmark。
    • Keyword 命中率与 filters 保留率应来自真实 query distribution;表的总行数本身不能决定是否迁移搜索。
    • PostgreSQL indexes 与 Job row 在同一事务维护;独立 Search Service 被建模为约 1 秒 refresh,再叠加 indexing backlog。
    • 生产路径使用 Outbox / CDC -> durable Queue -> Indexer;小规模版本可以省略 Queue,但不能同步双写 PostgreSQL 与 Search Service。
    • Result cache 默认关闭;只有 normalized popular-query / first-page 的实测 hit rate 足够高时,才使用 30 秒 TTL。Search session 则保存 bounded Top 1,000 job IDs 几分钟,换取稳定分页而非第一次查询加速。
    • 无论搜索读哪套 index,Application API 都以 PostgreSQL 当前 Job.status 作为最终正确性判断。