Elasticsearch NEST实现搜索词与周边郊区结果分组排序需求
实现Elasticsearch结果的自定义优先级排序
看起来你已经搭建好了基础的查询逻辑,现在核心需求是给三类结果设置明确的排序优先级:搜索词匹配结果 > 普通过滤结果 > 周边郊区结果,同时还要保留用户选择的次级排序(比如价格、距离、发布时间)。下面我给你一个基于NEST的具体实现方案:
核心思路
我们可以通过FunctionScore查询给不同类别的文档赋予不同的权重,然后将权重作为第一排序条件,再叠加用户选择的排序规则。这样既能保证优先级,又不影响用户自定义的排序需求。
修改后的完整代码示例
double firstTermLatForSort = 0; double firstTermLngForSort = 0; int requestedPage = request.Page > 0 ? request.Page - 1 : 0; FilterContainer termFilters = new FilterContainer(); FilterContainer refineFilters = new FilterContainer(); FilterContainer surroundingFilters = new FilterContainer(); // 先构建基础过滤条件(和你原代码逻辑一致) if (request.Terms != null && request.Terms.Any()) { var i = 1; foreach (var term in request.Terms) { FilterContainer termFilter = new FilterContainer(); if (term.TermType == TermType.Suburb) { termFilter = new FilterDescriptor<ResidentialDetails>().Term(x => x.Address.Suburb, term.Term.ToLower()); if (i == 1) { firstTermLatForSort = term.Location.Latitude; firstTermLngForSort = term.Location.Longitude; } } else if (term.TermType == TermType.Region) { termFilter = new FilterDescriptor<ResidentialDetails>().Term(x => x.Address.Region, term.Term.ToLower()); } else if (term.TermType == TermType.Council) { termFilter = new FilterDescriptor<ResidentialDetails>().Term(x => x.Address.Council, term.Term.ToLower()); } else if (term.TermType == TermType.Postcode) { termFilter = new FilterDescriptor<ResidentialDetails>().Term(x => x.Address.Postcode, term.Term.ToLower()); } if (term.TermType == TermType.State) { termFilters |= (new FilterDescriptor<ResidentialDetails>().Term(x => x.Address.StateAbbr, term.State.ToLower()) || new FilterDescriptor<ResidentialDetails>().Term(x => x.Address.State, term.State.ToLower())); } else { termFilters |= (termFilter && (new FilterDescriptor<ResidentialDetails>().Term(x => x.Address.StateAbbr, term.State.ToLower()) || new FilterDescriptor<ResidentialDetails>().Term(x => x.Address.State, term.State.ToLower()))); if (request.SurroundingSuburbs) { surroundingFilters |= new FilterDescriptor<ResidentialDetails>().GeoDistance(g => g.Address.Location, geoDistanceFilterDescriptor => geoDistanceFilterDescriptor .Location(term.Location.Latitude, term.Location.Longitude) .Distance("5km") .DistanceType(GeoDistance.Arc)); } } i++; } } if (request.PropertyType != null && request.PropertyType.Any()) refineFilters &= new FilterDescriptor<ResidentialDetails>().Terms(x => x.Category, request.PropertyType.Select(p => p.ToLower())); if (request.PriceMin > 0) refineFilters &= new FilterDescriptor<ResidentialDetails>().Range(r => r.OnField(x => x.Price.Price).GreaterOrEquals(request.PriceMin)); if (request.PriceMax > 0) refineFilters &= new FilterDescriptor<ResidentialDetails>().Range(r => r.OnField(x => x.Price.Price).LowerOrEquals(request.PriceMax)); if (request.LandSizeMin > 0) refineFilters &= new FilterDescriptor<ResidentialDetails>().Range(r => r.OnField(x => x.Dimensions.LandAreaSqMeters).GreaterOrEquals(request.LandSizeMin)); if (request.BedsMin > 0) refineFilters &= new FilterDescriptor<ResidentialDetails>().Range(r => r.OnField(x => x.Features.Bedrooms).GreaterOrEquals(request.BedsMin)); if (request.BedsMax > 0) refineFilters &= new FilterDescriptor<ResidentialDetails>().Range(r => r.OnField(x => x.Features.Bedrooms).LowerOrEquals(request.BedsMax)); if (request.BathsMin > 0) refineFilters &= new FilterDescriptor<ResidentialDetails>().Range(r => r.OnField(x => x.Features.Bathrooms).GreaterOrEquals(request.BathsMin)); if (request.CarsMin > 0) refineFilters &= new FilterDescriptor<ResidentialDetails>().Range(r => r.OnField(x => x.Features.TotalCarSpaces).GreaterOrEquals(request.CarsMin)); if (request.ExcludeUnderOffer) { refineFilters &= new FilterDescriptor<ResidentialDetails>().Term(x => x.UnderOffer, false); } // 构建基础查询过滤条件 var baseFilter = termFilters || surroundingFilters; baseFilter &= refineFilters; // 执行搜索,加入FunctionScore实现优先级排序 var exactMatches = await _elasticClient.SearchAsync<ResidentialDetails>(s => s .From(requestedPage * request.PageSize) .Size(request.PageSize > 0 ? request.PageSize : 20) .Query(q => q .FunctionScore(fs => fs .Query(baseQuery => baseQuery.Bool(b => b.Filter(baseFilter))) .Functions(funcs => { // 1. 搜索词匹配的文档,赋予最高权重(3) funcs.Add(f => f .Filter(fil => fil.Bool(b => b.Filter(termFilters && refineFilters))) .Weight(3) ); // 2. 周边郊区的文档,赋予最低权重(1) funcs.Add(f => f .Filter(fil => fil.Bool(b => b.Filter(surroundingFilters && refineFilters))) .Weight(1) ); // 3. 普通过滤文档,赋予中间权重(2) funcs.Add(f => f .Filter(fil => fil.Bool(b => b.Filter(!termFilters && !surroundingFilters && refineFilters))) .Weight(2) ); }) .ScoreMode(FunctionScoreMode.First) // 取第一个匹配的函数权重 .BoostMode(FunctionBoostMode.Replace) // 用函数权重替换原始评分 ) ) .Sort(st => st // 第一排序:按自定义权重降序,保证优先级 .OnField("_score").Descending() // 第二排序:用户选择的排序规则 .ThenBy(tb => { switch (request.Sort) { case Csn.Dto.Homesales.Enums.SortOrder.Distance: return tb.GeoDistance(gd => gd .OnField(x => x.Address.Location) .Order(Nest.SortOrder.Ascending) .Unit(GeoUnit.Kilometers) .Mode(SortMode.Min) .DistanceType(GeoDistance.Plane) .PinTo(firstTermLatForSort, firstTermLngForSort) ); case Csn.Dto.Homesales.Enums.SortOrder.LatestListing: return tb.OnField(x => x.DateCreated).Descending(); case Csn.Dto.Homesales.Enums.SortOrder.OldestListing: return tb.OnField(x => x.DateCreated).Ascending(); case Csn.Dto.Homesales.Enums.SortOrder.PriceHighest: return tb.OnField(x => x.Price.Price).Descending(); case Csn.Dto.Homesales.Enums.SortOrder.PriceLowest: return tb.OnField(x => x.Price.Price).Ascending(); default: // 默认按最新发布排序 return tb.OnField(x => x.DateCreated).Descending(); } }) ) // 保留原有的聚合逻辑 .Aggregations(ag => { return ag.Filter("SearchTermMatchCount", st => st .Filter(f => termFilters && refineFilters) .Aggregations(sag => sag.Terms("SearchTermMatch", t => t.Field(sf => sf.Address.Suburb).OrderAscending("_term"))) ); }) );
关键说明
- 权重分配逻辑:通过
FunctionScore给三类文档分别设置3、2、1的权重,_score字段就代表了优先级,降序排序后自然满足你的需求。 - 避免重叠匹配:使用
ScoreMode.First确保一个文档如果同时匹配多个规则(比如既在搜索词里又在周边郊区),会取第一个匹配的高权重,保证搜索词匹配的文档不会被分到郊区组。 - 分页兼容性:原有的分页逻辑完全可以保留,因为排序是全局生效的,分页会基于最终的排序结果返回正确的页。
- 聚合保留:原有的聚合逻辑不需要修改,依然可以正确统计搜索词匹配的结果数量。
如果需要单独统计另外两类的数量,你可以在聚合里再添加两个Filter聚合,分别对应普通过滤和周边郊区的条件。
内容的提问来源于stack exchange,提问作者Shweta Reddy
相关产品推荐
相关产品推荐

