Elasticsearch:使用 Elasticsearch Transforms 进行产品推荐

2021年12月22日   |   by mebius

这篇博文将引导你完成从包含订单项的索引创建产品推荐的步骤。 这篇文章展示了如何使用转换(transforms)从不断更新的订单项目中自动创建此数据,并在索引新产品时利用丰富处理器来存储推荐。

%title插图%num
最终获得文档内建议的四个步骤

为了开始,我使用 File Visualizer 从 kaggle 导入电子商务数据集。 我下载了 olist_order_items_dataset,其中包含一个带有订单项的 CSV 文件。

如果你不关心我们的示例数据的导入,请直接跳到转换数据的步骤部分。

安装

我们首先需要安装 Elasticsearch 及 Kibana。当然最容易的方法是启动 Elastic Cloud。你只需要花几分钟的实践就可以启动一个 Elasticsearch 的集群。你可以有14天的试用期,而不用提供任何的信用卡之类的付费信息。对于国内的开发者来说,你可以在阿里巴巴及腾讯云上启动 Elasticsearch 集群,并试用。具体文章可以参阅:

如果你想自己部署 Elasticsearch,请参阅我之前的文章:

对于喜欢 docker 部署的开发者来说,请参阅我的文章

使用 File Visualizer 工具导入数据

打开 Kibana:

%title插图%num

%title插图%num%title插图%num

在上面,我们选择下载的文件olist_order_items_dataset.csv:

%title插图%num

使用文件可视化工具,你可以在导入前查看一些导入统计信息。点击上面的 Import 按钮:

%title插图%num

在上面,我们选上 Create indexpattern,这样我们可以不用之后再单独去为这个索引创建一个 index pattern。有了 index pattern,我们可以在 Kibana 的 Discover 中进行查看数据。

%title插图%num

从上面的过程中,我们可以看得出来 import complete。为了查看摄入的数据,我们可以点击 View index in Discover:

%title插图%num

从上面的显示中我们可以看出来每个文档的一些字段(除去以 _ 开头的元字段): freight_value,order_id,order_item_id,price,product_id,seller_id,shipping_limit_date。

到此为止,我们已经完成了数据的摄入工作。

转换数据的步骤

数据集包含订单项目,因此订单的每个项目都有一个文档。 看起来像这样:

{
  "order_item_id" : 1,
  "@timestamp" : "2017-09-26T02:11:19.000+02:00",
  "price" : 89.9,
  "product_id" : "764292b2b0f73f77a0272be03fdd45f3",
  "order_id" : "4427e7da064254d8af538aca447a560c",
  "freight_value" : 12.97,
  "seller_id" : "bd23da7354813347129d751591d1a6e2",
  "shipping_limit_date" : "2017-09-26 02:11:19"
}

对于 Transforms 来说,它是针对每个 entity 来进行的转换。显然上面能够代表这个数据的 entity 是 product_id 及 order_id。这里有两个字段对我们很重要,即 order_id 和 product_id。

那么,我们如何从这样的数据集中获得基于每个产品的建议的能力? 通过根据不同的分组标准从该数据创建新索引。 所以基本上我们需要从 order_id->product_id 映射到 product_id->recommend_ids 映射,完全独立于产品订单。 让我们尝试像这样转换数据两次:

%title插图%num

从订单项目转变为产品推荐

那么,这张图是什么意思呢? 首先,我们将未分组的订单商品列表转换为订单列表,其中包含属于该订单的产品。

我们的第二步将是类似产品的文档索引,其中包含已一起订购的产品列表。

转换为 orders

为了理解问题,我们尝试手动查找一起订购的产品。

首先,找到购买最多的产品:

GET order-items/_search
{
  "size": 0,
  "aggs": {
    "by_product_id": {
      "terms": {
        "field": "product_id",
        "size": 10
      }
    }
  }
}

