# 推荐系统

Typesense 可以根据用户在特定会话中的行为生成推荐,这是通过 向量搜索 功能实现的。

这需要构建一个机器学习模型来生成 嵌入向量(embeddings),将其存储在 Typesense 中,然后在 Typesense 中进行最近邻搜索。

本文将介绍如何使用 Starspace (opens new window) 机器学习模型来生成嵌入向量。

Transformers4Rec (opens new window) 是另一个可以用于此场景的机器学习模型(还有其他适用模型)。

# 应用场景

本文将以电商产品数据集为例进行说明,但以下概念适用于任何领域(例如:推荐文章、电影或存储在 Typesense 中的任何类型记录)。

# 第一步:准备训练数据集

我们将使用 Starspace (opens new window) 来构建机器学习模型。

Starspace 要求训练数据集采用以下格式 - 每行代表一个用户会话以及该会话中用户交互过的商品集合。

在上面的示例中,第一行表示某个用户在一个会话中与商品 appleorangebananabroccolimango 进行了交互(查看、购买、加入购物车等)。

另一个用户(也可能是同一个用户)在另一个会话中与商品 cerealssodabreadnutscookies 进行了交互。

TIP

为了让本文更易读,我们在这个示例中使用了 product_name。 在生产环境中,你应该在训练数据集中使用商品的 ID 或 SKU。

# 第二步:设置 Starspace

# 安装系统依赖

确保你安装了 C++11 编译器(gcc-4.6.3 或更新版本,或 Visual Studio 2015,或 clang-3.3 或更新版本)。

在 macOS 上需要安装 XCode,在 Linux 发行版上需要通过发行版的包管理器安装 build-essential 包。

# 克隆 Starspace 源代码

git clone https://github.com/facebookresearch/Starspace.git
cd Starspace

# 设置 Boost

Boost 是 Starspace 所需的库。

在上述 Starspace 目录中运行以下命令:

curl -LO https://boostorg.jfrog.io/artifactory/main/release/1.82.0/source/boost_1_82_0.tar.gz
tar -xzvf boost_1_82_0.tar.gz

# 编译 Starspace

Starspace 目录中运行以下命令:

make -e BOOST_DIR=boost_1_82_0 && \
  make embed_doc -e BOOST_DIR=boost_1_82_0

要验证 Starspace 是否正常工作,运行 ./starspace 后应看到类似以下输出:

$ ./starspace
用法:需要指定是训练还是测试模式

"starspace train ..."  或 "starspace test ..."
...

# 步骤 3:训练 Starspace 模型

将步骤 1 中的训练数据集文件命名为 session-data.txt

然后运行以下命令训练模型:

./starspace train \
  -trainFile <path/to/session-data.txt> \
  -model productsModel \
  -label '' \
  -trainMode 1 \
  -epoch 25 \
  -dim 100

命令运行完成后,将生成两个文件:

  • 二进制文件 productsModel
  • 包含模型权重的 TSV 文件 productsModel.tsv

建议在用户使用网站/应用收集到新会话数据时定期重新执行此训练步骤。

# 步骤 4:生成嵌入向量

首先从训练数据集中提取所有唯一商品:

export unique_items=$(tr ' ' '\n' < session-data.txt | sed '/^$/d' | sort -u)
export output_jsonl_file="products-with-embeddings.jsonl"

为每个商品生成嵌入向量并存储到 JSONL 文件中:

echo -n > ${output_jsonl_file}

while read -r item; do
    embedding=$(echo "${item}" | ./embed_doc productsModel | tail -1 | tr ' ' ',')
    echo "{\"id\":\"${item}\",\"embedding\":[${embedding%?}]}" >> "${output_jsonl_file}"
done <<< "${unique_items}"

这将生成如下所示的 JSONL 文件:

现在我们可以将这个 JSONL 文件导入到 Typesense 中。

# 步骤5:在Typesense中索引嵌入向量

export TYPESENSE_API_KEY=xyz
export TYPESENSE_URL='https://xyz.a1.typesense.net'

创建集合:

