4、基于内容的推荐(CB):特征提取与用户画像构建

好,咱们进入第四章节。基于内容的推荐,简称CB。说白了,就是让系统学会「看人下菜碟」——根据你过去喜欢什么,再给你推荐类似的东西。

我个人习惯把CB推荐比作一个「老派但靠谱」的图书管理员。他不认识你,但看你借过《三体》,就会把《银河帝国》递给你。嗯,就是这么个逻辑。

4.1 特征提取:标签、关键词、类别

特征提取是CB的基石。你想想看,如果连物品长什么样都描述不清,怎么推荐?

我在项目中遇到过最典型的场景:一个电商平台,商品标题是「新款春季男士休闲运动鞋」。机器怎么理解这双鞋?靠的就是特征提取。

4.1.1 标签提取

标签是人工或半自动打上去的。比如「运动」、「休闲」、「春季」。标签的好处是精准,坏处是成本高。

我曾经在一个视频网站项目里,光给100万条视频打标签,就花了三个团队两个月。所以,别迷信标签,能用算法解决的尽量用算法。

4.1.2 关键词提取

关键词是从文本里自动抽出来的。常用的方法有TF-IDF、TextRank。ES里自带了一个叫more_like_this的查询,其实就是关键词匹配的升级版。

举个例子,一篇新闻稿:「苹果公司发布新款iPhone,搭载A17芯片」。关键词可能是「苹果」、「iPhone」、「A17芯片」。

// ES中关键词提取的简单示例
// 使用more_like_this查询
GET /articles/_search
{
  "query": {
    "more_like_this": {
      "fields": ["title", "content"],
      "like": "苹果公司发布新款iPhone",
      "min_term_freq": 1,
      "max_query_terms": 12
    }
  }
}

这里要注意,min_term_freq设成1,意味着只要出现一次就算。我建议根据数据量调整,一般设2-3比较稳妥。

4.1.3 类别提取

类别是粗粒度的特征。比如「科技」、「体育」、「娱乐」。类别通常用分类模型或规则来提取。

我记得有一次做新闻推荐,类别搞错了——把「科技」新闻分到了「娱乐」类。结果用户点进来一看是芯片技术,直接跳出。嗯,分类的准确性直接影响用户体验。

特征类型 提取方式 适用场景 我踩过的坑
标签 人工/半自动 电商、视频 成本高,容易过时
关键词 TF-IDF/TextRank 新闻、文章 同义词问题没处理好
类别 分类模型/规则 全品类 类别粒度太粗或太细
我的小技巧: 特征提取时,别只依赖一种方法。我习惯把标签、关键词、类别三者融合。比如一个商品,标签是「运动鞋」,关键词是「透气」、「轻便」,类别是「鞋靴」。这样召回时覆盖面更广。

4.2 用户画像构建

用户画像,说白了就是给用户「贴标签」。你想想看,一个用户看了10篇科技文章,点了5次体育新闻,收藏了3个美食视频。那他的画像就是:科技重度、体育中度、美食轻度。

构建用户画像,我一般分三步走:

  1. 收集行为数据:点击、收藏、购买、停留时长
  2. 计算特征权重:用时间衰减、行为类型加权
  3. 存储到ES:用嵌套文档或扁平结构

我曾经犯过一个错误:把所有行为一视同仁。结果一个用户误点了一次「减肥产品」,系统就疯狂推荐减肥药。后来我加了行为权重——购买权重10分,收藏5分,点击1分。这才正常。

// ES中用户画像的存储结构示例
PUT /user_profile/_doc/1001
{
  "user_id": 1001,
  "profile": {
    "tag_weights": {
      "科技": 0.85,
      "体育": 0.45,
      "美食": 0.30,
      "娱乐": 0.10
    },
    "last_updated": "2024-01-15T10:30:00Z"
  }
}
注意: 用户画像是动态的。我建议每天或每小时更新一次。别用一个月前的画像做今天的推荐,用户口味会变。

4.3 ES中实现基于内容的召回

好,特征有了,画像有了,怎么召回?ES提供了两种主力查询:multi_matchterm查询。

4.3.1 使用multi_match进行多字段匹配

multi_match适合关键词匹配。比如用户画像里有「科技」、「iPhone」,那我们就去文章索引里找标题或内容包含这些词的文章。

// 基于用户画像的multi_match召回
GET /articles/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "multi_match": {
            "query": "科技 iPhone 芯片",
            "fields": ["title^3", "tags^2", "content"],
            "type": "best_fields",
            "tie_breaker": 0.3
          }
        }
      ]
    }
  },
  "size": 20
}

这里我用了title^3,意思是标题匹配的权重是3倍。为什么?因为标题通常最能代表文章内容。这是我踩过坑后总结的——一开始所有字段权重一样,结果内容里出现一次「科技」就排前面,标题里出现反而不重要,这不合理。

4.3.2 使用term查询进行精确匹配

term查询适合类别、标签这种精确字段。比如用户画像显示用户喜欢「科技」类别,那就直接term查询。

// 基于类别的term召回
GET /articles/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "category": "科技"
          }
        }
      ],
      "must": [
        {
          "match": {
            "status": "published"
          }
        }
      ]
    }
  },
  "size": 50
}

注意,我用的是filter而不是must。为什么?因为filter不参与评分计算,性能更好。类别匹配不需要算分,只要「是或不是」就行。

核心思路: 基于内容的召回,本质上是「用用户的特征向量,去匹配物品的特征向量」。ES的multi_match和term查询,就是实现这个匹配的两种武器。前者适合模糊匹配,后者适合精确过滤。

4.3.3 组合使用:画像驱动的召回策略

在实际项目中,我很少只用一种查询。我习惯把multi_match和term组合起来:

// 组合召回策略
GET /articles/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "multi_match": {
            "query": "科技 iPhone 芯片",
            "fields": ["title^3", "tags^2", "content"],
            "type": "best_fields",
            "tie_breaker": 0.3
          }
        }
      ],
      "filter": [
        {
          "term": {
            "category": "科技"
          }
        },
        {
          "range": {
            "publish_date": {
              "gte": "now-7d"
            }
          }
        }
      ]
    }
  },
  "size": 20
}

这个查询的意思是:先过滤出「科技」类别且7天内发布的文章,再按关键词匹配度排序。嗯,这样既保证了相关性,又保证了时效性。

避坑指南: 我曾经把filter和must的顺序搞反了。记住,filter先执行,减少数据量;must后执行,计算评分。顺序对了,性能能提升30%以上。

好了,这一章的内容就这些。特征提取是基础,用户画像是核心,ES查询是工具。三者缺一不可。下一章我们会聊协同过滤,那又是另一套玩法了。