elasticsearch function_score 频道页排序

·  阅读 286

1:项目背景

     app的频道页面需要针对内容做分发优化,让一些优质的内容得到更多的曝光。频道页涉及一定的定向性(同一个频道页只能出现该类目的内容),并且每一个频道页的体量不是很大。思前想后不建议通过推荐系统做内容过滤,再去做分发。

2:技术方案调研

     之前对elasticsearch funciton_score 有一定的了解,但是不是很深入(阮一鸣大佬的es分享)。查找了相关文档可以通过function_score传入groovy脚本,动态打分。于是在集群上测试相关函数,并且与产品初步讨论相关纬度。通过script_score调整es的打分权重,答案是可行。

POST es_score_test/_search
{
  "_source": ["category_id", "parent_nature_id", "uid", "gmt_create","second_nature_id"],
  "from": 0,
  "size": 100,
  "query": {
    "function_score": {
      "query": {
        "match": {
          "parent_nature_id": "1"
        }
      },
      "functions": [
        {
          "gauss": {
            "gmt_create": {
              "origin": "2019-03-01T14:10:30Z",
              "scale": "6d",
              "offset": "1d"
            }
          }
        },
        {
          "field_value_factor": {
            "field": "click_cout",
            "modifier": "log1p",
            "factor": 0.1
          }
        },
        {
          "script_score": {
            "script": {
              "params": {
                        "threshold": 5,
                        "discount": 0.8,
                        "target":102
                    },
              "source": "double sortScore=0;double price =doc['second_nature_id'].value; double margin = 10; if (price < params.threshold) { sortScore = price * margin / params.target }else {sortScore = price * (1 - params.discount) * margin / params.target} return sortScore"
            }
          }
        },
        {
          "script_score": {
            "script": {
              "params": {
                        "threshold": 5,
                        "discount": 0.8,
                        "target":10
                    },
              "source": "double sortScore=0;double price =doc['second_nature_id'].value; double margin = 10000; if (price < params.threshold) { sortScore = price * margin / params.target }else {sortScore = price * (1 - params.discount) * margin / params.target} return sortScore"
            }
          }
        }
      ],
      "boost_mode": "sum"
    }
  }
}
复制代码

3:我们应该通过那些纬度做(大概列举一些因素)

计算因子权重相关测试代码
上架时间建议优先对于新上的视频做一些打分的倾斜。
最近一周/月的浏览,收藏,配音(log2)可以针对不同的行为打不同的比重 详细见下侧代码
是否高清,会员,人工处理,等等针对不同的因子降级
用户个人偏好的二级标签(历史访问记录)通过画像体系构建用户的历史行为
POST es_score_test/_search
{
"query": {
    "function_score": {
        "query": {
            "match": {
                "parent_nature_id": "356"
            }
        },
    "functions": [{    
        "script_score": {
            "script": "Math.log(doc['click_cout'].value *1 + doc['collect_cout'].value *2 +doc['dub_cout'].value *3)"
        }
     }],
    "boost_mode": "sum"
        }
    }
} 
复制代码

4:具体我们应该怎么做

1:视频表es 的设计

                1:视频与一级,二级类目并不是一对一的关系(多对多),如果所有的数据打平就会出现一个视频存在多行的情况,所以必须让视频id只存在一条。经过技术调研,引入elasticsearch array类型 ,让同一个视频对应的类目整合在一起。(刚开始做的时候担心性能问题,后续压测看来是多虑的)

官网地址:https://www.elastic.co/guide/en/elasticsearch/reference/current/array.html

In Elasticsearch, there is no dedicated array data type. Any field can contain zero or more values by default, however, all values in the array must be of the same data type. For instance:

an array of strings: [ "one""two" ]
an array of integers: [ 12 ]
an array of arrays: [ 1, [ 23 ]] which is the equivalent of123 ]
an array of objects: [ { "name": "Mary", "age": 12 }, { "name": "John", "age": 10 }]
复制代码

2:数据的更新策略

1:整体建设是基于大数据平台。对所有的行为做实时更新,确保检索的实时性。  整体的架构图如下:

image.png 2:视频与一级类目,二级类目的关系基本固定,不会涉及大量的改动。我们可以通过离线数据维护到hbase。我们的大数据平台是基于cdh6.1构建,hbase与datanode 混布。如果yarn任务cpu或内存使用较高会造成hbase响应较慢,c端服务响应超时。所以引入alibase .通过跨集群distcp到新集群,然后bulkload到alihbase。

