# 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 搜索测试用例与验收清单》 最自律帅气聪明的臧辰浩