# 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;
///
/// hw-portal 搜索索引实体。
/// 这不是业务原表,而是专门为搜索准备的索引表。
///
[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; }
///
/// 文档唯一键。
/// 例如:menu:12、web:7、document:abc001。
/// 用它实现幂等更新。
///
[Required]
[MaxLength(128)]
public string DocId { get; set; } = string.Empty;
///
/// 来源类型。
/// 值域与当前 PortalSearchDocConverter 里的常量保持一致。
///
[Required]
[MaxLength(32)]
public string SourceType { get; set; } = string.Empty;
[MaxLength(64)]
public string? BizId { get; set; }
[MaxLength(500)]
public string? Title { get; set; }
///
/// 正文。
/// 这里通常会存“已抽取、已清洗”的文本,而不是原始 JSON。
///
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; }
///
/// 基础分。
/// 保留旧系统“菜单比正文更高”的评分思想。
///
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;
///
/// 搜索子系统专用 DbContext。
/// 只管理搜索相关表,不把整个 hw-portal 都切到 EF Core。
///
public class HwPortalSearchDbContext : DbContext
{
public HwPortalSearchDbContext(DbContextOptions options) : base(options)
{
}
public DbSet SearchDocs => Set();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Why:
// 这里做 EF 层的结构约束声明。
// FULLTEXT 索引建议仍通过 SQL 脚本或迁移脚本手工加,避免 provider 差异。
modelBuilder.Entity(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;
///
/// 搜索索引服务接口。
/// 负责把业务对象同步成搜索文档。
///
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;
///
/// 搜索查询服务。
/// 负责对索引表做查询,而不是直接查业务表。
///
public class HwSearchQueryService
{
private readonly HwPortalSearchDbContext _db;
public HwSearchQueryService(HwPortalSearchDbContext db)
{
_db = db;
}
public async Task> SearchAsync(string keyword, int take, CancellationToken cancellationToken = default)
{
keyword = keyword.Trim();
// 第一阶段:先走普通 LIKE 版本,确保功能先跑通。
// 第二阶段:再补 FromSqlRaw + MATCH AGAINST 做 FULLTEXT 优化。
IQueryable 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;
///
/// 全量重建搜索索引。
/// 当前管理后台的 /portal/search/admin/rebuild 最终会调用这里。
///
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 menus = await _menuService.SelectHwWebMenuList(new HwWebMenu());
foreach (HwWebMenu item in menus)
{
await _indexService.UpsertMenuAsync(item);
}
List 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();
services.AddScoped();
services.AddScoped();
// 新增:注册搜索专用 DbContext。
// Why:
// 搜索子系统是独立读写模型,不和 SqlSugar 主链路抢职责。
services.AddDbContext(options =>
{
options.UseMySql(
"这里读取配置中的搜索连接串",
ServerVersion.AutoDetect("这里读取配置中的搜索连接串"));
});
services.AddScoped();
services.AddScoped();
services.AddScoped();
}
```
## 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 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 搜索测试用例与验收清单》
最自律帅气聪明的臧辰浩