上面的搜索显示:

  "aggregations" : {
    "by_product_id" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 108777,
      "buckets" : [
        {
          "key" : "aca2eb7d00ea1a7b8ebd4e68314663af",
          "doc_count" : 527
        },
        {
          "key" : "99a4788cb24856965c36a24e339b6058",
          "doc_count" : 488
        },
        {
          "key" : "422879e10f46682990de24d770e7f83d",
          "doc_count" : 484
        },
        {
          "key" : "389d119b48cf3043d311335e499d9c6b",
          "doc_count" : 392
        },
        {
          "key" : "368c6c730842d78016ad823897a372db",
          "doc_count" : 388
        },
        {
          "key" : "53759a2ecddad2bb87a079a1f1519f73",
          "doc_count" : 373
        },
        {
          "key" : "d1c427060a0f73f6b889a5c7c61f2ac4",
          "doc_count" : 343
        },
        {
          "key" : "53b36df67ebb7c41585e8d54d6772e08",
          "doc_count" : 323
        },
        {
          "key" : "154e7e31ebfa092203795c972e5804a6",
          "doc_count" : 281
        },
        {
          "key" : "3dd2a17168ec895c781a9191c1e95ad7",
          "doc_count" : 274
        }
      ]
    }

最多的一个产品被购买 527 次。我们可以使用如下的命令来查看所有的订单:

GET order-items/_search
{
  "query": {
    "term": {
      "product_id": {
        "value": "aca2eb7d00ea1a7b8ebd4e68314663af"
      }
    }
  }
}

所以这会从 527 个结果中返回 10 个订单项。 我们可以天真地通过以下方式收集客户端的所有订单 IDs:

GET order-items/_search?filter_path=**.order_id
{
  "size": 10000, 
  "query": {
    "term": {
      "product_id": {
        "value": "aca2eb7d00ea1a7b8ebd4e68314663af"
      }
    }
  }
}

上述命令将返回类似如下的结果:

{
  "hits" : {
    "hits" : [
      {
        "_source" : {
          "order_id" : "b9a3648dcf60f6b902216e563c539244"
        }
      },
      {
        "_source" : {
          "order_id" : "b9d5bea267e1dd6838786cc12ba40ceb"
        }
      },
      {
        "_source" : {
          "order_id" : "ba2018431e2b0cec56147edc7a0627a5"
        }
      },
   ...

然后使用术语过滤器通过将先前响应中的所有 order_id 添加到以下查询中的术语过滤器来过滤那些:

GET order-items/_search
{
  "size": 0,
  "query": {
    "bool": {
      "filter": [
        { "terms": {
          "order_id": [
            "44676f5946200b6318a3a13b081396bc",
            "..."
          ]
        }}
      ]
    }
  },
  "aggs": {
    "by_order": {
      "terms": {
        "field": "product_id",
        "size": 10
      }
    }
  }
}

这将在 aggs 响应中为我们提供三个产品 ID 的列表

{
  "aggregations" : {
    "by_order" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "aca2eb7d00ea1a7b8ebd4e68314663af",
          "doc_count" : 527
        },
        {
          "key" : "28b4eced95a52d9c437a4caf9d311b95",
          "doc_count" : 1
        },
        {
          "key" : "7fb7c9580222a2af9eb7a95a6ce85fc5",
          "doc_count" : 1
        }
      ]
    }
  }
}

第一个搜索到的是产品本身,需要删除。 这给我们留下了一起购买的两种产品。 我们参与了很多查询。 让我们尝试用这个转换来解决并逐步解释它:

PUT _ingest/pipeline/orders-set-id
{
  "description": "Sets _id to order id", 
  "processors": [
    {
      "script": {
        "lang": "painless",
        "source": "ctx._id = ctx.order_id;"
      }
    }
  ]
}


PUT _transform/order-item-transform-to-orders
{
  "source":    { "index": "order-items" },
  "dest":      { "index": "orders", "pipeline": "orders-set-id" },
  "frequency": "1h",
  "pivot": {
    "group_by": {
      "order_id": {
        "terms": {
          "field": "order_id"
        }
      }
    },
    "aggregations": {
      "products": {
        "scripted_metric": {
          "init_script": "state.docs = [:];state.docs.products = []",
          
          "map_script": """
          state.docs.products.add(doc['product_id'].value);
          """,
          
          "combine_script": "return state.docs",
          
          "reduce_script": """
          def products = new HashSet();
          for (state in states) {
            products.addAll(state.products);
          }
          
          return ['ids': products, 'length':products.size()];
          """
        }
      }
    }
  }
}

