You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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")))
        );
    })
);

关键说明

  1. 权重分配逻辑:通过FunctionScore给三类文档分别设置3、2、1的权重,_score字段就代表了优先级,降序排序后自然满足你的需求。
  2. 避免重叠匹配:使用ScoreMode.First确保一个文档如果同时匹配多个规则(比如既在搜索词里又在周边郊区),会取第一个匹配的高权重,保证搜索词匹配的文档不会被分到郊区组。
  3. 分页兼容性:原有的分页逻辑完全可以保留,因为排序是全局生效的,分页会基于最终的排序结果返回正确的页。
  4. 聚合保留:原有的聚合逻辑不需要修改,依然可以正确统计搜索词匹配的结果数量。

如果需要单独统计另外两类的数量,你可以在聚合里再添加两个Filter聚合,分别对应普通过滤和周边郊区的条件。

内容的提问来源于stack exchange,提问作者Shweta Reddy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 07:21:34