Search Infrastructure 负责三类组合:把外部检索引擎注册为 ISearchEngine;用 owner-local 表实现目录与弱词端口;提供平台停用词。Provider 是否可创建、索引是否已有数据、目录 catalog 是否存在,是三件不同的事。
1. Provider manifest
| Provider | 默认 | IsImplemented | 配置结果 |
|---|---|---|---|
| Lucene | 是 | 是 | 注册 AddBitzsoftLuceneSearch |
| ElasticSearch | 否 | 否 | 启动抛 InvalidOperationException |
| OpenSearch | 否 | 否 | 启动抛 InvalidOperationException |
空 Search:Provider 选择 Lucene;未知枚举名或 manifest 外值也失败。这个 fail-closed 选择是正确的,避免拼错后静默换 provider。
{ "Search": { "Provider": "Lucene", "LuceneBasePath": "/var/lib/bitzorcas/search" }}如果 BasePath 缺失,使用相对路径 search-index。生产应使用显式绝对持久卷,并确保运行用户权限、容量、inode、备份/重建策略和单 writer 所有权。
2. 外部包边界
版本由 Directory.Packages.props 固定为 1.0.0-alpha.8:
Bitzsoft.Integrations.Search定义ISearchEngine、SearchPerformInput、数据源、pipeline、词库和 catalog 端口;.Lucene包实现 analyzer、写入/删除/查询、延迟 commit 与目录替换能力。
平台文档可以描述组合和已验证行为,但不应把 package README 中每项能力自动升级为 BitzOrcas GA 保证。升级 alpha 版本前必须有独立 consumer contract、索引兼容与回滚测试。
3. DI 与失败关闭
生产有数据库/搜索能力时,API 的 PersistenceRegistration 调:
// 先由生成式默认端口提供 unavailable 实现,缺能力时查询必须失败而非返回空集合。services.AddBitzOrcasGeneratedPersistenceDefaults();
// 数据库 adapter 注入两个 IEntitySet owner-local 行。services.AddBitzOrcasGeneratedPersistenceAdapters(persistenceProvider);
// 最后解析 provider,注册 Lucene 引擎、两个 store、停用词和 CAP consumer。services.AddBitzOrcasSearchPlatform(configuration);ISearchEngine 默认端口需要 Search capability;ISegmentWordStore 同时需要 Database+Search。API Shell smoke/architecture tests守护缺能力时 unavailable,生产注册显式覆盖。
4. SearchIndexCatalog 不是索引本身
全局表字段:
| 字段 | 用途 | 当前边界 |
|---|---|---|
| IndexKey | 稳定场景键,唯一 | 无 seed、无创建命令 |
| DisplayName | 运维显示 | 全局字符串 |
| DirectoryName | Lucene 目录名 | API 可返回,可能泄漏拓扑 |
| EntityType | 领域类型说明 | 不作 CLR 反射入口 |
| AnalyzerType | analyzer 名 | DTO 不返回,Store update 也不更新 |
| Status | IndexStatus 字符串 | 未知值恢复为 Active |
| DocumentCount | 目录计数 | 当前无 writer 流程 |
| LastRebuiltAt | 上次重建 | 当前无成功重建更新 |
| RebuildStrategy | FullSwap 等 | DTO 不返回,Store update 不更新 |
| Version | 索引版本 | 当前无平台 writer 流程 |
| Description | 运维说明 | DTO 不返回 |
仓库找不到 catalog seed 或创建用例。新数据库的表可以存在但 GetIndexCatalog 返回空;Lucene engine 仍可能接受 IndexKey 并在物理目录工作,造成 catalog 与实际目录分离。
5. Catalog Store 的恢复语义
GetAll 使用 ListAsync(_ => true),没有排序。GetAsync 精确匹配 IndexKey。UpdateAsync 只更新已存在行;缺失时 no-op,不会创建隐式 catalog。
// 持久化字符串可能来自旧版本、手工写入或新 provider 状态。var status = Enum.TryParse<IndexStatus>(entity.Status, out var parsed) ? parsed : IndexStatus.Active;
// 这保证目录列表可读,却可能把损坏/新版本状态显示成可用。return new IndexCatalogEntry( entity.IndexKey, entity.DisplayName, entity.DirectoryName, entity.EntityType, status, entity.DocumentCount, ToUtcOffset(entity.LastRebuiltAt), entity.Version);管理 catalog 应对未知状态 fail closed 为 Unknown/Unavailable 并告警;不能假定 Active。LastRebuiltAt 持久化为 DateTime?,恢复时强制 offset zero;需要数据库 Kind/precision 双 ORM 测试。
6. Catalog 是全局还是租户级
表不带 TenantId,合理解释是“场景定义全局,物理文档按 tenant 分区”。但 DocumentCount/Version/LastRebuiltAt 究竟是全局目录、某租户还是所有租户总和没有协议。GetIndexStatistics 则明确带 current tenant。
建议分离:
SearchIndexDefinition:全局 schema/analyzer/required permission;SearchIndexPartitionState:TenantId+IndexKey+provider version+watermark+count;SearchIndexRebuildRun:一次操作状态与证据。
当前一张 catalog 行无法同时准确表达全局定义和每租户运行态。
7. SysSegmentConfig 作用域
弱词表同样无 TenantId,是平台全局配置。三种 SegmentType 来自外部包:LegalSuffix、OrgWeak、GeoWeak。唯一键 (SegmentType,Content),Content 最长 200。
新增流程:先精确查询同类型同内容;存在即 no-op;否则 insert;最后 _loaded=false。这是 check-then-insert,并发首次新增相同词可能撞唯一约束,Handler 没有把约束冲突映射为幂等成功。
删除使用精确内容;列表没有排序。输入 Trim 但不 Unicode normalize/case-fold。数据库不区分大小写时 LawFirm/lawfirm 与内存 OrdinalIgnoreCase 的结果可能不同。
8. 缓存实际生命周期
Store 注册 Scoped,缓存字段也在 store 实例:
- 同一 scope 首次 GetBuckets 用
SemaphoreSlim防重复加载; - 本 scope Add/Delete 把
_loaded=false; - 新 HTTP scope 本来就是新缓存;
- 其他并发 scope/节点没有失效通知;
- RefreshCache 只刷新当前实例。
所以它不是跨请求热缓存。若 Lucene 引擎持有 scoped store,每次请求可能重新读表;若某长生命周期组件错误捕获 scoped store,又会产生 scope 泄漏。需要用实际 DI graph 和 benchmark 确定。
9. 停用词 Provider
EmbeddedStopWordProvider 返回中英文标点集合,不做 I/O,也没有租户配置。返回对象声明 IReadOnlySet,底层是静态可变 HashSet,虽然调用方通常不能通过接口修改,仍应考虑暴露 immutable/frozen collection。
停用词与弱词不同:停用词主要去标点;LegalSuffix/OrgWeak/GeoWeak 影响实体名匹配,改变可能显著影响利冲结果。词库变更需要版本、审批、重建影响分析与审计,不能只当普通 CRUD。
10. 双 ORM 与 schema
两个 Record 使用编译期 [BitzTable]/[BitzColumn]/[BitzIndex] metadata,Store 只依赖 IEntitySet<T>。现有架构测试证明无 SqlSugar/EF Core 引用和元数据可发现,单元测试使用自制内存 EntitySet,不证明真实 provider:
- unique conflict 异常映射;
- string collation/case;
- DateTime Kind/precision;
- enum int 和非法值;
- 并发 add/delete/load;
- GetAll/ListWords 稳定排序。
需要同一 contract suite 分别跑 SqlSugar 与 EF Core。
11. 运维配置检查
# 生产必须显式配置绝对目录;不应依赖进程工作目录。test -n "$Search__LuceneBasePath"test "${Search__LuceneBasePath#/}" != "$Search__LuceneBasePath"
# 发布前检查目录可写与容量;不要在输出中打印索引内容。test -d "$Search__LuceneBasePath" && test -w "$Search__LuceneBasePath"df -h "$Search__LuceneBasePath"df -i "$Search__LuceneBasePath"容器应把目录挂到持久卷;滚动发布前验证新旧 package 读写兼容。索引若承诺可重建,仍要验证数据源与时间预算,不是简单删除目录。
12. 审查命令
# Provider 清单、配置键与 alpha 包版本应一起审查。rg -n "SearchProviderKind|IsImplemented|Search:Provider|LuceneBasePath|Bitzsoft.Integrations.Search" \ src/Platform/Search Directory.Packages.props -g '*.cs' -g '*.props'
# 目录当前无 seed/create writer,运行态字段只有 Store update 端口。rg -n "SearchIndexCatalogRecord|ISearchIndexCatalogStore|UpdateAsync\(" \ src tests -g '*.cs'
# 词库是全局行与 scoped instance cache。rg -n "SearchSegmentConfigRecord|TryAddScoped<ISegmentWordStore|_loaded|RefreshCacheAsync" \ src/Platform/Search tests -g '*.cs'返回 Search 总览 · 事件与重建 · 测试与 GA