hbaseFileDir=$1
tableName=$2
#hadoop2 version
date=$(date +%Y-%m-%d-%H-%M)
hadoop distcp -D dfs.replication=2 /data/hbase/${hbaseFileDir} hdfs://xxx.hbaseue.rds.aliyuncs.com:8020/data/hfile/  >> /home/bigdata/data/logs/hadoop-${date}.log 2>&1
num=$(echo $?)
if [ $num -ne 0 ];then
   echo $num
   exit 1
fi
/home/bigdata/data/alihbase-2.1.0/bin/hbase org.apache.hadoop.hbase.mapreduce.LoadIncrementalHFiles /data/hfile/${hbaseFileDir} "${tableName}" >> /home/bigdata/data/logs/hbase-${date}.log 2>&1
num2=$(echo $?)
if [ $num2 -ne 0 ];then
   echo $num2
   exit 1
fi
复制代码

3: 实时数据的更新,通过maxwell 采集binlog, flink消费相关binlog,实时更新数据到elasticserach.整体是基于画像体系构建,配置相关数据即可。(后续整体梳理一下画像体系,再补充到这里。)

3:性能压测

       相关功能已经满足业务的需求,发布之前例行压测。100qps一地鸡毛,elasticsearch load 非常高 。最后决定延迟发布,重点跟进后续的优化。           

1:基于es层面的性能优化。参数大家可自行查阅,根据需要动态的添加即可。

 {
  "es_score_test" : {
    "settings" : {
      "index" : {
        "search" : {
          "slowlog" : {
            "level" : "info"
          }
        },
        "refresh_interval" : "120s",
        "indexing" : {
          "slowlog" : {
            "level" : "debug",
            "threshold" : {
              "index" : {
                "warn" : "1000ms",
                "trace" : "800ms",
                "debug" : "800ms",
                "info" : "800ms"
              }
            },
            "source" : "1000"
          }
        },
        "number_of_shards" : "9",
        "translog" : {
          "flush_threshold_size" : "1024mb",
          "sync_interval" : "120s",
          "durability" : "async"
        },
        "provided_name" : "es_score_test",
        "max_result_window" : "1000000",
        "creation_date" : "1609815684065",
        "number_of_replicas" : "1",
        "uuid" : "ZMtszdE9SLqhUpmXgO-vfA",
        "version" : {
          "created" : "7040299"
        }
      }
    }
  }
}


number_of_replicas -> 副本个数
refresh_interval -> 索引刷新时间频率
durability -> 磁盘同步策略 aync为异步
 
curl -XPUT  -H "Content-Type:application/json" http://ip:9200/es_score_test/_settings  -d '{ "index" : { "number_of_replicas" : "0" }}'

curl -XPUT  -H "Content-Type:application/json" http://ip:9200/es_score_test/_settings  -d '{ "index" : { "refresh_interval" : "60s" }}'

curl -XPUT  -H "Content-Type:application/json" http://ip:9200/es_score_test/_settings  -d '{ "index" : { "translog": { "durability" : "async" }}}'
复制代码

2:groovy脚本动态计算非常消耗cpu,导致elasticsearch集群压力较高。

1:我们决定把部分数通过离线计算结果分值(点击,曝光,收藏)等用户行为。

        通过spark sql 把所有可以离线计算的分值,计算到一个字段。这样固定算分就可以减少计算。

2:elasticsearch 分页存在性能问题,通过scroll解决。

SearchRequest searchRequest = new SearchRequest();
searchRequest.indices(INDEX_NAME);
//from-size elasticsearch会取出所有的数据,再去目标数据。所以整体通过scrollId做深翻页 .
SCROLL_TIME_LIMIT 该项目我设置的时间为3分钟,具体可以根据用户的使用时长判断
searchRequest.scroll(new Scroll(TimeValue.timeValueSeconds(SCROLL_TIME_LIMIT)));

//至于为什么用QUERY_THEN_FETCH 大家可以查阅一下相关文档
searchRequest.searchType(SearchType.QUERY_THEN_FETCH);

//可以不用设置,默认值为512
searchRequest.setBatchedReduceSize(BATCHED_REDUCE_SIZE);

//配置多个shard提高并行度
searchRequest.setMaxConcurrentShardRequests(Constants.NUMBER_9);
//scroll每次的批次数
searchSourceBuilder.size(num);

复制代码

3:剔除所有的groovy脚本,把所有的权重计算通过FunctionScoreQueryBuilder 设置权重计算。

