酒店数据全文检索练习(3)-定价排名广告置顶

前言

上一节(酒店数据全文检索练习(2)-按距离排序实现离我最近功能)的学习我们实现了按照经纬度坐标实现距离排序,以及价格排序的功能,这一节我们学习广告置顶,竞价排名功能。

我们用百度的时候,有没有看到过这种情况,我们搜索的结果中,排在前面的是广告,而且根据不同公司交的钱不同,你排名的顺序就不一样,如下图。

那么我们如何用Elasticsearch实现这个功能呢?其实用es的相关性算分查询就能实现,相关原理可以去看我之前写的文章elasticsearch之DSL-查询

image-20220121143433761

实现步骤分析

我们要实现上述功能,首先要区分那些文档是广告,那些文档不是,所以我们需要在是广告的文档上打上标记,表明是广告,然后给广告不一样的打分权重,让他排到前面。我们用isAd字段进行标识,首先在HotelDoc中增加字段

private Boolean isAD;

然后选取一些文档,将isAd标记为true

POST /hotel/_update/60214
{
    "doc": {
         "isAD": true
    }
}

POST /hotel/_update/396189
{
    "doc": {
         "isAD": true
    }
}

POST /hotel/_update/609023
{
    "doc": {
         "isAD": true
    }
}

我随便选了三个数据标识为isAdtrue

然后修改查询方法

@Service
@RequiredArgsConstructor
public class HotelService extends ServiceImpl<HotelMapper, Hotel> implements IHotelService {

    private final RestHighLevelClient client;

    @Override
    public ResponseObject list(RequestParams requestParams) {
        SearchRequest request = new SearchRequest("hotel");

        //构建BooleanQuery
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();

        //构建查询语句
        buildBasicQuery(requestParams, boolQuery);

        //分页
        int page = requestParams.getPage();
        int size = requestParams.getSize();
        request.source().from((page - 1) * size).size(size);

        // 排序
        String location = requestParams.getLocation();
        if (StringUtils.isNotEmpty(location)) {
            request.source().sort(SortBuilders
                    .geoDistanceSort("location", new GeoPoint(location))
                    .order(SortOrder.ASC)
                    .unit(DistanceUnit.KILOMETERS) //单位:km
            );
        }

        // 价格和评价分数排序
        String sortBy = requestParams.getSortBy();
        if (StringUtils.isNotEmpty(sortBy)) {
            if ("score".equals(sortBy)) {
                request.source().sort("score", SortOrder.ASC);
            } else if ("price".equals(sortBy)) {
                request.source().sort("price", SortOrder.ASC);
            }
        }

        //算分控制
        FunctionScoreQueryBuilder functionScoreQuery = QueryBuilders.functionScoreQuery(
                // 原始查询,参与算分的查询
                boolQuery,
                // FunctionScore数组
                new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
                        new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                                // 过滤条件  只有广告需要处理权重
                                QueryBuilders.termQuery("isAD", true),
                                // 权重处理 直接乘10
                                ScoreFunctionBuilders.weightFactorFunction(10)
                        )
                }
        );


        request.source().query(functionScoreQuery);

        ResponseObject responseObject = new ResponseObject();
        List<HotelDoc> hotelDocList = new ArrayList<>();
        try {
            SearchResponse response = client.search(request, RequestOptions.DEFAULT);
            SearchHits hits = response.getHits();
            long total = hits.getTotalHits().value;
            responseObject.setTotal(total);
            SearchHit[] searchHits = hits.getHits();
            for (SearchHit searchHit : searchHits) {
                HotelDoc hotelDoc = JSONObject.parseObject(searchHit.getSourceAsString(), HotelDoc.class);
                //处理排序结果
                Object[] sortValues = searchHit.getSortValues();
                if (sortValues != null && sortValues.length != 0) {
                    hotelDoc.setDistance(sortValues[0]);
                }
                hotelDocList.add(hotelDoc);
            }
            responseObject.setHotels(hotelDocList);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return responseObject;
    }

重启项目,查看效果,我们发现刚刚标记的三个都被置顶了,而且前端根据isAD标识,在右上角添加了广告的图标

image-20220121170227295

竞价排名

现在这三个都是一样的,可以按照广告费的高低,实现不用的排名吗?

我们可以使用field_value_factor

例如:使用 likes 点赞数字段进行综合搜索:

{
  "query": {
    "function_score": {
      "query": { "match": { "message": "elasticsearch" } },
      "field_value_factor": {
        "field": "likes",
        "factor": 1.2,
        "missing": 1,
        "modifier": "log1p"
      }
    }
  }
}

