2016-05-06 6 views
2

Я пытаюсь создать простой механизм рекомендаций с помощью Neo4j и Reco4PHP.Рекомендации по повышению скорости Neo4j

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

(User) - [: HAS_BOUGHT] -> (продукт {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 секунд, чтобы вычислить не полезной в производственной среде.

Чтобы улучшить скорость у меня есть:

  • созданные индексы в продукте: идентификатор и конструктор: идентификатор
  • Добавить node_auto_indexing = истинный к neo4j.properties.
  • Добавить -Xmx4096m в .neo4j-community.vmoptions Но это действительно не имеет значения.

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

+0

Привет, я являюсь автором Reco4PHP. Очень здорово, что я никогда не ожидал увидеть вопрос о StackOverflow. На самом деле вы не отправили код DiscoveryEngine, можете ли вы вставить его, пожалуйста? На самом деле с вашим набором данных это должно работать в течение пары минут –

+0

@ user125553 Проверьте этот репозиторий и мой ответ: https://github.com/ikwattro/reco4php-example-so –

ответ

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

Update после получения данных

enter image description here

Тот факт, что у вас есть несколько отношений одного и того же типа между продуктом и пользователем добавления exponentionality на номер найденных паттернов.

Есть два решения:

Distinct их и добавить где положение в конце шаблона:

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

+0

Спасибо, я пробовал ваш запрос, и он немного быстрее, но не очень к сожалению. Даже если выполнить запрос вручную через браузер Neo4j, это займет несколько секунд. Вероятно, запрос слишком тяжелый. – user1255553

+0

Он работает под вторым на моем ноутбуке с 1000 пользователями и 5000 продуктами со степенью 100-200 покупок на пользователя, поэтому в основном он находит 5000 продуктов. Можете ли вы поделиться своим набором данных и своим репо? со мной: christophe at graphaware dot com –

0

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

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

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

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

+0

1. Да, reco - это продукт. 2. Да, поскольку datamodel выглядит как (Product - [: DESIGNED_BY] -> (Designer) 3. Код основан на шаблоне Reco4PHP, где результат класса обнаружения передается в постпроцессор (s) .Не думаю, что это должно быть изменено. – user1255553

Смежные вопросы