Unified search and conflict checking both call ISearchEngine.SearchAsync, but their business meaning is different. Unified search returns navigation candidates; a conflict result feeds a legal intake decision. Neither Lucene hit is source-resource authorization or a final business conclusion.
1. Input actually built by UnifiedSearch
The handler sets only IndexKey, current TenantId, Skip/Take, and—when present—the same Keyword under Name, Subject, and Content. It does not set resource types, culture, sort, highlights, field projection, DataScope, owner filters, or an authorization callback.
var input = new SearchPerformInput{ IndexKey = request.IndexKey, // TenantId comes only from the authenticated context. TenantId = currentUser.User.TenantId, Query = { Skip = request.Skip, Take = request.Take }};
if (!string.IsNullOrWhiteSpace(request.Keyword)){ // Every scenario uses the same three hard-coded field names. foreach (var field in new[] { "Name", "Subject", "Content" }) input.Query.SearchFieldWordsDict[field] = [request.Keyword];}Claims that language and resource type are mandatory filters are not supported by source.
2. Hit projection and StoredFields
{ "items": [ { "businessId": "case-0042", "businessType": "Case", "matchKeywords": "Hailan Technology", "hitLevel": 8, "storedFields": { "Name": "Hailan Technology Co., Ltd.", "Subject": "Equity acquisition", "InternalNote": "returned if a producer stored it" } } ], "total": 1}SearchHitItemProjection copies every StoredFields value via ToString() and maps null to empty. There is no per-index allowlist, type schema, sensitivity, maximum length, or masker. Missing BusinessId/BusinessType/MatchKeywords also become empty strings, concealing index-quality failures.
Define a versioned output schema per IndexKey: indexable fields, returnable fields, sensitivity, maximum size, and masking. Keep summaries minimal; fetch details from the owning endpoint.
3. Missing candidate authorization
The previous manual showed authorizer.FilterAsync(actor,candidates). No such call exists. Effective authorization checks only search.search.view, not whether the caller may see a specific Case/Document/Ticket, its department, participants, confidentiality, or individual fields.
Index ACLs are fast but revocation must propagate without gaps. Post-filtering is authoritative but requires oversampling, batch checks, and fill-after-filter paging. Sensitive scenarios often combine a coarse index filter with owner final authorization.
4. Total can leak unauthorized volume
Current Total is the raw engine count. If Items are filtered later while Total remains raw, callers learn how many hidden records exist. Define authorized Total semantics: expensive exact total, approximate/lower-bound, cursor plus hasMore, or no total. Hiding details does not neutralize a tenant-wide count.
5. Conflict term construction
For every PartyInfo, nonblank Name, ForeignName, FormerName, and CreditCode enter an ordinal HashSet; the dictionary key is party.Name.
// Normalize for matching while preserving the original values as controlled evidence.var normalized = PartySearchTerms.Create( name: party.Name, foreignName: party.ForeignName, formerName: party.FormerName, creditCode: NormalizeCreditCode(party.CreditCode));
// A stable unique category key prevents duplicate or blank names from overwriting a party.categoryKeywordMap[normalized.CategoryKey] = normalized.Terms;
// Audit a digest and rule version, not full person/organization identifiers in logs.audit.RecordInputDigest(normalized.Digest, rules.Version);Current code does not trim/normalize/case-fold or validate a credit code. Duplicate names overwrite; a blank Name with another term produces a blank key. Party role is not represented.
6. Conflict query input
The query fixes IndexKey=conflict, uses the current TenantId, selects PreProcessRoleOpposing, supplies the category map, and optionally excludes SourceBusinessId. It does not explicitly set Skip/Take, rule version, threshold, ordering, time budget, or maximum party/term count. HasConflict is simply matches.Count > 0.
7. A potential match is not a final conflict
Name, abbreviation, pinyin, and weak-word matching create false positives. Index delay or missing related parties creates false negatives. A complete workflow separates PotentialMatch, ReviewedConflict, Cleared, Waived/Approved, and Blocked.
Current Search has no review/record state, and GetConflictRecord always returns pending-module NotFound. HasConflict=true should be interpreted only as potential match.
8. Evidence required for conflict checking
Persist at least:
- record, tenant, matter/case, requester;
- encrypted/redacted original evidence plus canonical digest;
- index version, watermark, and provider version;
- rule/analyzer/segment-dictionary versions;
- matched owner id, field, and score;
- reviewer, decision, reason, and override approver;
- created/reviewed/expired times;
- rerun lineage and superseded conclusions.
The lightweight ConflictRecordDto with only HasConflict and MatchCount is not enough to explain a result.
9. ExcludeCaseId boundary
Current code sets SourceBusinessId and ExcludeSourceBusinessId=true. Verify that it excludes only the active Case, uses the same canonical ID as the index, ignores blank values, cannot be abused to suppress a real match, is audited, and does not over-exclude when a source ID is missing. Prefer deriving it from the server-side matter context rather than trusting arbitrary client input.
10. Data minimization
Names, former names, credit codes, and case subjects are sensitive. Do not put request/response bodies into normal logs, full terms into tracing tags, or unrestricted values into StoredFields. Audit access and purpose, bind retention to case policy, encrypt approved diagnostic snapshots, and keep metrics to term/hit counts, duration, and rule version.
11. Required scenarios
- Current TenantId overrides client TenantId.
- Incorrect TenantId at ingestion is detected/reconciled.
- Unified search cannot bypass conflict permission.
- Owner authorization, DataScope, and revocation delay.
- StoredFields allowlist/masking/large values.
- Authorized paging and Total semantics.
- Duplicate/blank/Unicode names and credit codes.
- ExcludeCaseId manipulation.
- Rule and dictionary version pinning/rerun.
- Provider failure never becomes “no conflict.”
- Stale watermark is visible and fail-closed where required.
- Review, override, and immutable audit evidence.
12. Review commands
# StoredFields are copied wholesale and no owner authorization/DataScope is called.rg -n "StoredFields|SearchHitItemProjection|DataScope|Authoriz.*Filter" \ src/Platform/Search -g '*.cs'
# Inspect term construction, trusted tenant, exclusion, and Boolean conclusion.rg -n "BuildCategoryKeywordMap|trustedTenantId|ExcludeSourceBusinessId|HasConflict" \ src/Platform/Search -g '*.cs'
# The target should add record storage, review, override, and audit tests.rg -n "ConflictRecord|ConflictReview|ConflictOverride|PreProcessRoleOpposing" \ src tests -g '*.cs'