说明:

  • field : 参与计算的字段。
  • factor : 乘积因子,默认为 1 ,将会与 field 的字段值相乘。
  • missing : 如果 field 字段不存在则使用 missing 指定的缺省值。
  • modifier : 计算函数,为了避免分数相差过大,用于平滑分数,可以是以下之一:
    • none : 不处理,默认
    • log : log(factor * field_value)
    • log1p : log(1 + factor * field_value)
    • log2p : log(2 + factor * field_value)
    • ln : ln(factor * field_value)
    • ln1p : ln(1 + factor * field_value)
    • ln2p : ln(2 + factor * field_value)
    • square : 平方,(factor * field_value)^2
    • sqrt : 开方,sqrt(factor * field_value)
    • reciprocal : 求倒数,1/(factor * field_value)

假设某个匹配的文档的点赞数是 1000 ,那么例子中其打分函数生成的分数就是 log(1 + 1.2 * 1000),最终的分数是原来的 query 分数与此打分函数分数相差的结果。

代码实现

我们新增一个字段adCost用来表示广告费

POST /hotel/_update/60214
{
    "doc": {
         "adCost": 1000
    }
}

POST /hotel/_update/396189
{
    "doc": {
         "adCost": 2000
    }
}

POST /hotel/_update/609023
{
    "doc": {
         "adCost": 3000
    }
}

HotelDoc中也新增

private Integer adCost;

修改算分逻辑

@Override
    public ResponseObject list(RequestParams requestParams) {
        SearchRequest request = new SearchRequest("hotel");

        //构建BooleanQuery
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();

        //构建查询语句
        buildBasicQuery(requestParams, boolQuery);

        //分页
        int page = requestParams.getPage();
        int size = requestParams.getSize();
        request.source().from((page - 1) * size).size(size);

        // 排序
        String location = requestParams.getLocation();
        if (StringUtils.isNotEmpty(location)) {
            request.source().sort(SortBuilders
                    .geoDistanceSort("location", new GeoPoint(location))
                    .order(SortOrder.ASC)
                    .unit(DistanceUnit.KILOMETERS) //单位:km
            );
        }

        // 价格和评价分数排序
        String sortBy = requestParams.getSortBy();
        if (StringUtils.isNotEmpty(sortBy)) {
            if ("score".equals(sortBy)) {
                request.source().sort("score", SortOrder.ASC);
            } else if ("price".equals(sortBy)) {
                request.source().sort("price", SortOrder.ASC);
            }
        }

        //算分控制
        FunctionScoreQueryBuilder functionScoreQuery = QueryBuilders.functionScoreQuery(
                // 原始查询,参与算分的查询
                boolQuery,
                // FunctionScore数组
                new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
                        new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                                // 过滤条件  只有广告需要处理权重
                                QueryBuilders.termQuery("isAD", true),
                                // 算分
                                ScoreFunctionBuilders.fieldValueFactorFunction("adCost")
                                        .factor(1.2f)
                                        .missing(1)
                                        .modifier(FieldValueFactorFunction.Modifier.LOG1P)
                        )
                }
        );


        request.source().query(functionScoreQuery);

        ResponseObject responseObject = new ResponseObject();
        List<HotelDoc> hotelDocList = new ArrayList<>();
        try {
            SearchResponse response = client.search(request, RequestOptions.DEFAULT);
            SearchHits hits = response.getHits();
            long total = hits.getTotalHits().value;
            responseObject.setTotal(total);
            SearchHit[] searchHits = hits.getHits();
            for (SearchHit searchHit : searchHits) {
                HotelDoc hotelDoc = JSONObject.parseObject(searchHit.getSourceAsString(), HotelDoc.class);
                //处理排序结果
                Object[] sortValues = searchHit.getSortValues();
                if (sortValues != null && sortValues.length != 0) {
                    hotelDoc.setDistance(sortValues[0]);
                }
                hotelDocList.add(hotelDoc);
            }
            responseObject.setHotels(hotelDocList);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return responseObject;
    }

我们在前端代码上显示一下广告费用,这样方便我们观察

<div v-if="hotel.isAD && hotel.adCost != null">广告费:{{hotel.adCost}} 元</div>

效果展示

启动项目,我们看看效果

image-20220124095227913

可以看到广告在最前面,而且广告费用越高,排名越靠前~
下一节我们讲酒店数据全文检索练习(4)-数据聚合,过滤条件


酒店数据全文检索练习(3)-定价排名广告置顶
https://www.zhaojun.inkhttps://www.zhaojun.ink/archives/1012
作者
卑微幻想家
发布于
2022-01-26
许可协议