关于 shadowjoin
shadowjoin 是用于衡量数据精确连接漏掉多少匹配项的工具,适合数据管道和分析工作。它测试规范化规则、报告可恢复匹配的下限,并可用真实标签核验结果。
主要功能
- 01测量精确连接漏配下限
- 02测试规范化规则
- 03真实标签核验匹配
- 04支持 CSV、JSON 和数据库
- 05CI 损失阈值检查
适用场景
- 01检查管道连接中的漏配
- 02比较可采用的规范化规则
- 03在 CI 中设置漏配阈值
MCP 相关
MCP 服务提供连接扫描、漏配解释、规范化建议和运行比较工具。它仅访问配置的数据源,不提供任意 SQL 执行工具,也不接受工具调用中的连接字符串。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
shadowjoin
测量你的精确连接悄悄漏掉了多少匹配项。
你的数据管道按名称、电子邮件、SKU、公司连接两个数据源。能匹配的就匹配上了。其余的就……消失了。没有错误,没有异常,没有警报——查询正常返回,仪表板正常呈现,但本该匹配上的行中有四分之一悄悄漏掉了。
shadowjoin 会在真实连接键旁运行影子匹配器,告诉你漏掉了多少匹配项、哪条规范化规则可以找回每一项;而且——只要你有任何真实数据作为基准——它还能证明找回的匹配确实正确。
npx shadowjoin scan \
--left customers.csv --left-key company_name \
--right crm_export.csv --right-key account_name
14 秒,无声。视频中的每个数字都直接来自下方已提交示例的 CLI 输出——更高分辨率版本。
上面的进度输出是真实的:shadowjoin 会在运行时显示当前操作。stdout 不是终端时,所有进度输出都会自动隐藏,因此无论是输出到文件、管道传给 jq,还是在 CI 中运行,产生的字节都与之前完全一致——而且没有安装后脚本,因为一个要连接生产数据库的工具不该在安装时运行代码。
它在公开数据上发现了什么
将标普 500 成分股名单与纽交所 + 纳斯达克上市名单按公司名称连接。两个文件都包含股票代码,因此每个找回的匹配项都可以核实,而非盲目采信:
left rows 503 right rows 6,709
exact join matches 1 0.2%
recovered by shadow join 428 85.09%
still unexplained 74
LOWER BOUND ON LOST MATCHES: 85.09% of left rows
verified against ground truth: 419 correct, 1 wrong, 8 ambiguous (precision 99.76%)
rules worth adopting, in order (greedy by marginal recovery):
1. strip security/listing boilerplate (common stock, ADS, ...) +68 68
2. strip corporate suffixes (inc, corp, ltd, plc, gmbh ...) +319 387
3. strip punctuation +33 420
4. sort tokens (order-insensitive match) +4 424
5. remove all whitespace +1 425
精确连接只找到 1 项匹配。实际上有 428 项,其中 419 项经股票代码确认无误。
亲自复现——数据已随仓库提供:
git clone https://github.com/harsh-chandak/shadowjoin && cd shadowjoin
npm install && npm run build
node dist/cli.js scan \
--left examples/sp500.csv --left-key Security --left-id Symbol --left-truth Symbol \
--right examples/listings.json --right-key name --right-id symbol --right-truth symbol
你实际会部署的连接
上面的示例刻意选得不太好看——用简短的指数名称去匹配完整的证券描述。下面来看一个更贴近实际的案例。
将Unicode CLDR 国家名称(界面上显示的名称)与联合国官方国家名称(参考数据中使用的名称)按国家名称连接,并以 ISO 3166 alpha-3 作为基准。这是人们每周都会编写的连接。
exact join matches 205 / 249 82.33%
recovered by shadow join 5 2.01% precision 100%
still unexplained 39
rules worth adopting:
1. & -> and +4
2. remove all whitespace +1
结果看起来很健康:82% 匹配,没有错误,也没人调查。但它悄悄漏掉了 Antigua & Barbuda、Trinidad & Tobago、São Tomé & Príncipe、Turks & Caicos Islands 和 Vietnam——最后一项是因为联合国将其拼作 Viet Nam。
这个示例展示了前一个示例没有展示的两点:
**应用所有规则也找不到更多匹配项。**完整规则集在这里一个匹配项也找不回来,而两条规则的子集却找回了 5 项。过度规范化会把本应保持不同的键合并在一起;shadowjoin 会指出更合适的子集,而不是让你想当然地认为规则越多越好。
**它会区分机械性漏匹配与语义性漏匹配。**剩下无法解释的 39 项根本不是规范化问题:
Vatican City vs Holy See
North Korea vs Democratic People's Republic of Korea
Cape Verde vs Cabo Verde
Bosnia vs Bosnia and Herzegovina
Hong Kong vs China, Hong Kong Special Administrative Region
字符串转换无法找回这些匹配项。它们需要别名表或真正的实体解析库。弄清哪些漏匹配是机械性的、哪些是语义性的,决定了你面对的是一行代码就能修好的问题,还是一个项目。
node dist/cli.js scan \
--left examples/countries_ui.csv --left-key country --left-truth iso3 \
--right examples/countries_reference.csv --right-key country --right-truth iso3
这个特定连接刻意选得不太好看——用简短的指数名称去匹配完整的证券描述。这是一个展示测量方法的完整示例,并非声称有人会部署这样的连接。用你自己的数据运行它,得到一个对你有实际意义的数字。
数字是下限,不是估算值
没有标签就无法计算召回率。实体解析领域将这个指标称为配对完整率,并且假设你在生产环境中并没有的预言机。
因此,shadowjoin 报告的是一个下限:影子匹配器找到、但精确匹配遗漏的匹配项。影子匹配器自身的召回率未知,因此实际损失至少有这么大,也可能更大。每个数据都会附带样例配对,供人工核验;如果两侧都有真实值,报告还会包含精确率数据。
这是有意为之的较弱主张,却也是经得起质疑的主张。
为什么不直接让 LLM 来检查?
如果只是临时查看一个小表,那就这么做——更快,而且这个工具也就多余了。但在以下情况中,这种方法就行不通:
- 你需要两次得到同一个数字。 Agent 每次运行都会编写不同的 SQL,因此你无法判断是流水线变了,还是查询变了。
- 你需要没人想到过的规则。 如果被问到“这个联接是否有损”,Agent 会尝试
lower()和trim()。它不会系统地尝试 NFKD 折叠、变音符号处理、前导零、公司后缀、词元重排或房源描述中的套话。穷尽各种规则就是这个产品的价值。 - 你需要在 CI 中使用它。
--max-loss会以非零状态码退出。“问 Claude”没有退出码。 - 在生产环境中进行探索性 SQL 查询成本很高。 它会针对预先计算的键执行哈希联接,在语句超时限制下,通过只读事务运行。
安装
npm install shadowjoin # or run it with npx, no install
运行时零依赖。pg 和 @modelcontextprotocol/sdk 是可选的对等依赖,仅在使用 Postgres 适配器或 MCP 服务时才会引入。
使用方法
命令行工具
# CSV or JSON, no database involved
npx shadowjoin scan --left a.csv --left-key name --right b.json --right-key title
# with ground truth on both sides, so recoveries are verified
npx shadowjoin scan \
--left a.csv --left-key name --left-truth customer_id \
--right b.csv --right-key title --right-truth customer_id
# straight at Postgres
npx shadowjoin scan \
--left 'postgres://ro@host/db' --left-table orders --left-key customer_name \
--right 'postgres://ro@host/db' --right-table customers --right-key name
# in CI: fail the build if the floor climbs above 2%
npx shadowjoin scan --left a.csv --left-key name --right b.csv --right-key name --max-loss 2
# MySQL and SQLite work the same way
npx shadowjoin scan \
--left 'mysql://ro@host/db' --left-table accounts --left-key company_name \
--right ./reference.sqlite --right-table vendors --right-key name
npx shadowjoin rules # list the normalization rules
数据源: CSV、JSON、Postgres、MySQL 和 SQLite。SQLite 使用 Node 内置的
node:sqlite(Node 22.5+),因此无需安装任何内容;Postgres 和 MySQL 需要 pg 或 mysql2,它们是可选的对等依赖。
其他任何数据源都可以通过库用三行代码接入,因为核心接口接收的是两个数组,而不是数据库连接:
import { scan } from "shadowjoin";
const [rows] = await anyDriver.query("SELECT id, company_name FROM accounts");
const left = rows.map((r) => ({ id: String(r.id), key: r.company_name }));
scan(left, right);
库
import { scan, normalize, rulesByIds, toSql } from "shadowjoin";
const report = scan(leftRows, rightRows); // rows are { id, key, truth? }
console.log(report.lowerBoundLossPct, report.stack);
// adopt the recommended rules in your own pipeline
const rules = rulesByIds(report.stack.map((s) => s.rule));
const key = (value) => normalize(value, rules);
toSql("company_name", rules); // the same thing as Postgres SQL
Claude Code 插件(最简单)
一条命令即可同时安装技能和 MCP 服务,并提示你提供配置文件路径:
claude plugin marketplace add harsh-chandak/shadowjoin
claude plugin install shadowjoin@harsh-chandak
该项目也已列入官方 MCP 注册表,名称为 io.github.harsh-chandak/shadowjoin。
MCP 服务
让 Agent 可以测量联接、查看真实示例、提出修复方案,然后证明修复确实有效——这是 Agent 无法伪造的循环,因为测量结果并非由它编写。
{
"mcpServers": {
"shadowjoin": {
"command": "npx",
"args": ["-y", "-p", "shadowjoin", "-p", "@modelcontextprotocol/sdk", "shadowjoin-mcp"],
"env": { "SHADOWJOIN_CONFIG": "/abs/path/to/shadowjoin.config.json" }
}
}
}
{
"sources": {
"prod": { "type": "postgres", "connectionString": "postgres://shadowjoin_ro@host/db" },
"crm": { "type": "csv", "path": "./crm_export.csv" }
}
}
工具:scan_join、explain_misses、suggest_normalization、compare_runs。
这个 MCP 服务刻意不提供两项功能:
- 没有执行任意 SQL 的工具。 Agent 可以指定一个已配置的数据源、一张表和一列。这就是全部的操作范围。
- 没有接受连接字符串的工具。 凭据保存在你的配置文件中,绝不会进入工具调用,因为工具调用最终会出现在对话记录中。
连接到真实数据库
shadowjoin 会开启一个 READ ONLY 事务,并设置 statement_timeout、lock_timeout 和 idle_in_transaction_session_timeout;如果用于连接的角色有权写入即将读取的表,它就会拒绝运行。
为它提供一个无需信任也能放心使用的角色:
CREATE ROLE shadowjoin_ro LOGIN PASSWORD 'xxx';
GRANT CONNECT ON DATABASE yourdb TO shadowjoin_ro;
GRANT USAGE ON SCHEMA public TO shadowjoin_ro;
GRANT SELECT ON public.orders, public.customers TO shadowjoin_ro;
可以的话,将它指向一个副本。
**除你指定的数据源外,它不会建立任何出站网络连接。**没有遥测数据,甚至没有选择退出的机制。检查一下导入项——它只有三个依赖,而且它们都不是 HTTP 客户端。
限制
引用其中的数据前,请先阅读以下内容。
- 该数值是下限。 shadowjoin 的自身召回率未知。实际损失至少有这么多,也可能更多。
- 找回的匹配可能有误。 在捆绑的示例中,428 个结果里有 1 个与真实情况相矛盾,另有 8 个存在歧义——主要是股份类别,因为去掉“Class A”会合并实际不同的证券。对于有歧义的匹配,需要用判定规则来打破平局,而不是采用规范化规则。务必查看
samplePairs。 - 各规则的计数彼此重叠,不会相加为总数。 每条规则都独立地与精确基准进行比较,这是刻意设计的,因此这些数字不会受到规则执行顺序的影响。使用
stack——贪心排序——来决定实际采用哪些规则。 - 规则并非越多越好。 应用所有规则,找回的匹配可能反而少于采用最佳子集,因为过度规范化会合并本应分开的键。shadowjoin 会在发生这种情况时发出警告,并指出更好的子集。
- 它会读取你的联接键,因此运行期间这些值会存留在内存中。
--redact会在示例输出中对它们进行哈希处理,服务端规范化也会将整行数据保存在数据库中,但这两种方式都无法让键不可见。对于受监管的数据,请在你自己的环境中的副本上运行。 - 它不会写入、修复或迁移任何内容。 它只负责测量。
- 生成的 SQL 中的
unaccent需要安装同名的 Postgres 扩展。
相关研究
阻塞召回率——配对完整性——在实体解析文献中已得到充分研究,其相关指标还包括配对质量和缩减比:
这些论文都假设存在带标签的黄金标准。shadowjoin 的贡献不在于指标本身,而在于没有任何标签时,针对实时管道计算一个合理可信的指标,并将其作为界限而非点估计进行报告。
如果要执行匹配,而不是测量你的方案漏掉了哪些匹配,请使用 Splink、 Dedupe 或 recordlinkage。shadowjoin 与它们互为补充:它会告诉你是否需要这些工具。
许可证
MIT © Harsh N. Chandak
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。
