提高速度推荐 Neo4j
Improve speed recommendations Neo4j
我正在尝试使用 Neo4j 和 Reco4PHP 创建一个简单的推荐引擎。
数据模型由以下节点和关系组成:
(User)-[:HAS_BOUGHT]->(Product {category_id: int}
)-[:DESIGNED_BY]->(Designer)
在这个系统中,我想推荐产品并提升与用户已购买的设计师相同的产品。为了创建推荐,我使用一个 Discovery-class 和一个 Post-Processor class 来提升产品。见下文。这有效,但速度很慢。完成需要超过 5 秒,而数据模型包含约 1000 种产品和约 100 位设计师。
// Disovery class
<?php
namespace App\Reco4PHP\Discovery;
use GraphAware\Common\Cypher\Statement;
use GraphAware\Common\Type\NodeInterface;
use GraphAware\Reco4PHP\Engine\SingleDiscoveryEngine;
class InCategory extends SingleDiscoveryEngine {
protected $categoryId;
public function __construct($categoryId) {
$this->categoryId = $categoryId;
}
/**
* @return string The name of the discovery engine
*/
public function name() {
return 'in_category';
}
/**
* The statement to be executed for finding items to be recommended
*
* @param \GraphAware\Common\Type\NodeInterface $input
* @return \GraphAware\Common\Cypher\Statement
*/
public function discoveryQuery(NodeInterface $input) {
$query = "
MATCH (reco:Card)
WHERE reco.category_id = {category_id}
RETURN reco, 1 as score
";
return Statement::create($query, ['category_id' => $this->categoryId]);
}
}
// Boost shared designers
class RewardSharedDesigners extends RecommendationSetPostProcessor {
public function buildQuery(NodeInterface $input, Recommendations $recommendations)
{
$ids = [];
foreach ($recommendations->getItems() as $recommendation) {
$ids[] = $recommendation->item()->identity();
}
$query = 'UNWIND {ids} as id
MATCH (reco) WHERE id(reco) = id
MATCH (user:User) WHERE id(user) = {userId}
MATCH (user)-[:HAS_BOUGHT]->(product:Product)-[:DESIGNED_BY]->()<-[:DESIGNED_BY]-(reco)
RETURN id, count(product) as sharedDesignedBy';
return Statement::create($query, ['ids' => $ids, 'userId' => $input->identity()]);
}
public function postProcess(Node $input, Recommendation $recommendation, Record $record) {
$recommendation->addScore($this->name(), new SingleScore((int)$record->get('sharedDesignedBy')));
}
public function name() {
return 'reward_shared_designers';
}
}
我很高兴它能工作,但如果计算时间超过 5 秒,它就无法在生产环境中使用。
为了提高速度我有:
- 在 Product:id 和 Designer:id
中创建了索引
- 将 node_auto_indexing=true 添加到 neo4j.properties.
- 添加 -Xmx4096m 到 .neo4j-community.vmoptions
但这并没有什么区别。
这些 Cypher 查询花费超过 5 秒是正常的,或者是否可以进行一些改进? :)
我只能对 Cypher 发表评论,即便如此也不能评论太多,因为您没有包括函数 GetItems() 或数据(密码转储)。
但很少有事情脱颖而出
- 在 (reco) 上使用标签会更快我假设它是产品?
- 我还假设这是可以放入的 Designer 标签 - [:DESIGNED_BY]->()<-[:DESIGNED_BY]?
- 如果有任何机会 GetItems() 一个一个地检索项目,那可能
是问题所在,也是需要索引的地方。顺便说一句,为什么不把这个条件放在主查询中呢?
我也不明白 id 上的索引?如果它们是 Neo4j id,它们就是物理位置,不需要索引,如果它们不是,为什么要使用 id() 函数?
总而言之,标签可能会有所帮助,但如果您的数据集很大,请不要指望奇迹,Neo4j 上的聚合速度不是很快。在没有过滤器的情况下统计 1000 万条记录花了我 12 秒。
主要问题出在您的 post 处理器查询上。目标是:
Boost the recommendation based on the number of products I bought from
the designer having designed the recommended item.
因此,您可以稍微修改您的查询以直接匹配设计者并在其上聚合,而且最好在 UNWIND
之前先找到用户,否则它会在每次迭代时匹配用户产品 ID:
MATCH (user) WHERE id(user) = {userId}
UNWIND {ids} as productId
MATCH (product:Product)-[:DESIGNED_BY]->(designer)
WHERE id(product) = productId
WITH productId, designer, user
MATCH (user)-[:BOUGHT]->(p)-[:DESIGNED_BY]->(designer)
RETURN productId as id, count(*) as score
完整的 post 处理器如下所示:
public function buildQuery(NodeInterface $input, Recommendations $recommendations)
{
$ids = [];
foreach ($recommendations->getItems() as $recommendation) {
$ids[] = $recommendation->item()->identity();
}
$query = 'MATCH (user) WHERE id(user) = {userId}
UNWIND {ids} as productId
MATCH (product:Product)-[:DESIGNED_BY]->(designer)
WHERE id(product) = productId
WITH productId, designer, user
MATCH (user)-[:BOUGHT]->(p)-[:DESIGNED_BY]->(designer)
RETURN productId as id, count(*) as score';
return Statement::create($query, ['userId' => $input->identity(), 'ids' => $ids]);
}
public function postProcess(Node $input, Recommendation $recommendation, Record $record)
{
$recommendation->addScore($this->name(), new SingleScore($record->get('score')));
}
我创建了一个存储库,其中我有一个功能完备的实现,遵循您的域:
https://github.com/ikwattro/reco4php-example-so
收到数据后更新
您在产品和用户之间拥有多个相同类型的关系这一事实增加了找到的模式数量的指数。
有两种解决方案:
区分它们并在模式末尾添加一个 WHERE 子句:
MATCH (user) WHERE id(user) = {userId}
UNWIND {ids} as cardId
MATCH (reco:Card)-[:DESIGNED_BY]->(designer) WHERE id(reco) = cardId
MATCH (user)-[:HAS_BOUGHT]->(x)
WHERE (x)-[:DESIGNED_BY]->(designer)
RETURN cardId as id, count(*) as sharedDesignedBy
在 Neo4j 3.0+ 中,您可以从 USING JOIN
用法中受益并保持与之前相同的查询:
MATCH (user) WHERE user.id = 245
UNWIND ids as id
MATCH (reco:Card) WHERE id(reco) = id
MATCH (user:User)-[:HAS_BOUGHT]->(card:Card)-[:DESIGNED_BY]->(designer:Designer)<-[:DESIGNED_BY]-(reco:Card)
USING JOIN ON card
RETURN id, count(card) as sharedDesignedBy
运行 那些查询,我用你当前的数据集将 discovery
+ post processing
的时间减少到 190 毫秒。
我正在尝试使用 Neo4j 和 Reco4PHP 创建一个简单的推荐引擎。
数据模型由以下节点和关系组成:
(User)-[:HAS_BOUGHT]->(Product {category_id: int} )-[:DESIGNED_BY]->(Designer)
在这个系统中,我想推荐产品并提升与用户已购买的设计师相同的产品。为了创建推荐,我使用一个 Discovery-class 和一个 Post-Processor class 来提升产品。见下文。这有效,但速度很慢。完成需要超过 5 秒,而数据模型包含约 1000 种产品和约 100 位设计师。
// Disovery class
<?php
namespace App\Reco4PHP\Discovery;
use GraphAware\Common\Cypher\Statement;
use GraphAware\Common\Type\NodeInterface;
use GraphAware\Reco4PHP\Engine\SingleDiscoveryEngine;
class InCategory extends SingleDiscoveryEngine {
protected $categoryId;
public function __construct($categoryId) {
$this->categoryId = $categoryId;
}
/**
* @return string The name of the discovery engine
*/
public function name() {
return 'in_category';
}
/**
* The statement to be executed for finding items to be recommended
*
* @param \GraphAware\Common\Type\NodeInterface $input
* @return \GraphAware\Common\Cypher\Statement
*/
public function discoveryQuery(NodeInterface $input) {
$query = "
MATCH (reco:Card)
WHERE reco.category_id = {category_id}
RETURN reco, 1 as score
";
return Statement::create($query, ['category_id' => $this->categoryId]);
}
}
// Boost shared designers
class RewardSharedDesigners extends RecommendationSetPostProcessor {
public function buildQuery(NodeInterface $input, Recommendations $recommendations)
{
$ids = [];
foreach ($recommendations->getItems() as $recommendation) {
$ids[] = $recommendation->item()->identity();
}
$query = 'UNWIND {ids} as id
MATCH (reco) WHERE id(reco) = id
MATCH (user:User) WHERE id(user) = {userId}
MATCH (user)-[:HAS_BOUGHT]->(product:Product)-[:DESIGNED_BY]->()<-[:DESIGNED_BY]-(reco)
RETURN id, count(product) as sharedDesignedBy';
return Statement::create($query, ['ids' => $ids, 'userId' => $input->identity()]);
}
public function postProcess(Node $input, Recommendation $recommendation, Record $record) {
$recommendation->addScore($this->name(), new SingleScore((int)$record->get('sharedDesignedBy')));
}
public function name() {
return 'reward_shared_designers';
}
}
我很高兴它能工作,但如果计算时间超过 5 秒,它就无法在生产环境中使用。
为了提高速度我有:
- 在 Product:id 和 Designer:id 中创建了索引
- 将 node_auto_indexing=true 添加到 neo4j.properties.
- 添加 -Xmx4096m 到 .neo4j-community.vmoptions 但这并没有什么区别。
这些 Cypher 查询花费超过 5 秒是正常的,或者是否可以进行一些改进? :)
我只能对 Cypher 发表评论,即便如此也不能评论太多,因为您没有包括函数 GetItems() 或数据(密码转储)。 但很少有事情脱颖而出
- 在 (reco) 上使用标签会更快我假设它是产品?
- 我还假设这是可以放入的 Designer 标签 - [:DESIGNED_BY]->()<-[:DESIGNED_BY]?
- 如果有任何机会 GetItems() 一个一个地检索项目,那可能 是问题所在,也是需要索引的地方。顺便说一句,为什么不把这个条件放在主查询中呢?
我也不明白 id 上的索引?如果它们是 Neo4j id,它们就是物理位置,不需要索引,如果它们不是,为什么要使用 id() 函数?
总而言之,标签可能会有所帮助,但如果您的数据集很大,请不要指望奇迹,Neo4j 上的聚合速度不是很快。在没有过滤器的情况下统计 1000 万条记录花了我 12 秒。
主要问题出在您的 post 处理器查询上。目标是:
Boost the recommendation based on the number of products I bought from the designer having designed the recommended item.
因此,您可以稍微修改您的查询以直接匹配设计者并在其上聚合,而且最好在 UNWIND
之前先找到用户,否则它会在每次迭代时匹配用户产品 ID:
MATCH (user) WHERE id(user) = {userId}
UNWIND {ids} as productId
MATCH (product:Product)-[:DESIGNED_BY]->(designer)
WHERE id(product) = productId
WITH productId, designer, user
MATCH (user)-[:BOUGHT]->(p)-[:DESIGNED_BY]->(designer)
RETURN productId as id, count(*) as score
完整的 post 处理器如下所示:
public function buildQuery(NodeInterface $input, Recommendations $recommendations)
{
$ids = [];
foreach ($recommendations->getItems() as $recommendation) {
$ids[] = $recommendation->item()->identity();
}
$query = 'MATCH (user) WHERE id(user) = {userId}
UNWIND {ids} as productId
MATCH (product:Product)-[:DESIGNED_BY]->(designer)
WHERE id(product) = productId
WITH productId, designer, user
MATCH (user)-[:BOUGHT]->(p)-[:DESIGNED_BY]->(designer)
RETURN productId as id, count(*) as score';
return Statement::create($query, ['userId' => $input->identity(), 'ids' => $ids]);
}
public function postProcess(Node $input, Recommendation $recommendation, Record $record)
{
$recommendation->addScore($this->name(), new SingleScore($record->get('score')));
}
我创建了一个存储库,其中我有一个功能完备的实现,遵循您的域:
https://github.com/ikwattro/reco4php-example-so
收到数据后更新
您在产品和用户之间拥有多个相同类型的关系这一事实增加了找到的模式数量的指数。
有两种解决方案:
区分它们并在模式末尾添加一个 WHERE 子句:
MATCH (user) WHERE id(user) = {userId}
UNWIND {ids} as cardId
MATCH (reco:Card)-[:DESIGNED_BY]->(designer) WHERE id(reco) = cardId
MATCH (user)-[:HAS_BOUGHT]->(x)
WHERE (x)-[:DESIGNED_BY]->(designer)
RETURN cardId as id, count(*) as sharedDesignedBy
在 Neo4j 3.0+ 中,您可以从 USING JOIN
用法中受益并保持与之前相同的查询:
MATCH (user) WHERE user.id = 245
UNWIND ids as id
MATCH (reco:Card) WHERE id(reco) = id
MATCH (user:User)-[:HAS_BOUGHT]->(card:Card)-[:DESIGNED_BY]->(designer:Designer)<-[:DESIGNED_BY]-(reco:Card)
USING JOIN ON card
RETURN id, count(card) as sharedDesignedBy
运行 那些查询,我用你当前的数据集将 discovery
+ post processing
的时间减少到 190 毫秒。