Skip to content
bitzorcas
中EN

Guide

Search Lucene Provider、目录与分词词库

说明 provider manifest、Lucene 配置/目录拓扑、owner-local catalog、状态恢复、全局分词弱词、停用词、缓存并发与双 ORM 行为。

Last updated

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 调:

Search 组合顺序
// 先由生成式默认端口提供 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运维显示全局字符串
DirectoryNameLucene 目录名API 可返回,可能泄漏拓扑
EntityType领域类型说明不作 CLR 反射入口
AnalyzerTypeanalyzer 名DTO 不返回,Store update 也不更新
StatusIndexStatus 字符串未知值恢复为 Active
DocumentCount目录计数当前无 writer 流程
LastRebuiltAt上次重建当前无成功重建更新
RebuildStrategyFullSwap 等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。

未知状态当前被恢复为 Active
// 持久化字符串可能来自旧版本、手工写入或新 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 确定。

Add/Delete 只置本实例false

Request scope A

Store A cache

Request scope B

Store B cache

Replica B

Store C cache

SysSegmentConfig

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. 运维配置检查

Terminal window
# 生产必须显式配置绝对目录;不应依赖进程工作目录。
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. 审查命令

Terminal window
# 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

100%

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