Skip to content
bitzorcas
中EN

Guide

Search 统一搜索、利冲与结果安全

深入统一搜索的 Query/Filter/Rule 输入、默认字段、租户边界、StoredFields 风险,以及利冲关键词、排除条件、证据与人工复核流程。

Last updated

统一搜索和利冲共用 ISearchEngine.SearchAsync,但业务语义完全不同:统一搜索返回候选导航;利冲是接案合规决策的输入。两者都不能把 Lucene 命中直接等同为源资源授权或最终业务结论。

1. UnifiedSearch 实际构造的输入

Handler 只设置:

  • IndexKey = request.IndexKey;
  • TenantId = currentUser.User.TenantId;
  • Query.Skip/Take;
  • Keyword 非空时,将原字符串分别放入 Name、Subject、Content。

它没有设置 resource types、culture、sort、highlight、field projection、DataScope filter、owner filter 或 source authorization callback。

当前统一搜索输入
var input = new SearchPerformInput
{
IndexKey = request.IndexKey,
// TenantId 只来自认证上下文,这是正确的跨租户基线。
TenantId = currentUser.User.TenantId,
Query =
{
Skip = request.Skip,
Take = request.Take
}
};
if (!string.IsNullOrWhiteSpace(request.Keyword))
{
// 同一词同时检索三个硬编码字段;没有场景特定 schema。
foreach (var field in new[] { "Name", "Subject", "Content" })
input.Query.SearchFieldWordsDict[field] = [request.Keyword];
}

文档不能声称“语言和资源类型成为过滤条件”;源码并没有这些字段。

2. SearchHitItem 投影

外部连接器命中被映射为:

UnifiedSearchResult
{
"items": [
{
"businessId": "case-0042",
"businessType": "Case",
"matchKeywords": "海岚科技",
"hitLevel": 8,
"storedFields": {
"Name": "海岚科技有限公司",
"Subject": "股权并购项目",
"InternalNote": "producer 如果写入,当前 API 会原样返回"
}
}
],
"total": 1
}

SearchHitItemProjection 把所有 StoredFields 的 value ToString(),null 变空字符串,没有字段 allowlist、值长度、类型 schema、敏感度或 masker。BusinessId/BusinessType/MatchKeywords 缺失时也变空字符串,隐藏了索引数据质量问题。

推荐每个 IndexKey 有版本化输出 schema:允许索引字段、允许返回字段、敏感分类、最大长度与 masker。搜索摘要应最小化,详情由 owner endpoint 返回。

3. 候选授权缺失

旧文档描述了 authorizer.FilterAsync(actor,candidates),实际 Handler 没有 authorizer。当前授权只判断调用者是否可执行 search.search.view,不判断:

  • 是否可查看命中的具体 Case/Document/Ticket;
  • 是否在对应 Office/Department/Team;
  • 是否是 owner、participant 或被显式分享;
  • 记录是否 sealed/confidential/legal hold;
  • StoredFields 中哪些列可见。

正确流程有两种:

Search query

方案 A: ACL/DataScope 写入索引

方案 B: oversample candidates

Owner batch authorization

authorized page

索引过滤快,但 ACL 变更必须同步且不能漏撤权;候选后复核更权威,但需要 oversampling、批量授权和“过滤后补页”,否则页面可能不足 Take。高敏场景可组合两者:索引粗过滤 + owner 最终复核。

4. Total 的泄漏语义

当前 Total 来自引擎原始命中数。若未来只过滤 Items 而保留 Total,未授权对象数量仍会侧信道泄漏。授权后分页必须同时定义 Total:

  • 精确 authorized total,成本高;
  • “至少 N 条”或不返回 Total;
  • 基于授权过滤的 cursor page;
  • 仅返回 hasMore。

不能返回 tenant-wide 原始 Total 再说“详情已隐藏”。

5. 利冲关键词构造

每个 PartyInfo 将非空的 Name、ForeignName、FormerName、CreditCode 放入 HashSet<string>(Ordinal),然后以 party.Name 作为 category key。

利冲关键词归一化目标
// 先按业务规则规范名称,保留原值用于证据展示。
var normalized = PartySearchTerms.Create(
name: party.Name,
foreignName: party.ForeignName,
formerName: party.FormerName,
creditCode: NormalizeCreditCode(party.CreditCode));
// CategoryKey 必须稳定且唯一;不能用可能空白/重复的 Name 覆盖前一方。
categoryKeywordMap[normalized.CategoryKey] = normalized.Terms;
// 日志记录哈希/计数和规则版本,不记录完整个人/机构识别信息。
audit.RecordInputDigest(normalized.Digest, rules.Version);