POST _transform/order-item-transform-to-orders/_start

# wait a second for the first run to finish, then run
GET orders/_count

依次运行上面的命令。过一会儿后,最后一个命令给出来的结果是:

{
  "count" : 98666,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  }
}

通过数据集,我们有 112650 个初始订单项。 将它们简化为订单列表会导致 98666 个订单。 所以我们的大部分订单都是单品订单。

让我们一步一步地解释上面的转换。 首先,我们定义一个 source.index 来获取数据,以及一个 dest.index 来写入数据。 然后频率决定变换在后台运行的频率。 pivot.group_by 条件是分组依据的字段。 由于我们要按订单分组以收集所有产品 ID,因此 order_id 是初始分组条件。

现在是花哨的部分 – scripted metric aggregation。 一种以最自定义的方式为聚合收集数据的非常强大的方法。 如果你对这个不是很明白的话,请参阅我之前的文章 “Elasticsearch:Script aggregation (2)”。scripted metric aggregation 由四个阶段组成,每个阶段都是可编写脚本的。 第一阶段允许初始化在其他阶段重用的数据结构。 在此示例中,在 init_script 中创建了一个示例文档映射,其中包含一个空的 products 数组。

state.docs = [:];
state.docs.products = [];

下一个阶段是 map_script,它为每个匹配查询的文档执行一次(所以在这个例子中的每个文档都会被执行)。

state.docs.products.add(doc['product_id'].value);

下一个名为 combine_script 的阶段对每个分片(shard)执行一次,并允许通过在发送之前做一些更多的工作来减少发送到协调节点(coordinating node)的数据。

return state.docs;

最后一步是从所有订单项目的不同分片中收集所有数据,并为该单个订单创建最终数据结构。 在此示例中,我们将产品 ID 收集到一个集合中(以防止重复),并存储该集合的大小以允许更轻松的范围查询。

def products = new HashSet();
for (state in states) {
  products.addAll(state.products);
}

return ['ids': products, 'length':products.size()];

我们来首先看看一个 orders 索引里的一个文档是啥样的:

GET orders/_search
{
  "size": 1
}

上面的命令显示:

{
  "took" : 15,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 10000,
      "relation" : "gte"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "orders",
        "_type" : "_doc",
        "_id" : "00010242fe8c5a6d1ba2dd792cb16214",
        "_score" : 1.0,
        "_source" : {
          "order_id" : "00010242fe8c5a6d1ba2dd792cb16214",
          "products" : {
            "length" : 1,
            "ids" : [
              "4244733e06e7ecb4970a6e2683c13e61"
            ]
     tgcode     }
        }
      }
    ]
  }
}

显示的订单由单个订单项目组成。有了这个索引,只要我们知道一个产品 ID,就可以通过这个索引找到一起订购的产品,例如本例中的 0bcc3eeca39e1064258aa1e932269894。

GET orders/_search
{
  "size": 0,
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "products.ids.keyword": "0bcc3eeca39e1064258aa1e932269894"
          }
        }
      ]
    }
  },
  "aggs": {
    "products": {
      "terms": {
        "field": "products.ids.keyword",
        "size": 10
      }
    }
  }
}

这将返回以下聚合响应:

"aggregations" : {
  "products" : {
    "doc_count_error_upper_bound" : 0,
    "sum_other_doc_count" : 4,
    "buckets" : [
      {
        "key" : "0bcc3eeca39e1064258aa1e932269894",
        "doc_count" : 100
      },
      {
        "key" : "422879e10f46682990de24d770e7f83d",
        "doc_count" : 6
      },
      {
        "key" : "368c6c730842d78016ad823897a372db",
        "doc_count" : 4
      },
      {
        "key" : "389d119b48cf3043d311335e499d9c6b",
        "doc_count" : 3
      },
      {
        "key" : "53759a2ecddad2bb87a079a1f1519f73",
        "doc_count" : 3
      },
      {
        "key" : "b0961721fd839e9982420e807758a2a6",
        "doc_count" : 3
      },
      {
        "key" : "3773a3773f5978591cff7b3e614989b3",
        "doc_count" : 1
      },
      {
        "key" : "4b5df063d69ffafb87c279672ecb4191",
        "doc_count" : 1
      },
      {
        "key" : "4e1346d7b7e02c737a366b086462e33e",
        "doc_count" : 1
      },
      {
        "key" : "774e21c631ca26cba7b5606bdca7460f",
        "doc_count" : 1
      }
    ]
  }
}