//默认的field 排序打分
FieldValueFactorFunctionBuilder fieldValueFactorFunctionBuilder = new FieldValueFactorFunctionBuilder(scoreType);
FunctionScoreQueryBuilder.FilterFunctionBuilder fieldValueFactorFilter = new FunctionScoreQueryBuilder.FilterFunctionBuilder(fieldValueFactorFunctionBuilder);
filterFunctionBuilderList.add(fieldValueFactorFilter); 


/**
    * 处理包含过滤
    *
    * @param score
    * @param tagName
    * @param data
    * @param isTerms
    * @return
  */
 private FunctionScoreQueryBuilder.FilterFunctionBuilder getContainsFilter(Integer score, String tagName, Object data, boolean isTerms) {
        ScoreFunctionBuilder<WeightBuilder> containsScoreFunctionBuilder = new WeightBuilder();
        containsScoreFunctionBuilder.setWeight(score);
        FunctionScoreQueryBuilder.FilterFunctionBuilder containsFilter = null;
        if (isTerms) {
            containsFilter = new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.termsQuery(tagName, (int[]) data), containsScoreFunctionBuilder);
        } else {
            containsFilter = new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.termQuery(tagName, data), containsScoreFunctionBuilder);
        }
        return containsFilter;
    }


 /**
     * 处理不包含过滤
     *
     * @param score
     * @param tagName
     * @param data
     * @param isTerms
     * @return
     */
    private FunctionScoreQueryBuilder.FilterFunctionBuilder getNotContainsFilter(Integer score, String tagName, Object data, boolean isTerms) {
        ScoreFunctionBuilder<WeightBuilder> notContainsScoreFunctionBuilder = new WeightBuilder();
        notContainsScoreFunctionBuilder.setWeight(score);
        FunctionScoreQueryBuilder.FilterFunctionBuilder notConstantsFilter = null;
        if (isTerms) {
            notConstantsFilter = new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.boolQuery().mustNot(QueryBuilders.termsQuery(tagName, (int[]) data)), notContainsScoreFunctionBuilder);
        } else {
            notConstantsFilter = new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.boolQuery().mustNot(QueryBuilders.termQuery(tagName, data)), notContainsScoreFunctionBuilder);
        }
        return notConstantsFilter;
    }
复制代码

4: scollid创建的太多会存在性能问题,所以scrollid时间不能设置的太长。那我们应该怎么避免用户下次访问还是从第一位开始分发的问题。引入min_score,这样在下次请求的时候加上minscore 就可以避免从0开始滚动的问题。相关min_score解释见:www.elastic.co/guide/en/el… 由于elasticsearch只支持min_score 过滤,所以我们要把最大的分值算出来(注意min_score不能为负数),用一个最大值反向计算排序。再把上次的min_score 存储在redis或hbase,设置一下ttl。这样就可以保障在同一定的时间段内保持不会重复。

5:线上压测&灰度

1:线上压测

经过以上优化,rt在 50ms以内。很大程度上满足了业务的需求。针对频道页,热门都可以用类似的方案。

2:线上灰度

整体采用crc32做灰度,在离线数仓中加入对应的udf函数。即可完成相关数据分析。整体分为10个样本,通过数据分析。确认是否可以增加样本。

 /**
     * 计算设备crc32值
     *
     * @param deviceId
     * @return
     */
    protected Integer getDeviceCrc32Number(String deviceId) {
        if (StringUtils.isBlank(deviceId)) {
            return null;
        }
        CRC32 crc32 = new CRC32();
        crc32.update(deviceId.getBytes());
        Long value = (crc32.getValue()) % 10;
        Integer result = value.intValue();
        return result;
    }

/**
 *  udf 函数开发,把相关jar 上传到hdfs
 *  根据设备号分流
 */
public final class Crc32UDF extends UDF{


    /**
     * @author tangpt
     * @param eDeviceId
     * @return
     */
    public   long  evaluate(String eDeviceId){
        if(null ==  eDeviceId  ||  eDeviceId.length() == 0){
            return 0l;
        }
        CRC32 crc32 =new  CRC32();
        crc32.update(eDeviceId.getBytes());
        return crc32.getValue();
    }


}
复制代码

6:总结

以上针对function_score 做了业务上的一些创新,频道页的点击也因此有较大的突破。如果大家有类似的场景,欢迎一起交流技术方案。

分类:
后端
分类:
后端
收藏成功!
已添加到「」, 点击更改