当前不 Trim/Unicode normalize/case-fold/校验信用代码,也不标记 party role。相同 Name 的两个 party 后者覆盖前者;空 Name 但 foreign/former/credit 有值时以空字符串为 key。

6. 利冲 SearchPerformInput

利冲固定:

  • IndexKey=conflict;
  • TenantId=当前租户;
  • Rule.Scenario=PreProcessRoleOpposing;
  • Rule.CategoryKeywordMap=上面的 map;
  • ExcludeCaseId 非空时设置 SourceBusinessId 并开启排除。

没有显式 Skip/Take,使用外部类型默认值;没有规则版本、命中阈值、排序、超时预算或最大 party/term 数。结果把所有 hits 映射后,以 matches.Count > 0 得出 HasConflict。

7. HasConflict 不是最终判定

搜索可能有假阳性:同名、简称、历史名称、拼音或弱词剥离;也可能有假阴性:索引延迟、未索引关联方、规则词典缺失。可靠工作流应区分:

  1. PotentialMatch:搜索召回;
  2. ReviewedConflict:有权限人员确认;
  3. Cleared:确认非冲突并说明理由;
  4. Waived/Approved:有冲突但按授权流程 override;
  5. Blocked:拒绝接案。

HasConflict=true 只能表示 potential match。当前没有记录/审核模型,甚至 GetConflictRecord 固定 404-like NotFound。

8. 利冲证据模型

商业化利冲记录至少要保存:

  • RecordId、TenantId、Case/MatterId、申请人;
  • 原始输入的加密/脱敏证据与 canonical digest;
  • search index version、watermark、provider version;
  • rule/analyzer/segment dictionary version;
  • 每个 match 的 owner ID、匹配字段与分值;
  • reviewer、decision、reason、override approver;
  • created/reviewed/expired 时间;
  • 重跑关系与旧结论是否失效。

只保存 HasConflict + MatchCount 的 DTO 也不足以复盘为何命中。

9. ExcludeCaseId 的边界

当前只把 ExcludeCaseId 写入 Query.SourceBusinessId 并打开 Filter.ExcludeSourceBusinessId。需要测试:

  • 仅排除当前 Case,不排除同一客户其他 Matter;
  • ID 大小写/规范化与索引值一致;
  • 空白值不触发;
  • 攻击者不能用任意 ID 排除真正冲突;
  • 被排除事实进入审计;
  • 若 source business id 缺失,不静默扩大排除。

排除条件应由服务器从正在创建/更新的案件上下文得到,不应完全信任客户端字符串。

10. 数据最小化与日志

姓名、曾用名、外文名、信用代码与案件主题可能是个人/商业敏感数据。要求:

  • 请求/响应 body 不进普通访问日志;
  • tracing tag 不记录完整 keyword/party;
  • StoredFields 按 schema 脱敏;
  • 利冲审计记录访问者和用途;
  • 保留/删除与案件合规策略绑定;
  • 调试快照加密、限时并需审批;
  • metrics 只记录 term 数、hit 数、耗时和规则版本。

11. 必测场景

  1. 当前 TenantId 覆盖客户端 TenantId;
  2. 索引中错误 TenantId 的防护与对账;
  3. 无场景权限不能通过 unified 搜索 conflict;
  4. 命中 owner 授权、DataScope 与撤权延迟;
  5. StoredFields allowlist/脱敏/超长值;
  6. 授权过滤后的分页和 Total;
  7. 同名 party、空 Name、Unicode、信用代码;
  8. ExcludeCaseId 欺骗与边界;
  9. 规则/词库版本固定与重跑;
  10. provider timeout/部分结果不得变“无冲突”;
  11. 索引延迟时 UI 明示 watermark;
  12. 人工确认、override 与审计不可篡改。

12. 审查命令

Terminal window
# 当前 SearchHitItemProjection 原样复制 StoredFields,且没有 owner authorizer/DataScope。
rg -n "StoredFields|SearchHitItemProjection|DataScope|Authoriz.*Filter" \
src/Platform/Search -g '*.cs'
# 检查利冲关键词、trusted tenant 与排除条件。
rg -n "BuildCategoryKeywordMap|trustedTenantId|ExcludeSourceBusinessId|HasConflict" \
src/Platform/Search -g '*.cs'
# 当前利冲记录只有固定未就绪 Handler;目标实现应出现 record store、review 和 audit 测试。
rg -n "ConflictRecord|ConflictReview|ConflictOverride|PreProcessRoleOpposing" \
src tests -g '*.cs'

返回 Search 总览 · HTTP 与授权 · 测试与 GA

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%