所以,我们用这个产品买了一堆其他的产品! 第一个产品是我们查询的产品 – 你可以手动过滤掉它或更改聚合以排除它,如下所示:

"aggs": {
  "products": {
    "terms": {
      "field": "products.ids.keyword",
      "size": 10,
      "exclude": "0bcc3eeca39e1064258aa1e932269894"
    }
  }
}

该列表的其余部分是一个排序列表,其中包含一起购买的产品,包括基于这些文档组合的订单数量的权重。 挺整洁的!

我们还可以要求最小文档计数,以便像这样出现在结果列表中:

"aggs": {
  "products": {
    "terms": {
      "field": "products.ids.keyword",
      "size": 10,
      "exclude": "0bcc3eeca39e1064258aa1e932269894",
      "min_doc_count": 2
    }
  }
}

在这个例子中,这会将我们的结果减少到五个,并确保购买的产品之间存在更强的相关性。

所以,基本上我们现在已经完成了,不是吗? 如果您有一个小数据集,这可能足以在每次用户观看你的页面(包括此类推荐)时运行聚合。 你也可以简单地将上述结果缓存 10 分钟,以防止当许多用户查看同一产品时出惊群效应(thundering herd)问题。

然而,我们也可以继续,而不是基于订单的数据,我们可以有一个以产品为中心的索引。 只需再变换一次!

转化为产品推荐

为了节省你一些时间摆弄,让我们先添加一个摄入管道,以解决一些小问题

PUT _ingest/pipeline/products-remove-own-id-pipeline
{
  "description": "Removes the own product id from the list of recommended product ids and renames the 'ids' field", 
  "processors": [
    {
      "script": {
        "lang": "painless",
        "source": """
        ctx.ids.removeAll([ctx.product_id]);
        ctx['recommendation_ids'] = ctx.remove('ids');
        ctx._id = ctx.product_id;
        """
      }
    }
  ]
}

此管道删除 ids 元素中的重复项,然后将 ids 重命名为 Recommendation_ids。 最后一部分是将 _id 设置为产品的 id 而不是自动生成的。

好的,现在让我们使用先前转换创建的索引作为输入来创建一个转换:

