基于Elasticsearch 9.15的音乐搜索条件匹配实现问询
Elasticsearch 9.15 音乐搜索DSL与.NET客户端实现思路
一、DSL动态构建逻辑
1. 无关键词场景:基础过滤 + 固定排序
过滤条件
- 时长范围匹配:使用
range查询实现minDuration ≤ duration ≤ maxDuration,仅当对应参数存在时加入条件。 - 嵌套字段全匹配:针对
tags/categories/singers/qualities这类嵌套数组,每个传入的参数(如tagId/categoryId等)都通过nested查询+term精确匹配,所有传入参数需同时满足(加入bool.must)。
排序规则
按优先级依次降序排序:
ratings字段直接排序favorites字段直接排序- 自定义评分排序:通过脚本计算
votesUp/(votesUp+votesDown),为避免除以0,可加极小值兜底(如votesUp/(votesUp+votesDown+1e-8))。
2. 有关键词场景:扩展匹配 + 条件分支
关键词分支处理
- ISRC精确匹配:用
term查询直接匹配isrc字段。 - 多字段模糊匹配:用
multi_match(或bool.should组合多个match)覆盖多语言标题(title/title_en/title_cn等)、歌手全名(singers.fullName)、标签名(tags.name)、分类名(categories.name)。嵌套字段需通过nested查询包裹,或提前用copy_to将所有需搜索的字段合并到一个顶级字段简化查询。
动态组合逻辑
所有条件均为可选:仅当对应参数存在时,才将查询加入bool.must(基础过滤)或bool.should(关键词匹配,需设置minimum_should_match:1确保至少匹配一个分支)。
二、.NET客户端(NEST)链式实现示例
以下是基于NEST的链式构建代码,核心思路是逐步向BoolQuery中添加条件,最终组装成完整的搜索请求:
using Nest; // 实体类定义与数据结构对应 public class Music { public int Duration { get; set; } public double Ratings { get; set; } public int Favorites { get; set; } public int VotesUp { get; set; } public int VotesDown { get; set; } public List<MusicTag> Tags { get; set; } public List<MusicCategory> Categories { get; set; } public List<MusicSinger> Singers { get; set; } public List<MusicQuality> Qualities { get; set; } public string Isrc { get; set; } public string Title { get; set; } public string Title_en { get; set; } public string Title_cn { get; set; } public string Title_de { get; set; } public string Title_ja { get; set; } // 其他字段省略 } public class MusicTag { public int TagId { get; set; } public string Name { get; set; } } public class MusicCategory { public int CategoryId { get; set; } public string Name { get; set; } } public class MusicSinger { public int SingerId { get; set; } public string FullName { get; set; } } public class MusicQuality { public int QualityId { get; set; } } // 搜索方法实现 public ISearchResponse<Music> SearchMusic( int? minDuration = null, int? maxDuration = null, List<int> tagIds = null, List<int> categoryIds = null, List<int> singerIds = null, List<int> qualityIds = null, string keyword = null) { var client = new ElasticClient(new Uri("http://your-es-node:9200")); var boolQuery = new BoolQuery(); // 添加时长范围过滤 if (minDuration.HasValue || maxDuration.HasValue) { boolQuery.Must.Add(new RangeQuery { Field = Infer.Field<Music>(m => m.Duration), GreaterThanOrEqualTo = minDuration, LessThanOrEqualTo = maxDuration }); } // 添加标签全匹配 if (tagIds?.Any() == true) { foreach (var tagId in tagIds) { boolQuery.Must.Add(new NestedQuery { Path = Infer.Field<Music>(m => m.Tags), Query = new TermQuery { Field = Infer.Field<MusicTag>(t => t.TagId), Value = tagId } }); } } // 处理分类全匹配 if (categoryIds?.Any() == true) { foreach (var catId in categoryIds) { boolQuery.Must.Add(new NestedQuery { Path = Infer.Field<Music>(m => m.Categories), Query = new TermQuery { Field = Infer.Field<MusicCategory>(c => c.CategoryId), Value = catId } }); } } // 处理歌手全匹配 if (singerIds?.Any() == true) { foreach (var singerId in singerIds) { boolQuery.Must.Add(new NestedQuery { Path = Infer.Field<Music>(m => m.Singers), Query = new TermQuery { Field = Infer.Field<MusicSinger>(s => s.SingerId), Value = singerId } }); } } // 处理音质全匹配 if (qualityIds?.Any() == true) { foreach (var qualityId in qualityIds) { boolQuery.Must.Add(new NestedQuery { Path = Infer.Field<Music>(m => m.Qualities), Query = new TermQuery { Field = Infer.Field<MusicQuality>(q => q.QualityId), Value = qualityId } }); } } // 处理关键词 if (!string.IsNullOrWhiteSpace(keyword)) { // ISRC精确匹配分支 var isrcQuery = new TermQuery { Field = Infer.Field<Music>(m => m.Isrc), Value = keyword }; // 多字段匹配分支(含嵌套字段) var multiMatchQuery = new MultiMatchQuery { Query = keyword, Fields = new[] { Infer.Field<Music>(m => m.Title), Infer.Field<Music>(m => m.Title_en), Infer.Field<Music>(m => m.Title_cn), Infer.Field<Music>(m => m.Title_de), Infer.Field<Music>(m => m.Title_ja), Infer.Field<MusicSinger>(s => s.FullName).AppendParentPath(), Infer.Field<MusicTag>(t => t.Name).AppendParentPath(), Infer.Field<MusicCategory>(c => c.Name).AppendParentPath() }, Type = TextQueryType.CrossFields }; // 两个分支满足其一即可 boolQuery.Should.Add(isrcQuery); boolQuery.Should.Add(multiMatchQuery); boolQuery.MinimumShouldMatch = 1; } // 设置排序规则 var sort = new List<ISort> { new FieldSort { Field = Infer.Field<Music>(m => m.Ratings), Order = SortOrder.Descending }, new FieldSort { Field = Infer.Field<Music>(m => m.Favorites), Order = SortOrder.Descending }, new ScriptSort { Script = new InlineScript("doc['votesUp'].value / (doc['votesUp'].value + doc['votesDown'].value + 1e-8)"), Order = SortOrder.Descending, Type = ScriptSortType.Number } }; // 组装搜索请求 var searchRequest = new SearchRequest<Music> { Query = boolQuery, Sort = sort }; return client.Search<Music>(searchRequest); }
关键注意事项
- 嵌套字段处理:所有嵌套数组的查询必须用
NestedQuery指定路径,否则无法正确匹配嵌套文档。 - 脚本排序容错:添加
1e-8避免votesUp+votesDown=0时的除以0错误。 - 动态条件控制:通过参数是否为空/存在来决定是否加入对应查询,实现条件的动态组合。
- 多语言字段优化:若多语言标题较多,可提前在映射中设置
copy_to: "all_titles",简化multi_match的字段列表。
内容的提问来源于stack exchange,提问作者unruledboy
相关产品推荐
相关产品推荐

