|
|
|
|
|
# hw-portal 搜索改造方案
|
|
|
|
|
|
|
|
|
|
|
|
## 1. 文档目的
|
|
|
|
|
|
|
|
|
|
|
|
- 文档范围:给 `Admin.NET-v2` 中的 `hw-portal` 插件模块设计一套新的搜索方案。
|
|
|
|
|
|
- 目标结果:使用 `EF Core 10 + MySQL 普通搜索引擎(全文检索 + 兜底模糊搜索)`,替换当前基于 `UNION ALL + LIKE` 的关键词搜索。
|
|
|
|
|
|
- 约束来源:基于当前对话已确认,不做向量搜索,不接大模型,不接本地 embedding 模型,保持现有搜索接口不变,允许新增搜索索引表与数据库索引。
|
|
|
|
|
|
- 文档用途:供后续开发、代码评审、数据库设计、前后端联调、测试验收使用。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 2. 当前理解
|
|
|
|
|
|
|
|
|
|
|
|
## 2.1 当前项目结构理解
|
|
|
|
|
|
|
|
|
|
|
|
- 后端主框架:`Furion + SqlSugar`
|
|
|
|
|
|
- 当前新增的门户模块:`Admin.NET\Plugins\Admin.NET.Plugin.HwPortal`
|
|
|
|
|
|
- 当前 `hw-portal` 已经是独立插件项目,但搜索仍是“兼容 MyBatis XML”的方案,不是 EF Core 方案。
|
|
|
|
|
|
- 当前仓库里没有发现 `EF Core`、`DbContext`、`Npgsql/Pomelo/MySql EF Provider` 等依赖,说明 EF Core 搜索子系统需要从零引入。
|
|
|
|
|
|
|
|
|
|
|
|
## 2.2 当前搜索实现理解
|
|
|
|
|
|
|
|
|
|
|
|
当前搜索主入口:
|
|
|
|
|
|
|
|
|
|
|
|
- [HwSearchController.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwSearchController.cs)
|
|
|
|
|
|
- [HwSearchAdminController.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwSearchAdminController.cs)
|
|
|
|
|
|
|
|
|
|
|
|
当前搜索核心实现:
|
|
|
|
|
|
|
|
|
|
|
|
- [HwSearchService.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Search/HwSearchService.cs)
|
|
|
|
|
|
|
|
|
|
|
|
当前搜索 SQL:
|
|
|
|
|
|
|
|
|
|
|
|
- [HwSearchMapper.xml](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwSearchMapper.xml)
|
|
|
|
|
|
|
|
|
|
|
|
当前索引重建实现:
|
|
|
|
|
|
|
|
|
|
|
|
- [IHwSearchRebuildService.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Search/IHwSearchRebuildService.cs)
|
|
|
|
|
|
- [HwNoopSearchRebuildService.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Search/HwNoopSearchRebuildService.cs)
|
|
|
|
|
|
|
|
|
|
|
|
当前文本抽取器:
|
|
|
|
|
|
|
|
|
|
|
|
- [PortalSearchDocConverter.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Search/PortalSearchDocConverter.cs)
|
|
|
|
|
|
|
|
|
|
|
|
## 2.3 当前搜索的实际问题
|
|
|
|
|
|
|
|
|
|
|
|
1. 搜索结果直接查业务表,`UNION ALL + LIKE` 性能一般。
|
|
|
|
|
|
2. 搜索和业务表强耦合,新增来源或调整评分成本高。
|
|
|
|
|
|
3. 重建接口当前是空实现,没有真正索引重建能力。
|
|
|
|
|
|
4. 关键词搜索只能做字符匹配,不是“搜索引擎式”的独立索引架构。
|
|
|
|
|
|
5. 当前不是 EF Core 方案,也没有独立搜索库/搜索表/全文索引。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 3. 对话中的选择结论
|
|
|
|
|
|
|
|
|
|
|
|
本方案不是拍脑袋决定,而是基于当前对话确认的选择:
|
|
|
|
|
|
|
|
|
|
|
|
### 3.1 已确认选择
|
|
|
|
|
|
|
|
|
|
|
|
- 不做 `Vector Search`
|
|
|
|
|
|
- 不使用大模型
|
|
|
|
|
|
- 可以做“普通搜索引擎”
|
|
|
|
|
|
- 保持原接口不变
|
|
|
|
|
|
- 允许改数据库结构
|
|
|
|
|
|
- 搜索相关改造切到 `EF Core 10`
|
|
|
|
|
|
|
|
|
|
|
|
### 3.2 因此得出的最终方案
|
|
|
|
|
|
|
|
|
|
|
|
不是:
|
|
|
|
|
|
|
|
|
|
|
|
- `EF Core 10 + Vector Search`
|
|
|
|
|
|
- `Elasticsearch`
|
|
|
|
|
|
- `Easy-ES`
|
|
|
|
|
|
- 继续使用 `MyBatis XML + LIKE`
|
|
|
|
|
|
|
|
|
|
|
|
而是:
|
|
|
|
|
|
|
|
|
|
|
|
- `EF Core 10 + MySQL 全文检索(FULLTEXT)`
|
|
|
|
|
|
- 辅助 `LIKE` 兜底
|
|
|
|
|
|
- 独立搜索索引表
|
|
|
|
|
|
- 独立搜索 `DbContext`
|
|
|
|
|
|
- 新旧搜索逻辑保留过渡接口,但默认切新方案
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 4. 具体需求
|
|
|
|
|
|
|
|
|
|
|
|
## 4.1 业务需求
|
|
|
|
|
|
|
|
|
|
|
|
1. 前台搜索继续支持:
|
|
|
|
|
|
- 关键词搜索
|
|
|
|
|
|
- 分页
|
|
|
|
|
|
- 命中摘要
|
|
|
|
|
|
- 高亮
|
|
|
|
|
|
- 路由跳转
|
|
|
|
|
|
2. 编辑端搜索继续支持:
|
|
|
|
|
|
- 编辑路由 `editRoute`
|
|
|
|
|
|
3. 搜索来源继续覆盖:
|
|
|
|
|
|
- 菜单 `hw_web_menu`
|
|
|
|
|
|
- 页面 `hw_web`
|
|
|
|
|
|
- 页面详情 `hw_web1`
|
|
|
|
|
|
- 资料文档 `hw_web_document`
|
|
|
|
|
|
- 配置类型 `hw_portal_config_type`
|
|
|
|
|
|
4. 后台继续保留“重建索引”接口。
|
|
|
|
|
|
|
|
|
|
|
|
## 4.2 技术需求
|
|
|
|
|
|
|
|
|
|
|
|
1. 新搜索查询必须使用 `EF Core 10`
|
|
|
|
|
|
2. 新搜索必须有独立搜索索引表,而不是每次直查原业务表
|
|
|
|
|
|
3. 新搜索必须兼容 MySQL
|
|
|
|
|
|
4. 现有公开接口路径与返回 DTO 不变
|
|
|
|
|
|
5. 要支持增量同步索引,不允许每次保存都全量扫描业务表
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 5. 应用场景分析
|
|
|
|
|
|
|
|
|
|
|
|
## 5.1 用户搜索官网内容
|
|
|
|
|
|
|
|
|
|
|
|
典型场景:
|
|
|
|
|
|
|
|
|
|
|
|
- 用户输入“工业物联网”
|
|
|
|
|
|
- 需要搜到:
|
|
|
|
|
|
- 产品中心配置类型
|
|
|
|
|
|
- 产品页面 JSON 文本
|
|
|
|
|
|
- 页面详情 JSON 文本
|
|
|
|
|
|
- 相关文档
|
|
|
|
|
|
- 菜单项
|
|
|
|
|
|
|
|
|
|
|
|
要求:
|
|
|
|
|
|
|
|
|
|
|
|
- 响应时间稳定
|
|
|
|
|
|
- 标题命中优先
|
|
|
|
|
|
- 摘要可读
|
|
|
|
|
|
- 可跳前台页面
|
|
|
|
|
|
|
|
|
|
|
|
## 5.2 编辑端搜索
|
|
|
|
|
|
|
|
|
|
|
|
典型场景:
|
|
|
|
|
|
|
|
|
|
|
|
- 运营同事在后台搜索某个页面或某个资料文档
|
|
|
|
|
|
- 需要直接跳到编辑页
|
|
|
|
|
|
|
|
|
|
|
|
要求:
|
|
|
|
|
|
|
|
|
|
|
|
- 复用同一套索引
|
|
|
|
|
|
- 只是在结果里补 `editRoute`
|
|
|
|
|
|
|
|
|
|
|
|
## 5.3 数据更新后的搜索可见性
|
|
|
|
|
|
|
|
|
|
|
|
典型场景:
|
|
|
|
|
|
|
|
|
|
|
|
- 新增/修改页面 JSON
|
|
|
|
|
|
- 更新资料文档
|
|
|
|
|
|
- 新增菜单
|
|
|
|
|
|
|
|
|
|
|
|
要求:
|
|
|
|
|
|
|
|
|
|
|
|
- 保存成功后索引同步更新
|
|
|
|
|
|
- 不依赖手工全量重建
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 6. 技术选型分析
|
|
|
|
|
|
|
|
|
|
|
|
## 6.1 为什么不继续用当前 SQL 搜索
|
|
|
|
|
|
|
|
|
|
|
|
当前 [HwSearchMapper.xml](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwSearchMapper.xml) 的特点:
|
|
|
|
|
|
|
|
|
|
|
|
- 多张表 `UNION ALL`
|
|
|
|
|
|
- 每张表都做 `%keyword%`
|
|
|
|
|
|
- 排序靠写死分数和更新时间
|
|
|
|
|
|
|
|
|
|
|
|
优点:
|
|
|
|
|
|
|
|
|
|
|
|
- 简单
|
|
|
|
|
|
- 快速可用
|
|
|
|
|
|
|
|
|
|
|
|
问题:
|
|
|
|
|
|
|
|
|
|
|
|
- 性能扩展性差
|
|
|
|
|
|
- 搜索能力依赖业务表结构
|
|
|
|
|
|
- 不像真正搜索引擎
|
|
|
|
|
|
- 不利于后续继续增加来源
|
|
|
|
|
|
|
|
|
|
|
|
## 6.2 为什么不选向量搜索
|
|
|
|
|
|
|
|
|
|
|
|
向量搜索至少需要:
|
|
|
|
|
|
|
|
|
|
|
|
1. 文本 embedding 模型
|
|
|
|
|
|
2. 向量字段
|
|
|
|
|
|
3. 相似度检索能力
|
|
|
|
|
|
|
|
|
|
|
|
当前对话里已明确:
|
|
|
|
|
|
|
|
|
|
|
|
- 不做向量搜索
|
|
|
|
|
|
- 不用大模型
|
|
|
|
|
|
|
|
|
|
|
|
所以本次方案不能再写成 `Vector Search`。
|
|
|
|
|
|
|
|
|
|
|
|
## 6.3 为什么选 MySQL FULLTEXT + 独立搜索表
|
|
|
|
|
|
|
|
|
|
|
|
优点:
|
|
|
|
|
|
|
|
|
|
|
|
1. 不依赖外部搜索引擎
|
|
|
|
|
|
2. 不依赖模型
|
|
|
|
|
|
3. 能明显优于当前 `LIKE` 方案
|
|
|
|
|
|
4. 可以保留原有 DTO 和接口
|
|
|
|
|
|
5. 可先做成普通搜索引擎,后续再升级成 ES/向量检索
|
|
|
|
|
|
|
|
|
|
|
|
## 6.4 为什么引入 EF Core 10
|
|
|
|
|
|
|
|
|
|
|
|
虽然主项目主链路是 `SqlSugar + Furion`,但搜索子系统可以独立引入 EF Core,理由如下:
|
|
|
|
|
|
|
|
|
|
|
|
1. 搜索索引表是新增子系统,不必受当前 MyBatis 兼容层约束
|
|
|
|
|
|
2. EF Core 对“独立读写模型 + 独立迁移 + 查询表达式”很合适
|
|
|
|
|
|
3. 后续若要演进到更多数据库特性,EF Core 子系统维护更清晰
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 7. 改造后的目标架构
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
用户请求 /portal/search
|
|
|
|
|
|
↓
|
|
|
|
|
|
HwSearchController
|
|
|
|
|
|
↓
|
|
|
|
|
|
HwSearchService(新)
|
|
|
|
|
|
↓
|
|
|
|
|
|
HwPortalSearchDbContext
|
|
|
|
|
|
↓
|
|
|
|
|
|
hw_portal_search_doc(搜索索引表)
|
|
|
|
|
|
↓
|
|
|
|
|
|
MySQL FULLTEXT / LIKE 兜底
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
业务数据更新流程:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
业务保存成功(页面/菜单/文档/配置类型)
|
|
|
|
|
|
↓
|
|
|
|
|
|
对应业务 Service
|
|
|
|
|
|
↓
|
|
|
|
|
|
IHwSearchIndexService.UpsertAsync(...)
|
|
|
|
|
|
↓
|
|
|
|
|
|
PortalSearchDocConverter
|
|
|
|
|
|
↓
|
|
|
|
|
|
hw_portal_search_doc
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 8. 数据库设计方案
|
|
|
|
|
|
|
|
|
|
|
|
## 8.1 新增搜索索引表
|
|
|
|
|
|
|
|
|
|
|
|
建议表名:
|
|
|
|
|
|
|
|
|
|
|
|
```sql
|
|
|
|
|
|
hw_portal_search_doc
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
建议字段:
|
|
|
|
|
|
|
|
|
|
|
|
```sql
|
|
|
|
|
|
CREATE TABLE `hw_portal_search_doc` (
|
|
|
|
|
|
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
|
|
|
|
|
`doc_id` VARCHAR(128) NOT NULL COMMENT '文档唯一标识,例如 menu:1、web:7',
|
|
|
|
|
|
`source_type` VARCHAR(32) NOT NULL COMMENT '来源类型:menu/web/web1/document/configType',
|
|
|
|
|
|
`biz_id` VARCHAR(64) NULL COMMENT '业务主键字符串',
|
|
|
|
|
|
`title` VARCHAR(500) NULL COMMENT '搜索标题',
|
|
|
|
|
|
`content` LONGTEXT NULL COMMENT '搜索正文',
|
|
|
|
|
|
`web_code` VARCHAR(64) NULL COMMENT '页面编码',
|
|
|
|
|
|
`type_id` VARCHAR(64) NULL COMMENT '类型ID',
|
|
|
|
|
|
`device_id` VARCHAR(64) NULL COMMENT '设备ID',
|
|
|
|
|
|
`menu_id` VARCHAR(64) NULL COMMENT '菜单ID',
|
|
|
|
|
|
`document_id` VARCHAR(64) NULL COMMENT '文档ID',
|
|
|
|
|
|
`base_score` INT NOT NULL DEFAULT 0 COMMENT '基础分',
|
|
|
|
|
|
`route` VARCHAR(255) NULL COMMENT '展示路由',
|
|
|
|
|
|
`route_query_json` JSON NULL COMMENT '展示路由参数',
|
|
|
|
|
|
`edit_route` VARCHAR(255) NULL COMMENT '编辑路由',
|
|
|
|
|
|
`is_delete` CHAR(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除',
|
|
|
|
|
|
`updated_at` DATETIME NULL COMMENT '业务更新时间',
|
|
|
|
|
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '索引创建时间',
|
|
|
|
|
|
`modified_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '索引更新时间',
|
|
|
|
|
|
PRIMARY KEY (`id`),
|
|
|
|
|
|
UNIQUE KEY `uk_hw_portal_search_doc_doc_id` (`doc_id`),
|
|
|
|
|
|
KEY `idx_hw_portal_search_doc_source_type` (`source_type`),
|
|
|
|
|
|
KEY `idx_hw_portal_search_doc_updated_at` (`updated_at`)
|
|
|
|
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='hw-portal 搜索索引表';
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
全文索引:
|
|
|
|
|
|
|
|
|
|
|
|
```sql
|
|
|
|
|
|
ALTER TABLE `hw_portal_search_doc`
|
|
|
|
|
|
ADD FULLTEXT KEY `ft_hw_portal_search_doc_title_content` (`title`, `content`);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 8.2 设计理由
|
|
|
|
|
|
|
|
|
|
|
|
- `doc_id`:保证一个业务对象只对应一条搜索文档
|
|
|
|
|
|
- `source_type`:保留来源分类
|
|
|
|
|
|
- `title/content`:统一搜索输入
|
|
|
|
|
|
- `route/edit_route`:避免查询后再做过多二次拼装
|
|
|
|
|
|
- `updated_at`:用于排序
|
|
|
|
|
|
- `base_score`:保留当前旧搜索里“不同来源不同基础权重”的思路
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 9. 代码改造总览
|
|
|
|
|
|
|
|
|
|
|
|
## 9.1 现有代码保留
|
|
|
|
|
|
|
|
|
|
|
|
保留但用途调整:
|
|
|
|
|
|
|
|
|
|
|
|
- [HwSearchController.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwSearchController.cs)
|
|
|
|
|
|
- [HwSearchAdminController.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwSearchAdminController.cs)
|
|
|
|
|
|
- [SearchPageDTO.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/SearchPageDTO.cs)
|
|
|
|
|
|
- [SearchResultDTO.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/SearchResultDTO.cs)
|
|
|
|
|
|
- [PortalSearchDocConverter.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Search/PortalSearchDocConverter.cs)
|
|
|
|
|
|
|
|
|
|
|
|
## 9.2 现有代码废弃或降级为兼容模式
|
|
|
|
|
|
|
|
|
|
|
|
- [HwSearchMapper.xml](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwSearchMapper.xml)
|
|
|
|
|
|
- [HwSearchService.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Search/HwSearchService.cs) 当前 XML 方案
|
|
|
|
|
|
- [HwNoopSearchRebuildService.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Search/HwNoopSearchRebuildService.cs)
|
|
|
|
|
|
|
|
|
|
|
|
这些文件可以:
|
|
|
|
|
|
|
|
|
|
|
|
- 保留一版作为 `legacy` 回退
|
|
|
|
|
|
- 或重命名为 `LegacyHwSearchService`
|
|
|
|
|
|
|
|
|
|
|
|
## 9.3 建议新增文件位置
|
|
|
|
|
|
|
|
|
|
|
|
推荐新增目录:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
Admin.NET\Plugins\Admin.NET.Plugin.HwPortal\SearchEf
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
建议文件:
|
|
|
|
|
|
|
|
|
|
|
|
- `SearchEf/HwPortalSearchDbContext.cs`
|
|
|
|
|
|
- `SearchEf/Entity/HwPortalSearchDoc.cs`
|
|
|
|
|
|
- `SearchEf/Option/HwPortalSearchOptions.cs`
|
|
|
|
|
|
- `SearchEf/Service/IHwSearchIndexService.cs`
|
|
|
|
|
|
- `SearchEf/Service/HwSearchIndexService.cs`
|
|
|
|
|
|
- `SearchEf/Service/HwSearchQueryService.cs`
|
|
|
|
|
|
- `SearchEf/Service/HwSearchRebuildService.cs`
|
|
|
|
|
|
- `SearchEf/Extensions/HwPortalSearchServiceCollectionExtensions.cs`
|
|
|
|
|
|
|
|
|
|
|
|
如希望保持插件结构更规整,也可以落在:
|
|
|
|
|
|
|
|
|
|
|
|
- `Entity/Search`
|
|
|
|
|
|
- `Service/SearchEf`
|
|
|
|
|
|
- `Option`
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 10. 具体代码设计
|
|
|
|
|
|
|
|
|
|
|
|
## 10.1 搜索索引实体
|
|
|
|
|
|
|
|
|
|
|
|
建议文件:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
Admin.NET\Plugins\Admin.NET.Plugin.HwPortal\SearchEf\Entity\HwPortalSearchDoc.cs
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
建议代码:
|
|
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
|
|
using System.ComponentModel.DataAnnotations;
|
|
|
|
|
|
using System.ComponentModel.DataAnnotations.Schema;
|
|
|
|
|
|
|
|
|
|
|
|
namespace Admin.NET.Plugin.HwPortal.SearchEf.Entity;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// hw-portal 搜索索引实体。
|
|
|
|
|
|
/// 这不是业务原表,而是专门为搜索准备的索引表。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[Table("hw_portal_search_doc")]
|
|
|
|
|
|
[Index(nameof(DocId), IsUnique = true)]
|
|
|
|
|
|
[Index(nameof(SourceType))]
|
|
|
|
|
|
[Index(nameof(UpdatedAt))]
|
|
|
|
|
|
public class HwPortalSearchDoc
|
|
|
|
|
|
{
|
|
|
|
|
|
[Key]
|
|
|
|
|
|
public long Id { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 文档唯一键。
|
|
|
|
|
|
/// 例如:menu:12、web:7、document:abc001。
|
|
|
|
|
|
/// 用它实现幂等更新。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[Required]
|
|
|
|
|
|
[MaxLength(128)]
|
|
|
|
|
|
public string DocId { get; set; } = string.Empty;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 来源类型。
|
|
|
|
|
|
/// 值域与当前 PortalSearchDocConverter 里的常量保持一致。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[Required]
|
|
|
|
|
|
[MaxLength(32)]
|
|
|
|
|
|
public string SourceType { get; set; } = string.Empty;
|
|
|
|
|
|
|
|
|
|
|
|
[MaxLength(64)]
|
|
|
|
|
|
public string? BizId { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
[MaxLength(500)]
|
|
|
|
|
|
public string? Title { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 正文。
|
|
|
|
|
|
/// 这里通常会存“已抽取、已清洗”的文本,而不是原始 JSON。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public string? Content { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
[MaxLength(64)]
|
|
|
|
|
|
public string? WebCode { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
[MaxLength(64)]
|
|
|
|
|
|
public string? TypeId { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
[MaxLength(64)]
|
|
|
|
|
|
public string? DeviceId { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
[MaxLength(64)]
|
|
|
|
|
|
public string? MenuId { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
[MaxLength(64)]
|
|
|
|
|
|
public string? DocumentId { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 基础分。
|
|
|
|
|
|
/// 保留旧系统“菜单比正文更高”的评分思想。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public int BaseScore { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
[MaxLength(255)]
|
|
|
|
|
|
public string? Route { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
public string? RouteQueryJson { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
[MaxLength(255)]
|
|
|
|
|
|
public string? EditRoute { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
[MaxLength(1)]
|
|
|
|
|
|
public string IsDelete { get; set; } = "0";
|
|
|
|
|
|
|
|
|
|
|
|
public DateTime? UpdatedAt { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
public DateTime CreatedAt { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
public DateTime ModifiedAt { get; set; }
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 10.2 搜索 DbContext
|
|
|
|
|
|
|
|
|
|
|
|
建议文件:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
Admin.NET\Plugins\Admin.NET.Plugin.HwPortal\SearchEf\HwPortalSearchDbContext.cs
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
建议代码:
|
|
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
using Admin.NET.Plugin.HwPortal.SearchEf.Entity;
|
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
|
|
|
|
|
|
|
|
namespace Admin.NET.Plugin.HwPortal.SearchEf;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 搜索子系统专用 DbContext。
|
|
|
|
|
|
/// 只管理搜索相关表,不把整个 hw-portal 都切到 EF Core。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class HwPortalSearchDbContext : DbContext
|
|
|
|
|
|
{
|
|
|
|
|
|
public HwPortalSearchDbContext(DbContextOptions<HwPortalSearchDbContext> options) : base(options)
|
|
|
|
|
|
{
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public DbSet<HwPortalSearchDoc> SearchDocs => Set<HwPortalSearchDoc>();
|
|
|
|
|
|
|
|
|
|
|
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
|
|
|
|
{
|
|
|
|
|
|
base.OnModelCreating(modelBuilder);
|
|
|
|
|
|
|
|
|
|
|
|
// Why:
|
|
|
|
|
|
// 这里做 EF 层的结构约束声明。
|
|
|
|
|
|
// FULLTEXT 索引建议仍通过 SQL 脚本或迁移脚本手工加,避免 provider 差异。
|
|
|
|
|
|
modelBuilder.Entity<HwPortalSearchDoc>(entity =>
|
|
|
|
|
|
{
|
|
|
|
|
|
entity.Property(x => x.Content).HasColumnType("longtext");
|
|
|
|
|
|
entity.Property(x => x.RouteQueryJson).HasColumnType("json");
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 10.3 搜索索引服务接口
|
|
|
|
|
|
|
|
|
|
|
|
建议文件:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
Admin.NET\Plugins\Admin.NET.Plugin.HwPortal\SearchEf\Service\IHwSearchIndexService.cs
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
建议代码:
|
|
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
namespace Admin.NET.Plugin.HwPortal.SearchEf.Service;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 搜索索引服务接口。
|
|
|
|
|
|
/// 负责把业务对象同步成搜索文档。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public interface IHwSearchIndexService
|
|
|
|
|
|
{
|
|
|
|
|
|
Task UpsertMenuAsync(HwWebMenu menu, CancellationToken cancellationToken = default);
|
|
|
|
|
|
|
|
|
|
|
|
Task UpsertWebAsync(HwWeb web, CancellationToken cancellationToken = default);
|
|
|
|
|
|
|
|
|
|
|
|
Task UpsertWeb1Async(HwWeb1 web1, CancellationToken cancellationToken = default);
|
|
|
|
|
|
|
|
|
|
|
|
Task UpsertDocumentAsync(HwWebDocument document, CancellationToken cancellationToken = default);
|
|
|
|
|
|
|
|
|
|
|
|
Task UpsertConfigTypeAsync(HwPortalConfigType configType, CancellationToken cancellationToken = default);
|
|
|
|
|
|
|
|
|
|
|
|
Task DeleteByDocIdAsync(string docId, CancellationToken cancellationToken = default);
|
|
|
|
|
|
|
|
|
|
|
|
Task RebuildAllAsync(CancellationToken cancellationToken = default);
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 10.4 搜索查询服务
|
|
|
|
|
|
|
|
|
|
|
|
建议文件:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
Admin.NET\Plugins\Admin.NET.Plugin.HwPortal\SearchEf\Service\HwSearchQueryService.cs
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
建议代码片段:
|
|
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
using Admin.NET.Plugin.HwPortal.SearchEf.Entity;
|
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
|
|
|
|
|
|
|
|
namespace Admin.NET.Plugin.HwPortal.SearchEf.Service;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 搜索查询服务。
|
|
|
|
|
|
/// 负责对索引表做查询,而不是直接查业务表。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class HwSearchQueryService
|
|
|
|
|
|
{
|
|
|
|
|
|
private readonly HwPortalSearchDbContext _db;
|
|
|
|
|
|
|
|
|
|
|
|
public HwSearchQueryService(HwPortalSearchDbContext db)
|
|
|
|
|
|
{
|
|
|
|
|
|
_db = db;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task<List<HwPortalSearchDoc>> SearchAsync(string keyword, int take, CancellationToken cancellationToken = default)
|
|
|
|
|
|
{
|
|
|
|
|
|
keyword = keyword.Trim();
|
|
|
|
|
|
|
|
|
|
|
|
// 第一阶段:先走普通 LIKE 版本,确保功能先跑通。
|
|
|
|
|
|
// 第二阶段:再补 FromSqlRaw + MATCH AGAINST 做 FULLTEXT 优化。
|
|
|
|
|
|
IQueryable<HwPortalSearchDoc> query = _db.SearchDocs
|
|
|
|
|
|
.AsNoTracking()
|
|
|
|
|
|
.Where(x => x.IsDelete == "0")
|
|
|
|
|
|
.Where(x =>
|
|
|
|
|
|
(x.Title != null && EF.Functions.Like(x.Title, $"%{keyword}%")) ||
|
|
|
|
|
|
(x.Content != null && EF.Functions.Like(x.Content, $"%{keyword}%")))
|
|
|
|
|
|
.OrderByDescending(x => x.BaseScore)
|
|
|
|
|
|
.ThenByDescending(x => x.UpdatedAt);
|
|
|
|
|
|
|
|
|
|
|
|
return await query.Take(take).ToListAsync(cancellationToken);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
说明:
|
|
|
|
|
|
|
|
|
|
|
|
- 第一阶段先保证 EF Core 路径可用。
|
|
|
|
|
|
- 第二阶段建议改成:
|
|
|
|
|
|
- `FromSqlInterpolated`
|
|
|
|
|
|
- `MATCH(title, content) AGAINST (...)`
|
|
|
|
|
|
- 这样能兼顾“先实现”和“后优化”。
|
|
|
|
|
|
|
|
|
|
|
|
## 10.5 搜索重建服务
|
|
|
|
|
|
|
|
|
|
|
|
建议文件:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
Admin.NET\Plugins\Admin.NET.Plugin.HwPortal\SearchEf\Service\HwSearchRebuildService.cs
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
建议代码片段:
|
|
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
namespace Admin.NET.Plugin.HwPortal.SearchEf.Service;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 全量重建搜索索引。
|
|
|
|
|
|
/// 当前管理后台的 /portal/search/admin/rebuild 最终会调用这里。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class HwSearchRebuildService : IHwSearchRebuildService
|
|
|
|
|
|
{
|
|
|
|
|
|
private readonly HwPortalSearchDbContext _db;
|
|
|
|
|
|
private readonly IHwSearchIndexService _indexService;
|
|
|
|
|
|
private readonly HwWebMenuService _menuService;
|
|
|
|
|
|
private readonly HwWebService _webService;
|
|
|
|
|
|
private readonly HwWeb1Service _web1Service;
|
|
|
|
|
|
private readonly HwWebDocumentService _documentService;
|
|
|
|
|
|
private readonly HwPortalConfigTypeService _configTypeService;
|
|
|
|
|
|
|
|
|
|
|
|
public HwSearchRebuildService(
|
|
|
|
|
|
HwPortalSearchDbContext db,
|
|
|
|
|
|
IHwSearchIndexService indexService,
|
|
|
|
|
|
HwWebMenuService menuService,
|
|
|
|
|
|
HwWebService webService,
|
|
|
|
|
|
HwWeb1Service web1Service,
|
|
|
|
|
|
HwWebDocumentService documentService,
|
|
|
|
|
|
HwPortalConfigTypeService configTypeService)
|
|
|
|
|
|
{
|
|
|
|
|
|
_db = db;
|
|
|
|
|
|
_indexService = indexService;
|
|
|
|
|
|
_menuService = menuService;
|
|
|
|
|
|
_webService = webService;
|
|
|
|
|
|
_web1Service = web1Service;
|
|
|
|
|
|
_documentService = documentService;
|
|
|
|
|
|
_configTypeService = configTypeService;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task RebuildAllAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
// Why:
|
|
|
|
|
|
// 全量重建时,先清空索引表,再重新从业务表扫描一次。
|
|
|
|
|
|
await _db.Database.ExecuteSqlRawAsync("TRUNCATE TABLE hw_portal_search_doc;");
|
|
|
|
|
|
|
|
|
|
|
|
List<HwWebMenu> menus = await _menuService.SelectHwWebMenuList(new HwWebMenu());
|
|
|
|
|
|
foreach (HwWebMenu item in menus)
|
|
|
|
|
|
{
|
|
|
|
|
|
await _indexService.UpsertMenuAsync(item);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
List<HwWeb> webs = await _webService.SelectHwWebList(new HwWeb());
|
|
|
|
|
|
foreach (HwWeb item in webs)
|
|
|
|
|
|
{
|
|
|
|
|
|
await _indexService.UpsertWebAsync(item);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 其余来源同理继续补齐。
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 10.6 与现有控制器对接
|
|
|
|
|
|
|
|
|
|
|
|
保持现有文件位置不变:
|
|
|
|
|
|
|
|
|
|
|
|
- [HwSearchController.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwSearchController.cs)
|
|
|
|
|
|
- [HwSearchAdminController.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwSearchAdminController.cs)
|
|
|
|
|
|
|
|
|
|
|
|
建议改造方式:
|
|
|
|
|
|
|
|
|
|
|
|
- 控制器路径不改
|
|
|
|
|
|
- 入参不改
|
|
|
|
|
|
- 返回 DTO 不改
|
|
|
|
|
|
- 内部调用从旧 `HwSearchService` 切到新 `HwSearchQueryService`
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 11. 关键代码改造位置
|
|
|
|
|
|
|
|
|
|
|
|
## 11.1 插件启动注册
|
|
|
|
|
|
|
|
|
|
|
|
当前文件:
|
|
|
|
|
|
|
|
|
|
|
|
- [Startup.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Startup.cs)
|
|
|
|
|
|
|
|
|
|
|
|
当前问题:
|
|
|
|
|
|
|
|
|
|
|
|
- 只注册了 XML 执行器
|
|
|
|
|
|
- `IHwSearchRebuildService` 绑定的是空实现
|
|
|
|
|
|
|
|
|
|
|
|
建议新增注册:
|
|
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
|
|
|
|
|
|
|
|
public void ConfigureServices(IServiceCollection services)
|
|
|
|
|
|
{
|
|
|
|
|
|
services.AddSingleton<HwPortalMapperRegistry>();
|
|
|
|
|
|
services.AddScoped<HwPortalMyBatisExecutor>();
|
|
|
|
|
|
services.AddScoped<PortalSearchDocConverter>();
|
|
|
|
|
|
|
|
|
|
|
|
// 新增:注册搜索专用 DbContext。
|
|
|
|
|
|
// Why:
|
|
|
|
|
|
// 搜索子系统是独立读写模型,不和 SqlSugar 主链路抢职责。
|
|
|
|
|
|
services.AddDbContext<HwPortalSearchDbContext>(options =>
|
|
|
|
|
|
{
|
|
|
|
|
|
options.UseMySql(
|
|
|
|
|
|
"这里读取配置中的搜索连接串",
|
|
|
|
|
|
ServerVersion.AutoDetect("这里读取配置中的搜索连接串"));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
services.AddScoped<IHwSearchIndexService, HwSearchIndexService>();
|
|
|
|
|
|
services.AddScoped<HwSearchQueryService>();
|
|
|
|
|
|
services.AddScoped<IHwSearchRebuildService, HwSearchRebuildService>();
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 11.2 业务服务改造点
|
|
|
|
|
|
|
|
|
|
|
|
需要接入索引增量同步的文件:
|
|
|
|
|
|
|
|
|
|
|
|
- [HwWebService.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwWebService.cs)
|
|
|
|
|
|
- [HwWeb1Service.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwWeb1Service.cs)
|
|
|
|
|
|
- [HwWebMenuService.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwWebMenuService.cs)
|
|
|
|
|
|
- [HwWebDocumentService.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwWebDocumentService.cs)
|
|
|
|
|
|
- [HwPortalConfigTypeService.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwPortalConfigTypeService.cs)
|
|
|
|
|
|
|
|
|
|
|
|
建议改造模式:
|
|
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
public class HwWebService : ITransient
|
|
|
|
|
|
{
|
|
|
|
|
|
private readonly IHwSearchIndexService _searchIndexService;
|
|
|
|
|
|
|
|
|
|
|
|
public async Task<int> InsertHwWeb(HwWeb input)
|
|
|
|
|
|
{
|
|
|
|
|
|
long identity = await _executor.InsertReturnIdentityAsync(Mapper, "insertHwWeb", input);
|
|
|
|
|
|
input.WebId = identity;
|
|
|
|
|
|
|
|
|
|
|
|
if (identity > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Why:
|
|
|
|
|
|
// 不再做全量重建,而是只更新这一个文档的搜索索引。
|
|
|
|
|
|
await _searchIndexService.UpsertWebAsync(input);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return identity > 0 ? 1 : 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 12. 配置设计
|
|
|
|
|
|
|
|
|
|
|
|
建议新增配置文件:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
Admin.NET\Admin.NET.Application\Configuration\Search.json
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
建议内容:
|
|
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json",
|
|
|
|
|
|
"HwPortalSearch": {
|
|
|
|
|
|
"Engine": "mysql_fulltext",
|
|
|
|
|
|
"EnableLegacyFallback": true,
|
|
|
|
|
|
"ConnectionString": "Server=localhost;Database=hw_portal;Uid=root;Pwd=123456;CharSet=utf8mb4;",
|
|
|
|
|
|
"BatchSize": 500,
|
|
|
|
|
|
"TakeLimit": 500
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
建议新增配置类:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
Admin.NET\Plugins\Admin.NET.Plugin.HwPortal\SearchEf\Option\HwPortalSearchOptions.cs
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 13. 返回结构兼容要求
|
|
|
|
|
|
|
|
|
|
|
|
现有返回 DTO 不变:
|
|
|
|
|
|
|
|
|
|
|
|
- [SearchPageDTO.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/SearchPageDTO.cs)
|
|
|
|
|
|
- [SearchResultDTO.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/SearchResultDTO.cs)
|
|
|
|
|
|
|
|
|
|
|
|
必须保持:
|
|
|
|
|
|
|
|
|
|
|
|
### SearchPageDTO
|
|
|
|
|
|
|
|
|
|
|
|
- `total`
|
|
|
|
|
|
- `rows`
|
|
|
|
|
|
|
|
|
|
|
|
### SearchResultDTO
|
|
|
|
|
|
|
|
|
|
|
|
- `sourceType`
|
|
|
|
|
|
- `title`
|
|
|
|
|
|
- `snippet`
|
|
|
|
|
|
- `score`
|
|
|
|
|
|
- `route`
|
|
|
|
|
|
- `routeQuery`
|
|
|
|
|
|
- `editRoute`
|
|
|
|
|
|
|
|
|
|
|
|
这样前端调用逻辑不用改。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 14. 评分策略建议
|
|
|
|
|
|
|
|
|
|
|
|
建议沿用当前评分思路并做增强:
|
|
|
|
|
|
|
|
|
|
|
|
### 14.1 基础分
|
|
|
|
|
|
|
|
|
|
|
|
- menu: 110
|
|
|
|
|
|
- web1: 90
|
|
|
|
|
|
- web: 80
|
|
|
|
|
|
- document: 70
|
|
|
|
|
|
- configType: 65
|
|
|
|
|
|
|
|
|
|
|
|
### 14.2 二次加分
|
|
|
|
|
|
|
|
|
|
|
|
- 标题命中:+20
|
|
|
|
|
|
- 正文命中:+10
|
|
|
|
|
|
- 最近更新时间更近:排序优先
|
|
|
|
|
|
|
|
|
|
|
|
### 14.3 后续可扩展
|
|
|
|
|
|
|
|
|
|
|
|
- 标题完整命中额外加分
|
|
|
|
|
|
- 命中次数加权
|
|
|
|
|
|
- 前台搜索与编辑搜索使用不同的加权策略
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 15. 方案实施步骤
|
|
|
|
|
|
|
|
|
|
|
|
## 15.1 第一步:搭建 EF Core 搜索基础设施
|
|
|
|
|
|
|
|
|
|
|
|
1. 引入 EF Core 10 MySQL Provider
|
|
|
|
|
|
2. 新建 `HwPortalSearchDbContext`
|
|
|
|
|
|
3. 新建 `HwPortalSearchDoc` 实体
|
|
|
|
|
|
4. 新建 `Search.json` 与配置对象
|
|
|
|
|
|
5. 在插件 `Startup` 中注册
|
|
|
|
|
|
|
|
|
|
|
|
## 15.2 第二步:建搜索索引表
|
|
|
|
|
|
|
|
|
|
|
|
1. 建表 `hw_portal_search_doc`
|
|
|
|
|
|
2. 建唯一索引
|
|
|
|
|
|
3. 建普通索引
|
|
|
|
|
|
4. 建 FULLTEXT 索引
|
|
|
|
|
|
|
|
|
|
|
|
## 15.3 第三步:实现索引同步
|
|
|
|
|
|
|
|
|
|
|
|
1. 补 `IHwSearchIndexService`
|
|
|
|
|
|
2. 实现 `UpsertMenuAsync`
|
|
|
|
|
|
3. 实现 `UpsertWebAsync`
|
|
|
|
|
|
4. 实现 `UpsertWeb1Async`
|
|
|
|
|
|
5. 实现 `UpsertDocumentAsync`
|
|
|
|
|
|
6. 实现 `UpsertConfigTypeAsync`
|
|
|
|
|
|
|
|
|
|
|
|
## 15.4 第四步:实现查询服务
|
|
|
|
|
|
|
|
|
|
|
|
1. 新建 `HwSearchQueryService`
|
|
|
|
|
|
2. 先实现 `LIKE` 版 EF 查询
|
|
|
|
|
|
3. 再补 `MATCH AGAINST`
|
|
|
|
|
|
4. 复用现有摘要、高亮、路由逻辑
|
|
|
|
|
|
|
|
|
|
|
|
## 15.5 第五步:替换控制器调用
|
|
|
|
|
|
|
|
|
|
|
|
1. 保持控制器不变
|
|
|
|
|
|
2. 改内部服务依赖
|
|
|
|
|
|
3. 管理端重建接口切到真正重建服务
|
|
|
|
|
|
|
|
|
|
|
|
## 15.6 第六步:移除空实现
|
|
|
|
|
|
|
|
|
|
|
|
1. 去掉 `HwNoopSearchRebuildService` 默认绑定
|
|
|
|
|
|
2. 保留旧 XML 搜索作为 `legacy` 回退实现
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 16. 测试方案
|
|
|
|
|
|
|
|
|
|
|
|
## 16.1 功能测试
|
|
|
|
|
|
|
|
|
|
|
|
1. 搜“工业物联网”,能返回多来源结果
|
|
|
|
|
|
2. 前台搜索返回 `route`
|
|
|
|
|
|
3. 编辑搜索返回 `editRoute`
|
|
|
|
|
|
4. 标题命中排在正文命中前面
|
|
|
|
|
|
5. 空结果时返回空分页对象
|
|
|
|
|
|
|
|
|
|
|
|
## 16.2 增量同步测试
|
|
|
|
|
|
|
|
|
|
|
|
1. 新增页面后立即可搜
|
|
|
|
|
|
2. 修改文档标题后立即可搜到新标题
|
|
|
|
|
|
3. 删除菜单后搜索不再命中
|
|
|
|
|
|
|
|
|
|
|
|
## 16.3 重建测试
|
|
|
|
|
|
|
|
|
|
|
|
1. 执行 `/portal/search/admin/rebuild`
|
|
|
|
|
|
2. 清空索引表后重新生成
|
|
|
|
|
|
3. 重建后结果数与原业务数据量匹配
|
|
|
|
|
|
|
|
|
|
|
|
## 16.4 性能测试
|
|
|
|
|
|
|
|
|
|
|
|
1. 对比旧版 `UNION ALL + LIKE`
|
|
|
|
|
|
2. 高频关键词响应时间明显下降
|
|
|
|
|
|
3. 数据量增长后查询性能仍可接受
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 17. 风险与注意事项
|
|
|
|
|
|
|
|
|
|
|
|
## 17.1 风险点
|
|
|
|
|
|
|
|
|
|
|
|
1. 当前仓库还未引入 EF Core 依赖
|
|
|
|
|
|
2. 当前本机没有 .NET SDK,暂时不能做真实编译验证
|
|
|
|
|
|
3. MySQL FULLTEXT 对中文分词能力有限
|
|
|
|
|
|
4. 如果业务文本大量是 JSON,必须先清洗文本,否则全文检索效果很差
|
|
|
|
|
|
|
|
|
|
|
|
## 17.2 风险应对
|
|
|
|
|
|
|
|
|
|
|
|
1. 先保留旧搜索实现作为 `legacy` 回退
|
|
|
|
|
|
2. 搜索文档始终存“清洗后的文本”
|
|
|
|
|
|
3. 先做功能正确,再做 MATCH AGAINST 优化
|
|
|
|
|
|
4. 若后续中文搜索质量不够,再评估:
|
|
|
|
|
|
- ngram parser
|
|
|
|
|
|
- ES
|
|
|
|
|
|
- PostgreSQL + 中文分词
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 18. 当前不做的内容
|
|
|
|
|
|
|
|
|
|
|
|
本次方案明确不做:
|
|
|
|
|
|
|
|
|
|
|
|
1. 向量搜索
|
|
|
|
|
|
2. embedding
|
|
|
|
|
|
3. 大模型召回
|
|
|
|
|
|
4. Elasticsearch
|
|
|
|
|
|
5. 把整个 `hw-portal` 全量迁到 EF Core
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 19. 当前建议的实际落地文件清单
|
|
|
|
|
|
|
|
|
|
|
|
### 19.1 必改
|
|
|
|
|
|
|
|
|
|
|
|
- [Startup.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Startup.cs)
|
|
|
|
|
|
- [HwSearchController.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwSearchController.cs)
|
|
|
|
|
|
- [HwSearchAdminController.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwSearchAdminController.cs)
|
|
|
|
|
|
- [HwWebService.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwWebService.cs)
|
|
|
|
|
|
- [HwWeb1Service.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwWeb1Service.cs)
|
|
|
|
|
|
- [HwWebMenuService.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwWebMenuService.cs)
|
|
|
|
|
|
- [HwWebDocumentService.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwWebDocumentService.cs)
|
|
|
|
|
|
- [HwPortalConfigTypeService.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwPortalConfigTypeService.cs)
|
|
|
|
|
|
|
|
|
|
|
|
### 19.2 建议新增
|
|
|
|
|
|
|
|
|
|
|
|
- `Admin.NET\Plugins\Admin.NET.Plugin.HwPortal\SearchEf\HwPortalSearchDbContext.cs`
|
|
|
|
|
|
- `Admin.NET\Plugins\Admin.NET.Plugin.HwPortal\SearchEf\Entity\HwPortalSearchDoc.cs`
|
|
|
|
|
|
- `Admin.NET\Plugins\Admin.NET.Plugin.HwPortal\SearchEf\Option\HwPortalSearchOptions.cs`
|
|
|
|
|
|
- `Admin.NET\Plugins\Admin.NET.Plugin.HwPortal\SearchEf\Service\IHwSearchIndexService.cs`
|
|
|
|
|
|
- `Admin.NET\Plugins\Admin.NET.Plugin.HwPortal\SearchEf\Service\HwSearchIndexService.cs`
|
|
|
|
|
|
- `Admin.NET\Plugins\Admin.NET.Plugin.HwPortal\SearchEf\Service\HwSearchQueryService.cs`
|
|
|
|
|
|
- `Admin.NET\Plugins\Admin.NET.Plugin.HwPortal\SearchEf\Service\HwSearchRebuildService.cs`
|
|
|
|
|
|
- `Admin.NET\Admin.NET.Application\Configuration\Search.json`
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 20. 总结
|
|
|
|
|
|
|
|
|
|
|
|
当前 `hw-portal` 的搜索还不是“搜索引擎”,本质上仍是业务表上的 SQL 模糊匹配。
|
|
|
|
|
|
本次方案的核心不是“把 LIKE 换成 EF”,而是:
|
|
|
|
|
|
|
|
|
|
|
|
1. 建独立搜索索引表
|
|
|
|
|
|
2. 用 EF Core 10 管理搜索索引子系统
|
|
|
|
|
|
3. 用 MySQL FULLTEXT + LIKE 兜底做普通搜索引擎
|
|
|
|
|
|
4. 保持现有接口和返回结构不变
|
|
|
|
|
|
5. 把空的索引重建能力变成真正可用的全量/增量索引能力
|
|
|
|
|
|
|
|
|
|
|
|
如果后续你要继续深化,我建议下一份文档再单独写:
|
|
|
|
|
|
|
|
|
|
|
|
- 《hw-portal 搜索表 SQL 脚本与迁移方案》
|
|
|
|
|
|
- 《hw-portal 搜索改造详细代码实施清单》
|
|
|
|
|
|
- 《hw-portal 搜索测试用例与验收清单》
|
|
|
|
|
|
|
|
|
|
|
|
最自律帅气聪明的臧辰浩
|