Search 最危险的失败通常不是 500,而是“查询成功但漏数据、越权、过期或错误判定无冲突”。GA 验收必须围绕结果可信度,而不是只验证 DI 能解析和 Handler 调过某个 Mock。
1. 当前证据边界
现有测试证明:
ListSegmentWords.Handler委托给ISegmentWordStore;- Catalog store 可用内存 EntitySet 读/更新一行;
- Segment store 的串行 add/list/delete/cache 基本路径;
- owner-local 行与生成式 metadata 存在;
- Search Infrastructure 无具体 ORM 依赖;
- API/JobHost 项目引用、API composition 和 provider manifest 存在;
- Search ports 有 unavailable 默认和显式生产覆盖。
它们不证明:
- Lucene 实际能按 TenantId 隔离并返回预期字段/排序;
- 九条 endpoint 的授权和错误契约;
search.index.changed字节可绑定、可重放或能写 Created/Updated;- StoredFields/Total 不泄漏未授权资源;
- 利冲在索引延迟、规则变化和 provider 失败时不会给出错误“无冲突”;
- 重建、多副本、损坏恢复和隐私擦除可执行。
2. 测试金字塔
外部 Search 包也必须作为 consumer contract 被测;“包有单测”不能替代本平台对配置、字段 schema、租户和升级的保证。
3. 真实 Lucene 行为套件
使用每测试独立临时目录,并确保 finally 清理。至少覆盖:
- 同 DocumentId 在 Tenant A/B 的隔离;
- Upsert 后查询、更新覆盖、Delete 幂等;
- Name/Subject/Content 与中文、拼音、NGram;
- LegalSuffix/OrgWeak/GeoWeak 和停用词;
- Skip/Take、Total、确定排序和相同分值;
- StoredFields 类型/空值/超长/Unicode;
- delayed commit 前后可见性;
- 并发 writer/reader 与文件锁;
- 目录损坏、磁盘满、只读目录;
- package 升级前后索引兼容;
- cancellation/timeout;
- 重建切换期间持续查询。
[Fact]public async Task SameDocumentId_InDifferentTenants_MustNeverCrossRead(){ await using var fixture = await LuceneFixture.CreateIsolatedAsync();
// 两个租户使用相同业务 ID,内容刻意不同。 await fixture.Engine.UpsertAsync(Document("tenant-a", "case-42", "Alpha secret")); await fixture.Engine.UpsertAsync(Document("tenant-b", "case-42", "Beta secret")); await fixture.ForceCommitAsync();
// tenant-a 的查询不能返回 tenant-b 内容或 StoredFields。 var result = await fixture.SearchAsync("tenant-a", "Beta"); result.Hits.ShouldBeEmpty(); result.TotalHits.ShouldBe(0);}4. 消息契约与投影序列
测试必须从 owner 实际 outbox bytes 开始,而非手工 new consumer DTO。固定 topic、envelope、schema、枚举编码、SourceVersion 和缺失字段失败。
序列矩阵:
| 序列 | 目标结果 |
|---|---|
| Create v1 | 文档 v1 可查询 |
| Create v1, Create v1 | Duplicate,不重复 |
| Update v2 | 替换字段与 ACL |
| Delete v3 | 不可查询并保留 tombstone |
| Delete v3, Update v2 | Stale,不复活 |
| Update v5 after v2 | GapDetected,触发 repair |
| unknown action/schema | quarantine,不广播伪更新 |
| provider transient fail | CAP 有界 retry |
当前 Created/Updated no-op 和无 publisher 会让这些测试首先失败,这是有价值的真实红灯。
5. 授权与泄漏测试
.check/.search和.manage/.create/.update/.delete精确 200/403;- Feature 禁用时拒绝;
- unified 不能绕过 conflict 专用权限;
- tenant filter 写入和查询双侧验证;
- owner ACL/DataScope 撤权后旧 index token 不泄漏;
- StoredFields allowlist/脱敏;
- unauthorized hits 不通过 Total/hasMore/耗时泄漏;
- global catalog/segment word 只对平台范围管理员开放;
- 日志、trace 和 Problem Details 不回显 keyword/party/物理路径。
安全测试要用生产 authorization pipeline 和真实 Search schema,不要注入 always-allow authorizer。
6. 利冲验收
利冲测试既要检索准确性,也要流程证据:
- exact name、former/foreign name、credit code;
- 同名假阳性、简称、拼音、弱词与地理词;
- party role/opposing role;
- ExcludeCaseId 仅排除目标案件;
- 多 party 同名不会覆盖;
- 索引 watermark 过期时 fail closed/标记不可判定;
- provider timeout 不能返回 HasConflict=false;
- reviewer/override/blocked/cleared 状态;
- 规则、analyzer、词库版本可复盘;
- 输入/命中敏感信息的保留、导出、擦除。
建立经法务确认的 golden corpus,版本化预期匹配和误报/漏报容忍度。算法升级要跑差异报告,不只看总通过率。
7. 双 ORM Store 契约
对 SqlSugar/EF Core 跑同一套:
- catalog GetAll/Get/Update;
- unknown status 应 fail closed;
- LastRebuiltAt UTC/precision;
- missing catalog update 的显式结果;
- segment unique/case/collation/Unicode;
- 并发 Add 同词;
- Delete missing 幂等;
- List 稳定排序;
- 非法 SegmentType;
- 大词库加载性能与 cancellation。
当前内存测试不会触发数据库唯一约束、collation 或时间转换。
8. 可观测性
| 层次 | 必要指标 |
|---|---|
| producer/outbox | published、pending、oldest age、schema version |
| CAP consumer | applied、duplicate、stale、gap、failed、DLQ |
| Lucene writer | queue、commit latency、lock、directory bytes、disk free |
| index partition | document count、source/index watermark、drift |
| query | QPS、p50/p95/p99、timeout、zero-hit、oversample/filter ratio |
| conflict | checks、potential matches、review outcome、false-positive sample |
| rebuild | state、processed/total、ETA、verify errors、cutover/rollback |
日志键:TenantId(允许时 hash)、IndexKey、DocumentId(敏感场景可 hash)、EventId、SourceVersion、index version、result、duration、trace id。禁止完整 keyword、party、StoredFields。
9. SLO 与新鲜度
查询延迟 SLO 之外,还要有 freshness SLO,例如“已提交 owner change 在 60 秒内进入可查询版本”。响应或 status endpoint 提供:
- index version;
- source watermark / last applied occurred-at;
- lag;
- rebuilding/degraded;
- result 是否经过 owner authorization;
- 对利冲,规则/词库版本。
如果 freshness 超限,普通搜索可显式 degraded;利冲等高价值决策应返回“无法确认”而不是 false。
10. 事故处置
搜索持续返回 0
先区分真正无数据和索引未生产:检查 producer/outbox、consumer applied、Lucene document count、catalog/watermark。当前源码没有 producer且 Create/Update no-op,重启 API 不会补数据。
API 副本结果不一致
记录响应实例与 index version,检查每个 BasePath 和 volume。若事件只由一个 consumer 实例处理,本地目录必然分叉;停止把流量随机路由到不完整副本,再按选定拓扑重建。
Lucene 目录损坏/锁死
停止 writer,保留只读诊断副本,不直接删除唯一目录。用已验证 source 在 shadow path 重建,对账后切换。若无数据源,必须按数据丢失事故处理,不能声称索引“随时可重建”。
利冲疑似漏报
冻结关联接案决策,记录 index/rule/词库版本与 watermark,对 authoritative source 复核并扩大样本,修复 producer/rebuild 后重跑受影响时间窗,保留旧/新结果和人工处置。
11. GA 门禁
- 业务 owner 生产契约和 byte tests 通过;
- Created/Updated/Delete 全生命周期、版本/inbox/gap 完成;
- 工作的 data source、shadow rebuild、对账、回滚演练通过;
- 权限/Feature/IndexKey/命中级授权闭环;
- StoredFields schema 与敏感字段策略生效;
- 利冲有记录、人工复核、override、审计和 freshness fail-closed;
- 真实 Lucene、双 ORM、九端点与多副本测试通过;
- 绝对持久路径、容量、单 writer 和拓扑明确;
- 指标、告警、SLO、Runbook 与 on-call owner 完整;
- package alpha 升级有兼容矩阵和回滚;
- 隐私擦除/撤权贯穿 active、shadow、backup;
- 文档每个“支持”结论可指向源码/测试/运行证据。
按当前源码,Search 不能宣告数据索引与利冲 GA。Lucene provider 可注册是基础能力,不是生产闭环。
12. 评审命令
# 当前 Search 行为测试非常少;目标应有 provider、consumer、endpoint、security、rebuild 套件。rg -n "Search|Lucene|Conflict" tests -g '*.cs'
# 暴露 no-op、吞错、未就绪重建和缺 publisher。rg -n "deferred|suppress|RebuildPendingDataSource|SearchIndexChangedIntegrationEvent" \ src/Platform/Search src -g '*.cs'
# 检查生产配置、相对路径和多 Host 组合。rg -n "AddBitzOrcasSearchPlatform|LuceneBasePath|search-index|SearchIndexEventHandler" \ src/Hosts src/Platform/Search -g '*.cs'返回 Search 总览 · 统一搜索与利冲 · 事件与重建 · Provider 与目录