PUT _transform/orders-transform-to-products
{
  "source": {
    "index": "orders",
    "query": {
      "range": {
        "products.length": {
          "gt": 1
        }
      }
    }
  },
  "dest": {
    "index": "product-recommendations",
    "pipeline" : "products-remove-own-id-pipeline"
  },
  "frequency": "1h",
  "pivot": {
    "group_by": {
      "product_id": {
        "terms": {
          "field": "products.ids.keyword"
        }
      }
    },
    "aggregations": {
      "ids": {
        "scripted_metric": {
          "init_script": "state.docs = [:];state.docs.products = []",
          
          "map_script": """
          for (def i = 0; i

这里的主要区别是包含大部分逻辑的 map_script 以及 group_by 分组标准。 首先,使用 orders 索引,但只有那些具有多个订单项的订单才用作输入,否则将不会有其他产品的链接。 这次 group_by 标准是产品 id,因为这是我们希望成为新索引的基础。

由于 product.length 查询过滤器,我们已经知道 product.ids 数组将大于单个元素。 因此,在 map_script 中,我们遍历该数组,对于找到的每个产品 ID,我们将其附加到 stats.docs.products 数组。 在有多个分片的情况下,最后的减少发生在减少脚本中,通过再次使用集合来防止重复。

是时候看看我们上面使用的产品了:

GET product-recommendations/_doc/0bcc3eeca39e1064258aa1e932269894

上述命令将生成:

{
  "_index" : "product-recommendations",
  "_type" : "_doc",
  "_id" : "0bcc3eeca39e1064258aa1e932269894",
  "_version" : 1,
  "_seq_no" : 227,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "recommendation_ids" : [
      "dfc9d9ea4c2fbabe7abb0066097d9905",
      "368c6c730842d78016ad823897a372db",
      "98e0de96ebb0711db1fc3d1bad1a902f",
      "3773a3773f5978591cff7b3e614989b3",
      "4e1346d7b7e02c737a366b086462e33e",
      "4b5df063d69ffafb87c279672ecb4191",
      "53759a2ecddad2bb87a079a1f1519f73",
      "b0961721fd839e9982420e807758a2a6",
      "389d119b48cf3043d311335e499d9c6b",
      "9d1893083966b9c51109f0dc6ab0b0d9",
      "422879e10f46682990de24d770e7f83d",
      "774e21c631ca26cba7b5606bdca7460f",
      "d1c427060a0f73f6b889a5c7c61f2ac4"
    ],
    "product_id" : "0bcc3eeca39e1064258aa1e932269894"
  }
}

你可以看到摄取管道已经工作,因为 _id 等于 product_id 并且 product_id 不是 Recommendation_ids 数组的一部分。

与之前的查询相比,这种特定方法有一个弱点:我们失去了所有权重。 与查询相比,没有任何迹象表明一个产品是否被一起购买过一次或八次,并且推荐 ID 字段也未按此排序。 我们应该先解决这个问题,然后再进一步。

在变换中添加回权重

看起来,我们只需要一张带有计数的 map。 因此,让我们通过预览功能处理转换(如果稍后运行,请不要忘记存储转换,本文未显示):

一、停止变换,删除变换,删除索引

POST _transform/orders-transform-to-products/_stop

DELETE _transform/orders-transform-to-products

DELETE product-recommendations

由于数据结构不同,而不仅仅是 id 数组,因此接下来更新摄取管道:

PUT _ingest/pipeline/products-remove-own-id-pipeline
{
  "description": "Removes the own product id from the list of recommended product ids and renames the 'ids' field", 
  "processors": [
    {
      "script": {
        "lang": "painless",
        "source": """
        ctx.ids.removeIf(p -> p.id.equals(ctx.product_id));
        ctx['recommendation_ids'] = ctx.remove('ids');
        ctx._id = ctx.product_id;
        """
      }
    }
  ]
}

现在让我们调整 transform:

POST _transform/_preview
{
  "source": {
    "index": "orders",
    "query": {
      "range": {
        "products.length": {
          "gt": 1
        }
      }
    }
  },
  "dest": {
    "index": "product-recommendations",
    "pipeline": "products-remove-own-id-pipeline"
  },
  "frequency": "1h",
  "pivot": {
    "group_by": {
      "product_id": {
        "terms": {
          "field": "products.ids.keyword"
        }
      }
    },
    "aggregations": {
      "ids": {
        "scripted_metric": {
          "init_script": "state.docs = [:];state.docs.products = [:]",
          
          "map_script": """
          // push all products into an array
          for (def i = 0; i (v == null) ? [ 'id':id, 'count': 1L] : [ 'id':id, 'count': v.count+1]);
          }
          """,
          
          "combine_script": "return state.docs",
          
          "reduce_script": """
          // merge same ids together
          def products = [];
          for (state in states) {
            for (product in state.products.values()) {
              def id = product.id;
              def optional = products.stream().filter(p -> id.equals(p.id)).findFirst();
              if (optional.isPresent()) {
                optional.get().count += product.count;
              } else {
                products.add(product);
              }
            }
          }
          
          // sort list by count of products!
          Collections.sort(products, (o1,o2) -> o2.count.compareTo(o1.count));
          
          return products;
          """
        }
      }
    }
  }
}

# Change the above request to save the transform instead of preview, then start
PUT _transform/orders-transform-to-products
{
  ..
}

POST _transform/orders-transform-to-products/_start

现在重新启动转换后,我们可以对产品 ID 运行一个简单的 GET 请求:

GET product-recommendations/_doc/0bcc3eeca39e1064258aa1e932269894
{
  "_index" : "product-recommendations",
  "_type" : "_doc",
  "_id" : "0bcc3eeca39e1064258aa1e932269894",
  "_version" : 1,
  "_seq_no" : 227,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "product_id" : "0bcc3eeca39e1064258aa1e932269894",
    "recommendation_ids" : [
      {
        "count" : 6,
        "id" : "422879e10f46682990de24d770e7f83d"
      },
      {
        "count" : 4,
        "id" : "368c6c730842d78016ad823897a372db"
      },
      {
        "count" : 3,
        "id" : "53759a2ecddad2bb87a079a1f1519f73"
      },
      {
        "count"tgcode : 3,
        "id" : "b0961721fd839e9982420e807758tgcodea2a6"
      },
      {
        "count" : 3,
        "id" : "389d119b48cf3043d311335e499d9c6b"
      },
      {
        // more documents with a count of 1
      }
    ]
  }
}

现在通过检索这样的文档,客户端可以决定计数 1 是否足以显示产品。

将其放入自己的索引还有一个好处。 由于能够查询包含其一起购买的产品的文档,我们甚至可以进一步在索引时添加此信息,当使用丰富策略对产品进行索引时。

如果你想获得有关转换的更多统计信息,可以运行:

GET _transform/order-item-transform-to-orders/_stats
GET _transform/orders-transform-to-products/_stats

创建丰富策略

想象一下,我们将店面的产品存储在产品索引中。 每当组件更新产品时,我们都可以使用一起购买的产品的 ID 来丰富它。 现在我们有了一个现成的产品推荐指数,下一步是创建一个丰富的策略。

PUT /_enrich/policy/product-recommendation-policy
{
  "match": {
    "indices": "product-recommendations",
    "match_field": "product_id",
    "enrich_fields": ["recommendation_ids"]
  }
}

POST /_enrich/policy/product-recommendation-policy/_execute

这会在该策略中创建一个中间索引。 请注意,这需要在更改后执行,以便策略与后台的索引保持同步。

接下来是一个摄取管道,它使用文档的 _id 字段查询上述策略。 重命名和删除处理器只是为了对数据结构进行一些清理,但完全是可选的。

PUT _ingest/pipeline/product-recommendations-pipeline
{
  "processors": [
    {
      "enrich": {
        "policy_name": "product-recommendation-policy",
        "field": "_id",
        "target_field": "ids"
      }
    },
    {
      "rename": {
        "field": "ids.recommendation_ids",
        "target_field": "recommendation_ids"
      }
    },
    {
      "remove": {
        "field": "ids"
      }
    }
  ]
}

最后是一些索引的时间,然后检索文档以查看管道是否正确应用:

PUT products/_doc/0bcc3eeca39e1064258aa1e932269894?pipeline=product-recommendations-pipeline
{
  "title" : "My favourite product"
}

GET products/_doc/0bcc3eeca39e1064258aa1e932269894

上面命令的输出为:

{
  "_index" : "products",
  "_type" : "_doc",
  "_id" : "0bcc3eeca39e1064258aa1e932269894",
  "_version" : 1,
  "_seq_no" : 0,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "title" : "My favourite product",
    "recommendation_ids" : [
      { "count" : 6, "id" : "422879e10f46682990de24d770e7f83d" },
      { "count" : 4, "id" : "368c6c730842d78016ad823897a372db" },
      { "count" : 3, "id" : "53759a2ecddad2bb87a079a1f1519f73" },
      { "count" : 3, "id" : "b0961721fd839e9982420e807758a2a6" },
      { "count" : 3, "id" : "389d119b48cf3043d311335e499d9c6b" },
      { "count" : 1, "id" : "dfc9d9ea4c2fbabe7abb0066097d9905" },
      { "count" : 1, "id" : "3773a3773f5978591cff7b3e614989b3" },
      { "count" : 1, "id" : "98e0de96ebb0711db1fc3d1bad1a902f" },
      { "count" : 1, "id" : "4e1346d7b7e02c737a366b086462e33e" },
      { "count" : 1, "id" : "4b5df063d69ffafb87c279672ecb4191" },
      { "count" : 1, "id" : "9d1893083966b9c51109f0dc6ab0b0d9" },
      { "count" : 1, "id" : "774e21c631ca26cba7b5606bdca7460f" },
      { "count" : 1, "id" : "d1c427060a0f73f6b889a5c7c61f2ac4" }
    ]
  }
}

重新过一遍上面的过程

所以,这真是太棒了,让我们快速重申一下我们在这里做了什么 – 以及我们为什么这样做。

  1. 导入订单项目的 CSV 数据,每个项目一行导致每个订单项目一个文档
  2. 使用转换将订单商品分组为单个订单,能够汇总通过查询一起购买的产品
  3. 使用转换将已购买的产品组合在一起,这样只需检索单个文档即可
  4. 改造转换以获取信息,哪个产品与其他产品一起购买了多少次,将大多数购买的产品组合在一起
  5. 根据最终转换索引创建丰富策略,以便可以在索引时间使用一起购买的产品丰富产品
%title插图%num
最终获得文档内推荐的四个步骤

考虑 recency

Recency 是我们在这个例子中还没有考虑到的东西,以保持它很小。在这个例子中,我们使用了 order-items 索引中的所有数据来创建一个 orders 索引。我们可以使用最近 1d 的数据作为范围查询,以便只考虑最近的订单项目。此外,每个转换都支持 retention_policy 字段,允许您删除早于某个时间戳的数据。如果你还更改了用于存储文档中 ctx._timestamp 字段的两个管道,那么你就会知道文档何时太旧。

另一方面,可能有一段时间未购买的产品的用例,以保留历史数据,而不是根本不提供任何建议,以及排除订单项目的某些日期(黑色星期五或圣诞节,或者当你做那个电视广告时)如果想排除这些数据以不歪曲结果 – 我总是会包括在内。

概括

像往常一样,总结是关于深思熟虑的。第一部分不是关于技术问题,例如聚合中的低效脚本,这些问题还有改进的余地。

最大的障碍可能是数据质量和数据可信度。

第一,数据质量。您是否有足够的订单和订单项目,这样的丰富实际上是有意义的?你确定你的数据没有被十几个人订购同样的东西所歪曲吗?

第二,数据可信。这实际上是要查找的正确数据吗?你是想显示订单推荐,还是根据用户访问的内容显示推荐,而不是购买,因为你可能拥有更多此类数据,从而更早地执行此操作。

第三,这是一个经典的“赢家通吃”的推荐方法,如果你只查看过去的数据,很难让新产品得分。此外,如果你有一些总是一起购买的常见商品(例如食品店中的牛奶和面包),那么尽管它们并不适合一起购买,但这些商品的结果会完全偏向于这些常见商品。这就是重要的术语聚合可以用来代替的 – 当前转换中不支持它,需要另一个过程来创建这些建议。

这种方法的一个优点是具有闭环。当你索引集群中的所有订单项时,你基本上拥有创建这些转换的闭环 – 即使创建丰富策略仍然是一项手动任务,你需要通过 cronjob 触发它。

运行最新的订单商品数据也可以考虑退货,因此当产品经常与另一种产品组合退货时(可能两者不合适),你可以忽略这些数据。

目前的方法有一些限制。首先,不可能将转换链接起来,一个接一个地运行,这样一旦你需要触发丰富策略,你就可以确保您在最新数据上运行。其次,正如已经提到的,你需要一个 cronjob 来触发策略丰富。

还有一件事:这种方法假设产品索引经常更新,否则那些推荐不会得到更新并且对产品推荐索引的查询更有效。请记住,即使使用单个转换,我们也已经处于通过查询检索推荐的阶段。

希望这可以帮助你了解,为了扩展你自己的数据,你还可以用转换做什么。

许多常见的例子都讨论了 Web 服务器日志(和创建总结会话),但转换 – 特别是多阶段的 – 更强大,并且可能会在你不知道的数据中显示信息。

快乐 transforming!

文章来源于互联网:Elasticsearch:使用 Elasticsearch Transforms 进行产品推荐

相关推荐: Kibana:如何在 Maps 应用中显示图片提示

Kibana 中的 Maps 给我们位置搜索及分析创建了许多的方便。在 Maps 应用中,有一个叫做 tooltip 的功能能。它可以帮我们在点击文档时显示文档的扼要信息。这些信息可以是我们想要的字段,由我们自己配置。有时这些字段可能是一个图片的 url,那么…