curl "${TYPESENSE_URL}/collections" \
       -X POST \
       -H "Content-Type: application/json" \
       -H "X-TYPESENSE-API-KEY: ${TYPESENSE_API_KEY}" \
       -d '{
         "name": "products",
         "fields": [
           {"name": "name", "type": "string", "optional": true},
           {"name": "description", "type": "string", "optional": true},
           {"name": "price", "type": "float", "optional": true},
           {"name": "categories", "type": "string[]", "optional": true},
           {"name": "embedding", "type": "float[]", "num_dim": 100 }
         ]
       }'

注意我们设置了 num_dim: 100。这对应着我们训练 Starspace 模型时设置的 -dim 100 参数。

将包含嵌入向量的JSONL文件导入集合:

curl -H "X-TYPESENSE-API-KEY: ${TYPESENSE_API_KEY}" \
      -X POST \
      -T products-with-embeddings.jsonl \
      "${TYPESENSE_URL}/collections/products/documents/import?action=emplace"

这里我们只插入了每个产品的嵌入向量。你也可以使用相同的 id 字段作为参考,分别导入其他值如名称、描述、价格、类别等,以填充产品记录的其他部分。

# 步骤6:生成推荐结果

假设现在有一位用户访问我们的网站/应用,并在会话中与以下产品进行了交互:

mango broccoli milk

为了基于这些会话数据生成推荐,我们首先需要生成嵌入向量:

export embedding=$(echo "mango broccoli milk" | ./embed_doc productsModel | tail -1 | tr ' ' ',')
echo ${embedding}
-0.0862846,0.127956,0.0558543,0.0745331,0.02449,-0.131018,0.0886827,-0.0571893,-0.0398686,-0.0116799,-0.0164978,-0.173818,0.0478985,0.109211,-0.0826394,-0.177671,-0.219366,0.180478,-0.0140154,-0.0237589,-0.010896,0.115979,-0.044924,0.129452,-0.0111529,-0.0978542,-0.121468,-0.0700872,-0.0190036,0.116127,0.0617186,-0.0463324,-0.172141,0.0302211,0.0610366,-0.0831281,0.04558,-0.00370933,-0.107602,-0.0394414,0.0334175,0.0429023,0.133572,-0.124658,0.225743,-0.0156787,-0.284864,0.148183,-0.0508378,0.175489,-0.0417769,-0.0920536,-0.0443016,-0.0838343,-0.0694042,-0.0333535,-0.108574,-0.0894618,-0.022049,-0.0500605,-0.0234268,0.00732048,0.0817547,0.00764651,0.0285933,0.100818,-0.229398,0.0508415,0.117766,-0.0289333,-0.0493134,0.167664,0.0696889,0.115228,-0.0609508,-0.12562,-0.0450054,-0.0648439,0.0817176,0.169663,0.133255,-0.111001,-0.0467052,-0.0373238,0.005385,0.111311,-0.0171787,0.0311545,0.0474074,-0.0301008,-0.0555648,0.0776044,-0.0287841,-0.162136,-0.0511268,0.174767,-0.0169033,-0.0223623,-0.140496,0.154727

现在我们可以将这个用户会话数据生成的嵌入向量作为 vector_query 发送给 Typesense,进行最近邻搜索,这将返回推荐给该用户的产品列表:

curl "${TYPESENSE_URL}/multi_search" \
        -X POST \
        -H "Content-Type: application/json" \
        -H "X-TYPESENSE-API-KEY: ${TYPESENSE_API_KEY}" \
        -d '{
          "searches": [
            {
              "q": "*",
              "collection": "products",
              "vector_query": "embedding:(['${embedding%?}'], k:10, distance_threshold: 1)"
            }
          ]
        }' | jq '.results[0].hits[] | .document.id'

这将返回以下推荐结果,我们可以在过滤掉用户在当前会话中已经看过的商品后,将其展示在用户界面中:

"broccoli"
"mango"
"banana"
"apple"
"orange"
"tissue"
"detergent"
"cheese"
"milk"
"butter"

:::提示 本文中我们使用产品名称是为了便于阅读。 在实际生产环境中,你应该在训练数据集中使用产品ID或SKU,并按以下方式生成嵌入向量:

sku_1 sku_4 sku_5
sku_5 sku_8 sku_1 sku_2 sku_10
sku_5 sku_1 sku_4 sku_21 sku_22

:::