Улучшение скоростных рекомендаций Neo4j

Я пытаюсь создать простой механизм рекомендаций, используя Neo4j и Reco4PHP.

Модель данных состоит из следующих узлов и взаимосвязей:

(Пользователь) — [: HAS_BOUGHT] -> (Product {category_id: int}
) — [: DESIGNED_BY] -> (конструктор)

В этой системе я хочу рекомендовать продукты и улучшать продукты с тем же дизайнером, что и пользователь, уже купленный. Для создания рекомендаций я использую один класс Discovery и один класс Post-Processor для повышения качества продуктов. Увидеть ниже. Это работает, но это очень медленно. Это займет более 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 = верно до neo4j.properties.
  • добавлять -Xmx4096m to .neo4j-community.vmoptions
    Но это не имеет значения.

Это нормально, что эти запросы Cypher занимают более 5 секунд или есть некоторые улучшения возможно? 🙂

2

Решение

Основная проблема связана с вашим запросом постпроцессора. Целью является:

Повысить рекомендацию в зависимости от количества продуктов, которые я купил
дизайнер разработал рекомендованный товар.

Поэтому вы можете немного изменить свой запрос, чтобы он соответствовал непосредственно дизайнеру и агрегировал его, также лучше сначала найти пользователя до того, как UNWIND в противном случае он будет соответствовать пользователю на каждой итерации идентификаторов продукта:

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

Весь постпроцессор будет выглядеть так:

    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 мс с вашим текущим набором данных.

2

Другие решения

Я могу только комментировать Cypher и даже тогда не так много, так как вы не включили функцию GetItems () или data (cypher dump).
Но мало что выделяется

  1. Это будет быстрее использовать ярлык на (reco) Я предполагаю, что это продукт?
  2. Также я предполагаю, что это ярлык дизайнера, который можно вставить в — [: DESIGNED_BY] -> ()<-[:РАЗРАБОТАНО]?
  3. Если по какой-либо причине GetItems () получает элементы по одному, это может
    быть проблемой, а также где нужны индексы. Кстати, почему бы не поставить это условие в основной запрос?

Я тоже не понимаю индексы по id? Если это идентификатор Neo4j, это физическое местоположение, и их не нужно индексировать, и если это не так, почему вы используете функцию id ()?

В заключение, метки могут помочь, но не ожидайте чудес, если ваш набор данных большой, агрегаты не очень быстрые в Neo4j. Подсчет 10M записей без фильтров занял у меня 12 секунд.

0

По вопросам рекламы [email protected]