From 2a3dad21c0828065de5e9ed1525c9de9973a6885 Mon Sep 17 00:00:00 2001 From: tienvx Date: Sun, 23 Oct 2022 12:10:05 +0700 Subject: [PATCH] Replace client --- .github/workflows/main.yml | 44 +++ .gitignore | 3 +- .php-cs-fixer.php | 18 ++ .styleci.yml | 9 - .travis.yml | 19 -- README.md | 261 ++++++++++++------ UPGRADE.md | 76 +++++ _algo-examples/cosine-similarity.php | 43 ++- _demo/Github/FollowedByFollowers.php | 34 +++ .../PenalizeTooMuchFollowers.php | 24 +- _demo/Github/RecommendationEngine.php | 28 ++ _demo/Github/SameContribution.php | 32 +++ _demo/github/FollowedByFollowers.php | 34 --- _demo/github/RecommendationEngine.php | 28 -- _demo/github/SameContribution.php | 33 --- build/install-jdk8.sh | 11 - build/install-neo.sh | 10 - composer.json | 10 +- example.php | 20 +- phpunit.xml | 8 +- src/Algorithms/Model/KNNModelBuilder.php | 16 +- src/Algorithms/Model/Rating.php | 22 +- .../Similarity/CosineSimilarity.php | 6 +- src/Algorithms/Similarity/Similarity.php | 2 +- src/Common/NodeSet.php | 63 ----- src/Common/ObjectSet.php | 24 +- src/Config/Config.php | 6 +- src/Config/KeyValueConfig.php | 32 +-- src/Config/SimpleConfig.php | 24 +- src/Context/Context.php | 12 +- src/Context/SimpleContext.php | 27 +- src/Context/Statistics.php | 43 +-- src/Cypher/Query.php | 9 - src/Engine/BaseRecommendationEngine.php | 73 ++--- src/Engine/DiscoveryEngine.php | 40 +-- src/Engine/RecommendationEngine.php | 64 ++--- src/Engine/SingleDiscoveryEngine.php | 48 ++-- src/Executor/DiscoveryPhaseExecutor.php | 42 ++- src/Executor/PostProcessPhaseExecutor.php | 38 +-- src/Executor/RecommendationExecutor.php | 49 ++-- src/Filter/BaseBlacklistBuilder.php | 20 +- src/Filter/BlackListBuilder.php | 28 +- src/Filter/ExcludeSelf.php | 6 +- src/Filter/Filter.php | 9 +- src/Graph/Direction.php | 6 +- src/Persistence/DatabaseService.php | 21 +- src/Post/BasePostProcessor.php | 16 -- src/Post/CypherAwarePostProcessor.php | 10 +- src/Post/PostProcessor.php | 2 +- src/Post/RecommendationSetPostProcessor.php | 40 ++- src/Post/RewardSomethingShared.php | 28 +- src/RecommenderService.php | 91 ++---- src/Result/PartialScore.php | 83 ------ src/Result/Reason.php | 35 --- src/Result/Recommendation.php | 50 +--- src/Result/Recommendations.php | 85 ++---- src/Result/ResultCollection.php | 46 +++ src/Result/Score.php | 25 +- src/Result/SingleScore.php | 31 +-- src/Transactional/BaseCypherAware.php | 37 --- src/Transactional/CypherAware.php | 19 -- src/Util/NodeProxy.php | 92 ------ .../Algorithms/Model/KNNModelBuilderTest.php | 77 +++--- tests/Config/SimpleConfigUnitTest.php | 7 +- tests/Context/ContextUnitTest.php | 6 +- tests/Engine/OverrideDiscoveryEngine.php | 37 +-- tests/Engine/RecommendationEngineTest.php | 7 +- tests/Engine/SingleDiscoveryEngineTest.php | 35 +-- tests/Engine/TestDiscoveryEngine.php | 18 +- .../Example/Discovery/FromSameGenreILike.php | 15 +- tests/Example/Discovery/RatedByOthers.php | 18 +- tests/Example/ExampleRecommendationEngine.php | 40 +-- tests/Example/ExampleRecommenderService.php | 19 +- ...lacklist.php => AlreadyRatedBlackList.php} | 14 +- tests/Example/Filter/ExcludeOldMovies.php | 13 +- .../PostProcessing/RewardWellRated.php | 23 +- tests/Helper/FakeNode.php | 81 +----- tests/Integration/Model/FriendsEngine.php | 16 +- tests/Integration/Model/RecoEngine.php | 23 +- tests/Integration/Model/SimpleBlacklist.php | 13 +- .../SimpleFriendsRecoEngineTest.php | 50 ++-- tests/Result/RecommendationsListTest.php | 15 +- 82 files changed, 1061 insertions(+), 1631 deletions(-) create mode 100644 .github/workflows/main.yml create mode 100644 .php-cs-fixer.php delete mode 100644 .styleci.yml delete mode 100644 .travis.yml create mode 100644 UPGRADE.md create mode 100644 _demo/Github/FollowedByFollowers.php rename _demo/{github => Github}/PenalizeTooMuchFollowers.php (62%) create mode 100644 _demo/Github/RecommendationEngine.php create mode 100644 _demo/Github/SameContribution.php delete mode 100644 _demo/github/FollowedByFollowers.php delete mode 100644 _demo/github/RecommendationEngine.php delete mode 100644 _demo/github/SameContribution.php delete mode 100755 build/install-jdk8.sh delete mode 100755 build/install-neo.sh delete mode 100644 src/Common/NodeSet.php delete mode 100644 src/Cypher/Query.php delete mode 100644 src/Post/BasePostProcessor.php delete mode 100644 src/Result/PartialScore.php delete mode 100644 src/Result/Reason.php create mode 100644 src/Result/ResultCollection.php delete mode 100644 src/Transactional/BaseCypherAware.php delete mode 100644 src/Transactional/CypherAware.php delete mode 100644 src/Util/NodeProxy.php rename tests/Example/Filter/{AlreadyRatedBlacklist.php => AlreadyRatedBlackList.php} (50%) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..5ab54ef --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,44 @@ +name: Main + +on: [push, pull_request] + +jobs: + phpunit: + runs-on: ubuntu-latest + strategy: + matrix: + php: [8.0, 8.1] + name: PHP ${{ matrix.php }} + steps: + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + tools: php-cs-fixer:3 + + - name: Checkout + uses: actions/checkout@v3 + + - name: Run PHP CS Fixer + run: php-cs-fixer fix --diff --dry-run + + - name: Install + uses: "ramsey/composer-install@v2" + + - name: Run tests + run: ./vendor/bin/phpunit + services: + neo4j: + image: neo4j + ports: + - 7687:7687 + - 7474:7474 + env: + NEO4J_AUTH: none + options: >- + --health-cmd "wget http://localhost:7474 || exit 1" + --health-interval 1s + --health-timeout 10s + --health-retries 20 + --health-start-period 3s diff --git a/.gitignore b/.gitignore index 2d24543..27eaed8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ vendor/ composer.lock -.DS_Store \ No newline at end of file +.DS_Store +.phpunit.result.cache diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..d2f9c2c --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,18 @@ +in(__DIR__) + ->exclude('.github') + ->exclude('vendor') +; + +$config = new PhpCsFixer\Config(); + +return $config + ->setRules([ + '@PSR12' => true, + '@Symfony' => true, + ]) + ->setUsingCache(false) + ->setFinder($finder) +; diff --git a/.styleci.yml b/.styleci.yml deleted file mode 100644 index 9faf6cc..0000000 --- a/.styleci.yml +++ /dev/null @@ -1,9 +0,0 @@ -preset: symfony - -finder: - exclude: - - "build" - - "vendor" - -enabled: - - short_array_syntax diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 556bb70..0000000 --- a/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -language: php -php: - - 7.0 - - 7.1 - -before_install: - - sudo apt-get update > /dev/null - # install Oracle JDK8 - - sh -c ./build/install-jdk8.sh - # install and launch neo4j - - sh -c ./build/install-neo.sh - - composer install --prefer-source --no-interaction - - composer self-update - -script: - - vendor/bin/phpunit - -notifications: - email: "christophe@graphaware.com" \ No newline at end of file diff --git a/README.md b/README.md index 20421d8..4b78e62 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ GraphAware Reco4PHP is a library for building complex recommendation engines atop Neo4j. -[![Build Status](https://travis-ci.org/graphaware/neo4j-php-client.svg)](https://travis-ci.org/graphaware/reco4php) +[![Build Status](https://github.com/graphaware/reco4php/workflows/main/badge.svg)](https://github.com/graphaware/reco4php/actions) Features: @@ -15,8 +15,8 @@ Features: Requirements: -* PHP7.0+ -* Neo4j 2.2.6+ (Neo4j 3.0+ recommended) +* PHP8.0+ +* Neo4j 3.5 / 4.0+ The library imposes a specific recommendation engine architecture, which has emerged from our experience building recommendation engines and solves the architectural challenge to run recommendation engines remotely via Cypher. @@ -79,16 +79,18 @@ The dataset is publicly available here : http://grouplens.org/datasets/movielens Once downloaded and extracted the archive, you can run the following Cypher statements for importing the dataset, just adapt the file urls to match your actual path to the files : +> **_NOTE:_** This is Cypher version 4.4 syntax. + ``` -CREATE CONSTRAINT ON (m:Movie) ASSERT m.id IS UNIQUE; -CREATE CONSTRAINT ON (g:Genre) ASSERT g.name IS UNIQUE; -CREATE CONSTRAINT ON (u:User) ASSERT u.id IS UNIQUE; +CREATE CONSTRAINT FOR (m:Movie) REQUIRE m.id IS UNIQUE; +CREATE CONSTRAINT FOR (g:Genre) REQUIRE g.name IS UNIQUE; +CREATE CONSTRAINT FOR (u:User) REQUIRE u.id IS UNIQUE; ``` ``` LOAD CSV WITH HEADERS FROM "file:///Users/ikwattro/dev/movielens/movies.csv" AS row WITH row -MERGE (movie:Movie {id: toInt(row.movieId)}) +MERGE (movie:Movie {id: toInteger(row.movieId)}) ON CREATE SET movie.title = row.title WITH movie, row UNWIND split(row.genres, '|') as genre @@ -98,16 +100,19 @@ MERGE (movie)-[:HAS_GENRE]->(g) ``` -USING PERIODIC COMMIT 500 -LOAD CSV WITH HEADERS FROM "file:///Users/ikwattro/dev/movielens/ratings.csv" AS row +:auto LOAD CSV WITH HEADERS FROM "file:///Users/ikwattro/dev/movielens/ratings.csv" AS row WITH row -MATCH (movie:Movie {id: toInt(row.movieId)}) -MERGE (user:User {id: toInt(row.userId)}) -MERGE (user)-[r:RATED]->(movie) -ON CREATE SET r.rating = toInt(row.rating), r.timestamp = toInt(row.timestamp) +LIMIT 500 +CALL { + WITH row + MATCH (movie:Movie {id: toInteger(row.movieId)}) + MERGE (user:User {id: toInteger(row.userId)}) + MERGE (user)-[r:RATED]->(movie) + ON CREATE SET r.rating = toInteger(row.rating), r.timestamp = toInteger(row.timestamp) +} IN TRANSACTIONS ``` -For the purpose of the example, we will assume we are recommending movies for the User with ID 460. +For the purpose of the example, we will assume we are recommending movies for the User with ID 4. ### Installation @@ -127,39 +132,72 @@ In order to recommend movies people should watch, you have decided that we shoul * Find movies rated by people who rated the same movies than me, but that I didn't rated yet As told before, the `reco4php` recommendation engine framework makes all the plumbing so you only have to concentrate on the business logic, that's why it provides base class that you should extend and just implement -the methods of the upper interfaces, here is how you would create your first discovery engine : +the methods of the upper interfaces, here are how you would create your first discovery engines : ```php (m)<-[:RATED]-(o) WITH distinct o MATCH (o)-[:RATED]->(reco) RETURN distinct reco LIMIT 500'; - return Statement::create($query, ['id' => $input->identity()]); + return Statement::create($query, ['id' => $input->getId()]); } - public function name() + public function name(): string { - return "rated_by_others"; + return 'rated_by_others'; } } ``` -The `discoveryMethod` method should return a `Statement` object containing the query for finding recommendations, +```php +(movie)-[:HAS_GENRE]->(genre) + WITH distinct genre, sum(r.rating) as score + ORDER BY score DESC + LIMIT 15 + MATCH (genre)<-[:HAS_GENRE]-(reco) + RETURN reco + LIMIT 200'; + + return Statement::create($query, ['id' => $input->getId()]); + } +} +``` + +The `discoveryQuery` method should return a `Statement` object containing the query for finding recommendations, the `name` method should return a string describing the name of your engine (this is mostly for logging purposes). The query here has some logic, we don't want to return as candidates all the movies found, as in the initial dataset it would be 10k+, so imagine what it would be on a 100M dataset. So we are summing the score @@ -181,19 +219,19 @@ As an example of a filter, we will filter the movies that were produced before t namespace GraphAware\Reco4PHP\Tests\Example\Filter; -use GraphAware\Common\Type\Node; use GraphAware\Reco4PHP\Filter\Filter; +use Laudis\Neo4j\Types\Node; class ExcludeOldMovies implements Filter { - public function doInclude(Node $input, Node $item) + public function doInclude(Node $input, Node $item): bool { - $title = $item->value("title"); + $title = (string) $item->getProperty('title'); preg_match('/(?:\()\d+(?:\))/', $title, $matches); if (isset($matches[0])) { - $y = str_replace('(','',$matches[0]); - $y = str_replace(')','', $y); + $y = str_replace('(', '', $matches[0]); + $y = str_replace(')', '', $y); $year = (int) $y; if ($year < 1999) { return false; @@ -218,22 +256,22 @@ Of course we do not want to recommend movies that the current user has already r namespace GraphAware\Reco4PHP\Tests\Example\Filter; -use GraphAware\Common\Cypher\Statement; -use GraphAware\Common\Type\Node; use GraphAware\Reco4PHP\Filter\BaseBlacklistBuilder; +use Laudis\Neo4j\Databags\Statement; +use Laudis\Neo4j\Types\Node; class AlreadyRatedBlackList extends BaseBlacklistBuilder { - public function blacklistQuery(Node $input) + public function blacklistQuery(Node $input): Statement { - $query = 'MATCH (input) WHERE id(input) = {inputId} + $query = 'MATCH (input) WHERE id(input) = $inputId MATCH (input)-[:RATED]->(movie) RETURN movie as item'; - return Statement::create($query, ['inputId' => $input->identity()]); + return Statement::create($query, ['inputId' => $input->getId()]); } - public function name() + public function name(): string { return 'already_rated'; } @@ -252,39 +290,39 @@ nodes against the blacklists provided. namespace GraphAware\Reco4PHP\Tests\Example\PostProcessing; -use GraphAware\Common\Cypher\Statement; -use GraphAware\Common\Result\Record; -use GraphAware\Common\Type\Node; use GraphAware\Reco4PHP\Post\RecommendationSetPostProcessor; use GraphAware\Reco4PHP\Result\Recommendation; use GraphAware\Reco4PHP\Result\Recommendations; use GraphAware\Reco4PHP\Result\SingleScore; +use Laudis\Neo4j\Databags\Statement; +use Laudis\Neo4j\Types\CypherMap; +use Laudis\Neo4j\Types\Node; class RewardWellRated extends RecommendationSetPostProcessor { - public function buildQuery(Node $input, Recommendations $recommendations) + public function buildQuery(Node $input, Recommendations $recommendations): Statement { - $query = 'UNWIND {ids} as id + $query = 'UNWIND $ids as id MATCH (n) WHERE id(n) = id MATCH (n)<-[r:RATED]-(u) RETURN id(n) as id, sum(r.rating) as score'; $ids = []; foreach ($recommendations->getItems() as $item) { - $ids[] = $item->item()->identity(); + $ids[] = $item->item()->getId(); } return Statement::create($query, ['ids' => $ids]); } - public function postProcess(Node $input, Recommendation $recommendation, Record $record) + public function postProcess(Node $input, Recommendation $recommendation, CypherMap $result): void { - $recommendation->addScore($this->name(), new SingleScore($record->get('score'), 'total_ratings_relationships')); + $recommendation->addScore($this->name(), new SingleScore((float) $result->get('score'), 'total_ratings_relationships')); } - public function name() + public function name(): string { - return "reward_well_rated"; + return 'reward_well_rated'; } } ``` @@ -299,44 +337,46 @@ Now that our components are created, we need to build effectively our recommenda namespace GraphAware\Reco4PHP\Tests\Example; use GraphAware\Reco4PHP\Engine\BaseRecommendationEngine; +use GraphAware\Reco4PHP\Tests\Example\Discovery\FromSameGenreILike; +use GraphAware\Reco4PHP\Tests\Example\Discovery\RatedByOthers; use GraphAware\Reco4PHP\Tests\Example\Filter\AlreadyRatedBlackList; use GraphAware\Reco4PHP\Tests\Example\Filter\ExcludeOldMovies; use GraphAware\Reco4PHP\Tests\Example\PostProcessing\RewardWellRated; -use GraphAware\Reco4PHP\Tests\Example\Discovery\RatedByOthers; class ExampleRecommendationEngine extends BaseRecommendationEngine { - public function name() + public function name(): string { - return "example"; + return 'user_movie_reco'; } - public function discoveryEngines() + public function discoveryEngines(): array { - return array( - new RatedByOthers() - ); + return [ + new RatedByOthers(), + new FromSameGenreILike(), + ]; } - public function blacklistBuilders() + public function blacklistBuilders(): array { - return array( - new AlreadyRatedBlackList() - ); + return [ + new AlreadyRatedBlackList(), + ]; } - public function postProcessors() + public function postProcessors(): array { - return array( - new RewardWellRated() - ); + return [ + new RewardWellRated(), + ]; } - public function filters() + public function filters(): array { - return array( - new ExcludeOldMovies() - ); + return [ + new ExcludeOldMovies(), + ]; } } ``` @@ -351,32 +391,25 @@ namespace GraphAware\Reco4PHP\Tests\Example; use GraphAware\Reco4PHP\Context\SimpleContext; use GraphAware\Reco4PHP\RecommenderService; +use GraphAware\Reco4PHP\Result\Recommendations; class ExampleRecommenderService { - /** - * @var \GraphAware\Reco4PHP\RecommenderService - */ - protected $service; + protected RecommenderService $service; /** * ExampleRecommenderService constructor. - * @param string $databaseUri */ - public function __construct($databaseUri) + public function __construct(string $databaseUri) { $this->service = RecommenderService::create($databaseUri); $this->service->registerRecommendationEngine(new ExampleRecommendationEngine()); } - /** - * @param int $id - * @return \GraphAware\Reco4PHP\Result\Recommendations - */ - public function recommendMovieForUserWithId($id) + public function recommendMovieForUserWithId(int $id): Recommendations { $input = $this->service->findInputBy('User', 'id', $id); - $recommendationEngine = $this->service->getRecommender("user_movie_reco"); + $recommendationEngine = $this->service->getRecommender('user_movie_reco'); return $recommendationEngine->recommend($input, new SimpleContext()); } @@ -390,9 +423,14 @@ The `recommend()` method on a recommendation engine will returns you a `Recommen Each score is inserted so you can easily inspect why such recommendation has been produced, example : ```php +recommendMovieForUserWithId(460); +use GraphAware\Reco4PHP\Tests\Example\ExampleRecommenderService; + +$recommender = new ExampleRecommenderService('bolt://localhost:7687'); +$recommendations = $recommender->recommendMovieForUserWithId(4); print_r($recommendations->getItems(1)); @@ -400,18 +438,59 @@ Array ( [0] => GraphAware\Reco4PHP\Result\Recommendation Object ( - [item:protected] => GraphAware\Bolt\Result\Type\Node Object + [item:protected] => Laudis\Neo4j\Types\Node Object ( - [identity:protected] => 13248 - [labels:protected] => Array + [id:Laudis\Neo4j\Types\Node:private] => 2700 + [labels:Laudis\Neo4j\Types\Node:private] => Laudis\Neo4j\Types\CypherList Object ( - [0] => Movie + [keyCache:protected] => Array + ( + [0] => 0 + ) + + [cache:protected] => Array + ( + [0] => Movie + ) + + [cacheLimit:Laudis\Neo4j\Types\AbstractCypherSequence:private] => 9223372036854775807 + [currentPosition:protected] => 0 + [generatorPosition:protected] => 1 + [generator:protected] => ArrayIterator Object + ( + [storage:ArrayIterator:private] => Array + ( + ) + + ) + ) - [properties:protected] => Array + [properties:Laudis\Neo4j\Types\Node:private] => Laudis\Neo4j\Types\CypherMap Object ( - [id] => 2571 - [title] => Matrix, The (1999) + [keyCache:protected] => Array + ( + [0] => id + [1] => title + ) + + [cache:protected] => Array + ( + [id] => 3578 + [title] => Gladiator (2000) + ) + + [cacheLimit:Laudis\Neo4j\Types\AbstractCypherSequence:private] => 9223372036854775807 + [currentPosition:protected] => 0 + [generatorPosition:protected] => 2 + [generator:protected] => ArrayIterator Object + ( + [storage:ArrayIterator:private] => Array + ( + ) + + ) + ) ) @@ -420,13 +499,12 @@ Array ( [rated_by_others] => GraphAware\Reco4PHP\Result\Score Object ( - [score:protected] => 1067 + [score:protected] => 1 [scores:protected] => Array ( [0] => GraphAware\Reco4PHP\Result\SingleScore Object ( - [score:GraphAware\Reco4PHP\Result\SingleScore:private] => 1067 - [reason:GraphAware\Reco4PHP\Result\SingleScore:private] => + [score:GraphAware\Reco4PHP\Result\SingleScore:private] => 1 ) ) @@ -435,13 +513,13 @@ Array [reward_well_rated] => GraphAware\Reco4PHP\Result\Score Object ( - [score:protected] => 261 + [score:protected] => 9 [scores:protected] => Array ( [0] => GraphAware\Reco4PHP\Result\SingleScore Object ( - [score:GraphAware\Reco4PHP\Result\SingleScore:private] => 261 - [reason:GraphAware\Reco4PHP\Result\SingleScore:private] => + [score:GraphAware\Reco4PHP\Result\SingleScore:private] => 9 + [reason:GraphAware\Reco4PHP\Result\SingleScore:private] => total_ratings_relationships ) ) @@ -450,8 +528,9 @@ Array ) - [totalScore:protected] => 261 + [totalScore:protected] => 10 ) + ) ``` ### License diff --git a/UPGRADE.md b/UPGRADE.md new file mode 100644 index 0000000..a1442aa --- /dev/null +++ b/UPGRADE.md @@ -0,0 +1,76 @@ +# Upgrades + +## 2.0 to 3.0 + +```diff +- use GraphAware\Common\Type\Node; +- use GraphAware\Common\Type\NodeInterface; ++ use Laudis\Neo4j\Types\Node; + +- $node->identity() ++ $node->getId() + +- $node->get('login') ++ $node->getProperty('login') + +- $node->hasValue($key) ++ $node->getProperties()->hasKey($key) + +- $node->value($key) ++ $node->getProperty($key) +``` + +```diff +- use GraphAware\Common\Cypher\StatementInterface; +- use GraphAware\Common\Cypher\Statement; ++ use Laudis\Neo4j\Databags\Statement; +``` + +```diff +- use GraphAware\Common\Result\Record; +- use GraphAware\Common\Result\RecordViewInterface; ++ use Laudis\Neo4j\Types\CypherMap; + +- RecordViewInterface $record +- Record $record ++ CypherMap $result + +- $record->hasValue($key) ++ $result->hasKey($key) + +- $record->value($key) ++ $result->get($key) +``` + + +```diff +- use GraphAware\Common\Result\ResultCollection; ++ use GraphAware\Reco4PHP\Result\ResultCollection; +``` + +```diff +- use GraphAware\Common\Result\Result; ++ use Laudis\Neo4j\Types\CypherList; ++ use Laudis\Neo4j\Types\CypherMap; + +- Result $result ++ CypherList $results + +- foreach ($result->records() as $record) { ++ /** @var CypherMap $result */ ++ foreach ($results as $result) { + +- $result->getRecord() ++ $results->first() + +- $result->firstRecord() ++ $results->first() +``` + +```diff +- use GraphAware\Common\Result\RecordCursorInterface; ++ use Laudis\Neo4j\Types\CypherList; + +- RecordCursorInterface $result ++ CypherList $results +``` diff --git a/_algo-examples/cosine-similarity.php b/_algo-examples/cosine-similarity.php index d4bea8e..bfc1c34 100644 --- a/_algo-examples/cosine-similarity.php +++ b/_algo-examples/cosine-similarity.php @@ -2,57 +2,56 @@ require_once __DIR__.'/../vendor/autoload.php'; -use GraphAware\Reco4PHP\Persistence\DatabaseService; use GraphAware\Reco4PHP\Algorithms\Model\KNNModelBuilder; use GraphAware\Reco4PHP\Algorithms\Model\Rating; -use GraphAware\Reco4PHP\Common\ObjectSet; use GraphAware\Reco4PHP\Algorithms\Similarity\CosineSimilarity; +use GraphAware\Reco4PHP\Common\ObjectSet; +use GraphAware\Reco4PHP\Persistence\DatabaseService; use Symfony\Component\Stopwatch\Stopwatch; -$db = new DatabaseService("http://neo4j:error@localhost:7474"); +$db = new DatabaseService('http://neo4j:error@localhost:7474'); $driver = $db->getDriver(); -$knn = new KNNModelBuilder(null, new CosineSimilarity()); +$knn = new KNNModelBuilder(new CosineSimilarity()); $s = microtime(true); - -$qA = "MATCH (m:Movie) OPTIONAL MATCH (m)<-[r:RATED]-(u) -RETURN id(m) as m, collect({rating: r.rating, user: id(u)}) as ratings LIMIT 1500"; +$qA = 'MATCH (m:Movie) OPTIONAL MATCH (m)<-[r:RATED]-(u) +RETURN id(m) as m, collect({rating: r.rating, user: id(u)}) as ratings LIMIT 1500'; $stopwatch = new Stopwatch(); -$stopwatch->start("e"); +$stopwatch->start('e'); $result = $driver->run($qA); -$e = $stopwatch->stop("e"); -echo $e->getDuration() . PHP_EOL; +$e = $stopwatch->stop('e'); +echo $e->getDuration().PHP_EOL; -$stopwatch->start("simil"); +$stopwatch->start('simil'); $pairs = []; $crs = array_chunk($result->records(), 100); foreach ($result->records() as $record) { $source = new ObjectSet(Rating::class); - $m = $record->value("m"); - foreach ($record->value("ratings") as $rating) { + $m = $record->value('m'); + foreach ($record->value('ratings') as $rating) { $source->add(new Rating($rating['rating'], $rating['user'])); } foreach ($crs as $cr) { foreach ($cr as $record2) { - $m2 = $record2->value("m"); + $m2 = $record2->value('m'); $k = $m + $m2; - if (!array_key_exists($k, $pairs) && $record2->value("m") !== $record->value("m")) { + if (!array_key_exists($k, $pairs) && $record2->value('m') !== $record->value('m')) { $destination = new ObjectSet(Rating::class); - foreach ($record2->value("ratings") as $rating2) { + foreach ($record2->value('ratings') as $rating2) { $destination->add(new Rating($rating2['rating'], $rating2['user'])); } $simil = $knn->computeSimilarity($source, $destination); $pairs[$k] = [ - 'source' => $record->value("m"), - 'desc' => $record2->value("m"), - 'similarity' => $simil + 'source' => $record->value('m'), + 'desc' => $record2->value('m'), + 'similarity' => $simil, ]; - //echo $simil . PHP_EOL; + // echo $simil . PHP_EOL; } } } } -$e2 = $stopwatch->stop("simil"); -echo $e2->getDuration() . PHP_EOL; \ No newline at end of file +$e2 = $stopwatch->stop('simil'); +echo $e2->getDuration().PHP_EOL; diff --git a/_demo/Github/FollowedByFollowers.php b/_demo/Github/FollowedByFollowers.php new file mode 100644 index 0000000..706a43b --- /dev/null +++ b/_demo/Github/FollowedByFollowers.php @@ -0,0 +1,34 @@ +(reco) + WHERE size((follower)-[:FOLLOWS]->()) < $max_follows + RETURN reco, count(*) as score + LIMIT 100'; + + return Statement::create($query, ['id' => $input->getId(), 'max_follows' => 200]); + } + + public function buildScore(Node $input, Node $item, CypherMap $result, Context $context): SingleScore + { + return new SingleScore((float) $result->get('score')); + } +} diff --git a/_demo/github/PenalizeTooMuchFollowers.php b/_demo/Github/PenalizeTooMuchFollowers.php similarity index 62% rename from _demo/github/PenalizeTooMuchFollowers.php rename to _demo/Github/PenalizeTooMuchFollowers.php index d2a0629..f67eca2 100644 --- a/_demo/github/PenalizeTooMuchFollowers.php +++ b/_demo/Github/PenalizeTooMuchFollowers.php @@ -2,41 +2,37 @@ namespace GraphAware\Reco4PHP\Demo\Github; -use GraphAware\Common\Cypher\Statement; -use GraphAware\Common\Result\Record; -use GraphAware\Common\Type\Node; -use GraphAware\Common\Type\NodeInterface; use GraphAware\Reco4PHP\Post\RecommendationSetPostProcessor; use GraphAware\Reco4PHP\Result\Recommendation; use GraphAware\Reco4PHP\Result\Recommendations; -use GraphAware\Reco4PHP\Result\Score; use GraphAware\Reco4PHP\Result\SingleScore; +use Laudis\Neo4j\Databags\Statement; +use Laudis\Neo4j\Types\CypherMap; +use Laudis\Neo4j\Types\Node; class PenalizeTooMuchFollowers extends RecommendationSetPostProcessor { - public function name() + public function name(): string { return 'too_much_followers'; } - public function buildQuery(NodeInterface $input, Recommendations $recommendations) + public function buildQuery(Node $input, Recommendations $recommendations): Statement { $ids = []; foreach ($recommendations->getItems() as $recommendation) { - $ids[] = $recommendation->item()->identity(); + $ids[] = $recommendation->item()->getId(); } - $query = 'UNWIND {ids} as id + $query = 'UNWIND $ids as id MATCH (n) WHERE id(n) = id RETURN id, size((n)<-[:FOLLOWS]-()) as followersCount'; return Statement::create($query, ['ids' => $ids]); - } - public function postProcess(Node $input, Recommendation $recommendation, Record $record) + public function postProcess(Node $input, Recommendation $recommendation, CypherMap $result): void { - $recommendation->addScore($this->name(), new SingleScore(- $record->get('followersCount') / 50)); + $recommendation->addScore($this->name(), new SingleScore(-(int) $result->get('followersCount') / 50)); } - -} \ No newline at end of file +} diff --git a/_demo/Github/RecommendationEngine.php b/_demo/Github/RecommendationEngine.php new file mode 100644 index 0000000..75b1c8e --- /dev/null +++ b/_demo/Github/RecommendationEngine.php @@ -0,0 +1,28 @@ +(repo)<-[:CONTRIBUTED_TO]-(reco) + RETURN reco, count(*) as score'; + + return Statement::create($query, ['id' => $input->getId()]); + } + + public function buildScore(Node $input, Node $item, CypherMap $result, Context $context): SingleScore + { + return new SingleScore((float) $result->get('score') * 10); + } +} diff --git a/_demo/github/FollowedByFollowers.php b/_demo/github/FollowedByFollowers.php deleted file mode 100644 index ce40169..0000000 --- a/_demo/github/FollowedByFollowers.php +++ /dev/null @@ -1,34 +0,0 @@ -(reco) - WHERE size((follower)-[:FOLLOWS]->()) < {max_follows} - RETURN reco, count(*) as score - LIMIT 100'; - - return Statement::create($query, ['id' => $input->identity(), 'max_follows' => 200]); - } - - public function buildScore(NodeInterface $input, NodeInterface $item, RecordViewInterface $record) - { - return new SingleScore($record->get('score')); - } - - -} \ No newline at end of file diff --git a/_demo/github/RecommendationEngine.php b/_demo/github/RecommendationEngine.php deleted file mode 100644 index 45c832b..0000000 --- a/_demo/github/RecommendationEngine.php +++ /dev/null @@ -1,28 +0,0 @@ -(repo)<-[:CONTRIBUTED_TO]-(reco) - RETURN reco, count(*) as score'; - - return Statement::create($query, ['id' => $input->identity()]); - } - - public function buildScore(NodeInterface $input, NodeInterface $item, RecordViewInterface $record) - { - return new SingleScore($record->get('score') * 10); - } - - -} \ No newline at end of file diff --git a/build/install-jdk8.sh b/build/install-jdk8.sh deleted file mode 100755 index 8b08600..0000000 --- a/build/install-jdk8.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -# Get dependencies (for adding repos) -sudo apt-get install -y python-software-properties -sudo add-apt-repository -y ppa:webupd8team/java -sudo apt-get update - -# install oracle jdk 8 -sudo apt-get install -y oracle-java8-installer -sudo update-alternatives --auto java -sudo update-alternatives --auto javac \ No newline at end of file diff --git a/build/install-neo.sh b/build/install-neo.sh deleted file mode 100755 index 6cb81d5..0000000 --- a/build/install-neo.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -export JAVA_HOME=/usr/lib/jvm/java-8-oracle -export JRE_HOME=/usr/lib/jvm/java-8-oracle - -wget http://dist.neo4j.org/neo4j-enterprise-3.0.2-unix.tar.gz > null -mkdir neo -tar xzf neo4j-enterprise-3.0.2-unix.tar.gz -C neo --strip-components=1 > null -sed -i.bak '/\(dbms\.security\.auth_enabled=\).*/s/^#//g' ./neo/conf/neo4j.conf -neo/bin/neo4j start > null & \ No newline at end of file diff --git a/composer.json b/composer.json index 9948c16..31d34ba 100644 --- a/composer.json +++ b/composer.json @@ -10,14 +10,12 @@ } ], "require": { - "php": "^7.0", - "graphaware/neo4j-php-client": "^4.0", - "symfony/event-dispatcher": "^2.7 || ^3.0 || ^4.0", - "psr/log": "^1.0", - "symfony/stopwatch": "^2.7 || ^3.0 || ^4.0" + "php": "^8.0", + "laudis/neo4j-php-client": "^2.7", + "symfony/stopwatch": "^4.4 || ^5.4 || ^6.0" }, "require-dev": { - "phpunit/phpunit": "^5.1" + "phpunit/phpunit": "^9.5" }, "autoload": { "psr-4": { diff --git a/example.php b/example.php index 4cda787..9064935 100644 --- a/example.php +++ b/example.php @@ -2,28 +2,30 @@ require_once __DIR__.'/vendor/autoload.php'; +use GraphAware\Reco4PHP\Context\SimpleContext; use GraphAware\Reco4PHP\Demo\Github\RecommendationEngine; use GraphAware\Reco4PHP\RecommenderService; +use Symfony\Component\Stopwatch\Stopwatch; -$rs = RecommenderService::create("http://localhost:7474"); +$rs = RecommenderService::create('bolt://localhost:7687'); $rs->registerRecommendationEngine(new RecommendationEngine()); -$stopwatch = new \Symfony\Component\Stopwatch\Stopwatch(); +$stopwatch = new Stopwatch(); $input = $rs->findInputBy('User', 'login', 'jakzal'); -$engine = $rs->getRecommender("github_who_to_follow"); +$engine = $rs->getRecommender('github_who_to_follow'); $stopwatch->start('reco'); -$recommendations = $engine->recommend($input); +$recommendations = $engine->recommend($input, new SimpleContext()); $e = $stopwatch->stop('reco'); -//echo $recommendations->size() . ' found in ' . $e->getDuration() . 'ms' .PHP_EOL; +// echo $recommendations->size() . ' found in ' . $e->getDuration() . 'ms' .PHP_EOL; foreach ($recommendations->getItems(10) as $reco) { - echo $reco->item()->get('login') . PHP_EOL; - echo $reco->totalScore() . PHP_EOL; + echo $reco->item()->getProperty('login').PHP_EOL; + echo $reco->totalScore().PHP_EOL; foreach ($reco->getScores() as $name => $score) { - echo "\t" . $name . ':' . $score->score() . PHP_EOL; + echo "\t".$name.':'.$score->getScore().PHP_EOL; } -} \ No newline at end of file +} diff --git a/phpunit.xml b/phpunit.xml index 65a1fb8..dc67867 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -5,11 +5,11 @@ ./tests - - + + tests vendor bin - - + + \ No newline at end of file diff --git a/src/Algorithms/Model/KNNModelBuilder.php b/src/Algorithms/Model/KNNModelBuilder.php index f64b790..42b32a1 100644 --- a/src/Algorithms/Model/KNNModelBuilder.php +++ b/src/Algorithms/Model/KNNModelBuilder.php @@ -16,27 +16,21 @@ class KNNModelBuilder { - protected $model; + protected Similarity $similarityFunction; - protected $similarityFunction; - - protected $dataset; - - public function __construct($model = null, Similarity $similarityFunction = null, $dataset = null) + public function __construct(Similarity $similarityFunction) { - $this->model = $model; $this->similarityFunction = $similarityFunction; - $this->dataset = $dataset; } - public function computeSimilarity(ObjectSet $tfSource, ObjectSet $tfDestination) + public function computeSimilarity(ObjectSet $tfSource, ObjectSet $tfDestination): float { $vectors = $this->createVectors($tfSource, $tfDestination); return $this->similarityFunction->getSimilarity($vectors[0], $vectors[1]); } - public function createVectors(ObjectSet $tfSource, ObjectSet $tfDestination) + public function createVectors(ObjectSet $tfSource, ObjectSet $tfDestination): array { $ratings = []; foreach ($tfSource->getAll() as $source) { @@ -58,6 +52,6 @@ public function createVectors(ObjectSet $tfSource, ObjectSet $tfDestination) $yVector[] = array_key_exists(1, $ratings[$k]) ? $ratings[$k][1] : 0; } - return array($xVector, $yVector); + return [$xVector, $yVector]; } } diff --git a/src/Algorithms/Model/Rating.php b/src/Algorithms/Model/Rating.php index 7f271bc..b536230 100644 --- a/src/Algorithms/Model/Rating.php +++ b/src/Algorithms/Model/Rating.php @@ -13,34 +13,22 @@ class Rating { - /** - * @var float - */ - protected $rating; + protected float $rating; - /** - * @var int - */ - protected $userNodeId; + protected int $userNodeId; - public function __construct($rating, $userNodeId) + public function __construct(float $rating, int $userNodeId) { $this->rating = (float) $rating; $this->userNodeId = (int) $userNodeId; } - /** - * @return float - */ - public function getRating() + public function getRating(): float { return $this->rating; } - /** - * @return int - */ - public function getId() + public function getId(): int { return $this->userNodeId; } diff --git a/src/Algorithms/Similarity/CosineSimilarity.php b/src/Algorithms/Similarity/CosineSimilarity.php index 0efb82e..1ee5603 100644 --- a/src/Algorithms/Similarity/CosineSimilarity.php +++ b/src/Algorithms/Similarity/CosineSimilarity.php @@ -13,7 +13,7 @@ class CosineSimilarity implements Similarity { - public function getSimilarity(array $xVector, array $yVector) + public function getSimilarity(array $xVector, array $yVector): float { $a = $this->getDotProduct($xVector, $yVector); $b = $this->getNorm($xVector) * $this->getNorm($yVector); @@ -25,7 +25,7 @@ public function getSimilarity(array $xVector, array $yVector) return 0; } - private function getDotProduct(array $xVector, array $yVector) + private function getDotProduct(array $xVector, array $yVector): float { $sum = 0.0; foreach ($xVector as $k => $v) { @@ -35,7 +35,7 @@ private function getDotProduct(array $xVector, array $yVector) return $sum; } - private function getNorm(array $vector) + private function getNorm(array $vector): float { $sum = 0.0; foreach ($vector as $k => $v) { diff --git a/src/Algorithms/Similarity/Similarity.php b/src/Algorithms/Similarity/Similarity.php index 1439e60..a40c7a4 100644 --- a/src/Algorithms/Similarity/Similarity.php +++ b/src/Algorithms/Similarity/Similarity.php @@ -13,5 +13,5 @@ interface Similarity { - public function getSimilarity(array $xVector, array $yVector); + public function getSimilarity(array $xVector, array $yVector): float; } diff --git a/src/Common/NodeSet.php b/src/Common/NodeSet.php deleted file mode 100644 index 20f4c32..0000000 --- a/src/Common/NodeSet.php +++ /dev/null @@ -1,63 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace GraphAware\Reco4PHP\Common; - -use GraphAware\Common\Type\Node; - -class NodeSet extends ObjectSet -{ - /** - * @param \GraphAware\Common\Type\Node $node - */ - public function add(Node $node) - { - if (parent::valid($node) && !$this->contains($node)) { - $this->elements[$node->identity()] = $node; - } - } - - /** - * @param $key - * - * @return \GraphAware\Common\Type\Node - */ - public function get($key) - { - return array_values($this->elements)[$key]; - } - - /** - * @return \GraphAware\Common\Type\Node[] - */ - public function all() - { - return $this->elements; - } - - /** - * @return int - */ - public function size() - { - return count($this->elements); - } - - /** - * @param \GraphAware\Common\Type\Node $node - * - * @return bool - */ - public function contains(Node $node) - { - return array_key_exists($node->identity(), $this->elements); - } -} diff --git a/src/Common/ObjectSet.php b/src/Common/ObjectSet.php index 5b42304..01e2ea3 100644 --- a/src/Common/ObjectSet.php +++ b/src/Common/ObjectSet.php @@ -13,17 +13,11 @@ class ObjectSet implements Set { - /** - * @var string - */ - protected $className; + protected string $className; - /** - * @var array - */ - protected $elements = []; + protected array $elements = []; - final public function __construct($className) + final public function __construct(string $className) { if (!class_exists($className)) { throw new \InvalidArgumentException(sprintf('The classname %s does not exist', $className)); @@ -32,7 +26,7 @@ final public function __construct($className) $this->className = $className; } - public function add($element) + public function add(object $element): void { if ($this->valid($element) && !in_array($element, $this->elements)) { $this->elements[] = $element; @@ -42,27 +36,25 @@ public function add($element) /** * @return object[] */ - public function getAll() + public function getAll(): array { return $this->elements; } - public function size() + public function size(): int { return count($this->elements); } - public function get($key) + public function get($key): ?object { return $this->elements[$key]; } /** - * @param object $element - * * @return bool */ - protected function valid($element) + protected function valid(object $element) { return $element instanceof $this->className; } diff --git a/src/Config/Config.php b/src/Config/Config.php index f8ca3ce..91f29bf 100644 --- a/src/Config/Config.php +++ b/src/Config/Config.php @@ -13,16 +13,16 @@ interface Config { - const UNLIMITED = PHP_INT_MAX; + public const UNLIMITED = PHP_INT_MAX; /** * @return int maximum number of items to recommend */ - public function limit(); + public function limit(): int; /** * @return int maximum number of ms the recommendation-computing process should take. Note that it is * for information only, it is the responsibility of the engines to honour this configuration or not. */ - public function maxTime(); + public function maxTime(): int; } diff --git a/src/Config/KeyValueConfig.php b/src/Config/KeyValueConfig.php index 26e0633..6b8d48c 100644 --- a/src/Config/KeyValueConfig.php +++ b/src/Config/KeyValueConfig.php @@ -13,46 +13,24 @@ abstract class KeyValueConfig implements Config { - /** - * @var array - */ - protected $values = []; + protected array $values = []; - /** - * @param $key - * - * @return mixed - */ - public function get($key) + public function get(string $key): mixed { return array_key_exists($key, $this->values) ? $this->values[$key] : null; } - /** - * @param string $key - * @param mixed $value - */ - public function add($key, $value) + public function add(string $key, mixed $value): void { $this->values[$key] = $value; } - /** - * @param string $key - * - * @return bool - */ - public function containsKey($key) + public function containsKey(string $key): bool { return array_key_exists($key, $this->values); } - /** - * @param mixed $o - * - * @return bool - */ - public function contains($o) + public function contains(mixed $o): bool { return in_array($o, $this->values); } diff --git a/src/Config/SimpleConfig.php b/src/Config/SimpleConfig.php index 0d9c0a1..aeb953c 100644 --- a/src/Config/SimpleConfig.php +++ b/src/Config/SimpleConfig.php @@ -13,30 +13,20 @@ class SimpleConfig extends KeyValueConfig { - /** - * @var int - */ - protected $limit; + protected int $limit; - /** - * @var int - */ - protected $maxTime; + protected int $maxTime; - /** - * @param int|null $limit - * @param int|null $maxTime - */ - public function __construct($limit = null, $maxTime = null) + public function __construct(?int $limit = null, ?int $maxTime = null) { - $this->limit = null !== $limit ? $limit : self::UNLIMITED; - $this->maxTime = null !== $maxTime ? $maxTime : self::UNLIMITED; + $this->limit = $limit ?? self::UNLIMITED; + $this->maxTime = $maxTime ?? self::UNLIMITED; } /** * {@inheritdoc} */ - public function limit() + public function limit(): int { return $this->limit; } @@ -44,7 +34,7 @@ public function limit() /** * {@inheritdoc} */ - public function maxTime() + public function maxTime(): int { return $this->maxTime; } diff --git a/src/Context/Context.php b/src/Context/Context.php index 082990e..6c68261 100644 --- a/src/Context/Context.php +++ b/src/Context/Context.php @@ -15,18 +15,12 @@ interface Context { - /** - * @return \GraphAware\Reco4PHP\Config\Config - */ - public function config() : Config; + public function config(): Config; - /** - * @return bool - */ - public function timeLeft() : bool; + public function timeLeft(): bool; /** * @return \GraphAware\Reco4PHP\Context\Statistics */ - public function getStatistics() : Statistics; + public function getStatistics(): Statistics; } diff --git a/src/Context/SimpleContext.php b/src/Context/SimpleContext.php index 47c07bb..557256f 100644 --- a/src/Context/SimpleContext.php +++ b/src/Context/SimpleContext.php @@ -11,35 +11,25 @@ namespace GraphAware\Reco4PHP\Context; -use GraphAware\Common\Type\Node; use GraphAware\Reco4PHP\Config\Config; use GraphAware\Reco4PHP\Config\SimpleConfig; class SimpleContext implements Context { - /** - * @var \GraphAware\Reco4PHP\Config\Config - */ - protected $config; + protected Config $config; - /** - * @var \GraphAware\Reco4PHP\Context\Statistics - */ - protected $statistics; + protected Statistics $statistics; - /** - * @param \GraphAware\Reco4PHP\Config\Config $config - */ - public function __construct(Config $config = null) + public function __construct(?Config $config = null) { - $this->config = null !== $config ? $config : new SimpleConfig(); + $this->config = $config ?? new SimpleConfig(); $this->statistics = new Statistics(); } /** * {@inheritdoc} */ - public function config() : Config + public function config(): Config { return $this->config; } @@ -47,12 +37,15 @@ public function config() : Config /** * {@inheritdoc} */ - public function timeLeft() : bool + public function timeLeft(): bool { return $this->statistics->getCurrentTimeSpent() < $this->config()->limit(); } - public function getStatistics() : Statistics + /** + * {@inheritdoc} + */ + public function getStatistics(): Statistics { return $this->statistics; } diff --git a/src/Context/Statistics.php b/src/Context/Statistics.php index 84c3001..55396b0 100644 --- a/src/Context/Statistics.php +++ b/src/Context/Statistics.php @@ -16,71 +16,56 @@ class Statistics { private static $DISCOVERY_KEY = 'discovery'; - private static $POST_PROCESS_KEY = 'post_process'; + private static $TOTAL_KEY = 'total'; - /** - * @var \Symfony\Component\Stopwatch\Stopwatch - */ - protected $stopwatch; + protected Stopwatch $stopwatch; - /** - * @var float - */ - protected $discoveryTime; + protected float $discoveryTime; - /** - * @var float - */ - protected $postProcessingTime; + protected float $postProcessingTime; public function __construct() { $this->stopwatch = new Stopwatch(); } - public function startDiscovery() + public function startDiscovery(): void { $this->stopwatch->start(self::$DISCOVERY_KEY); } - public function stopDiscovery() + public function stopDiscovery(): void { $e = $this->stopwatch->stop(self::$DISCOVERY_KEY); $this->discoveryTime = $e->getDuration(); } - public function startPostProcess() + public function startPostProcess(): void { $this->stopwatch->start(self::$POST_PROCESS_KEY); } - public function stopPostProcess() + public function stopPostProcess(): void { $e = $this->stopwatch->stop(self::$POST_PROCESS_KEY); $this->postProcessingTime = $e->getDuration(); } - /** - * @return array - */ - public function getTimes() + public function getTimes(): array { - return array( + return [ self::$DISCOVERY_KEY => $this->discoveryTime, self::$POST_PROCESS_KEY => $this->postProcessingTime, - 'total' => $this->discoveryTime + $this->postProcessingTime - ); + self::$TOTAL_KEY => $this->discoveryTime + $this->postProcessingTime, + ]; } - /** - * @return float - */ - public function getCurrentTimeSpent() + public function getCurrentTimeSpent(): float { $discovery = null !== $this->discoveryTime ? $this->discoveryTime : 0.0; $postProcess = null !== $this->postProcessingTime ? $this->postProcessingTime : 0.0; return $discovery + $postProcess; } -} \ No newline at end of file +} diff --git a/src/Cypher/Query.php b/src/Cypher/Query.php deleted file mode 100644 index 3080e49..0000000 --- a/src/Cypher/Query.php +++ /dev/null @@ -1,9 +0,0 @@ -discoveryEngines(), function (DiscoveryEngine $discoveryEngine) { return true; @@ -86,9 +71,9 @@ final public function getDiscoveryEngines() : array } /** - * @return \GraphAware\Reco4PHP\Filter\BlackListBuilder[] + * {@inheritdoc} */ - final public function getBlacklistBuilders() : array + final public function getBlacklistBuilders(): array { return array_filter($this->blacklistBuilders(), function (BlackListBuilder $blackListBuilder) { return true; @@ -96,9 +81,9 @@ final public function getBlacklistBuilders() : array } /** - * @return \GraphAware\Reco4PHP\Filter\Filter[] + * {@inheritdoc} */ - final public function getFilters() : array + final public function getFilters(): array { return array_filter($this->filters(), function (Filter $filter) { return true; @@ -106,9 +91,9 @@ final public function getFilters() : array } /** - * @return \GraphAware\Reco4PHP\Post\PostProcessor[] + * {@inheritdoc} */ - final public function getPostProcessors() : array + final public function getPostProcessors(): array { return array_filter($this->postProcessors(), function (PostProcessor $postProcessor) { return true; @@ -116,28 +101,16 @@ final public function getPostProcessors() : array } /** - * @return array|\Psr\Log\LoggerInterface[] + * {@inheritdoc} */ - final public function getLoggers() : array + final public function recommend(Node $input, Context $context): Recommendations { - return array_filter($this->loggers(), function (LoggerInterface $logger) { - return true; - }); + return $this->recommendationExecutor->processRecommendation($input, $this, $context); } /** - * @param Node $input - * @param Context $context - * - * @return \GraphAware\Reco4PHP\Result\Recommendations + * {@inheritdoc} */ - final public function recommend(Node $input, Context $context) : Recommendations - { - $recommendations = $this->recommendationExecutor->processRecommendation($input, $this, $context); - - return $recommendations; - } - final public function setDatabaseService(DatabaseService $databaseService) { $this->databaseService = $databaseService; diff --git a/src/Engine/DiscoveryEngine.php b/src/Engine/DiscoveryEngine.php index 991ab9c..7b95d63 100644 --- a/src/Engine/DiscoveryEngine.php +++ b/src/Engine/DiscoveryEngine.php @@ -11,58 +11,42 @@ namespace GraphAware\Reco4PHP\Engine; -use GraphAware\Common\Cypher\StatementInterface; -use GraphAware\Common\Result\Record; -use GraphAware\Common\Type\Node; -use GraphAware\Common\Result\ResultCollection; use GraphAware\Reco4PHP\Context\Context; use GraphAware\Reco4PHP\Result\Recommendations; +use GraphAware\Reco4PHP\Result\ResultCollection; use GraphAware\Reco4PHP\Result\SingleScore; +use Laudis\Neo4j\Databags\Statement; +use Laudis\Neo4j\Types\CypherMap; +use Laudis\Neo4j\Types\Node; interface DiscoveryEngine { /** * @return string The name of the discovery engine */ - public function name() : string; + public function name(): string; /** * The statement to be executed for finding items to be recommended. - * - * @param Node $input - * @param Context $context - * - * @return \GraphAware\Common\Cypher\Statement */ - public function discoveryQuery(Node $input, Context $context) : StatementInterface; + public function discoveryQuery(Node $input, Context $context): Statement; /** * Returns the score produced by the recommended item. * - * @param Node $input - * @param Node $item - * @param Record $record - * @param Context $context - * - * @return \GraphAware\Reco4PHP\Result\SingleScore A single score produced for the recommended item + * @return SingleScore A single score produced for the recommended item */ - public function buildScore(Node $input, Node $item, Record $record, Context $context) : SingleScore; + public function buildScore(Node $input, Node $item, CypherMap $result, Context $context): SingleScore; /** * Returns a collection of Recommendation object produced by this discovery engine. - * - * @param Node $input - * @param ResultCollection $resultCollection - * @param Context $context - * - * @return Recommendations */ - public function produceRecommendations(Node $input, ResultCollection $resultCollection, Context $context) : Recommendations; + public function produceRecommendations(Node $input, ResultCollection $resultCollection, Context $context): Recommendations; /** * @return string The column identifier of the row result representing the recommended item (node) */ - public function recoResultName() : string; + public function recoResultName(): string; /** * @return string The column identifier of the row result representing the score to be used, note that this @@ -70,10 +54,10 @@ public function recoResultName() : string; * defaultScore() or the score logic if the concrete class override the buildScore * method. */ - public function scoreResultName() : string; + public function scoreResultName(): string; /** * @return float The default score to be given to the discovered recommended item */ - public function defaultScore() : float; + public function defaultScore(): float; } diff --git a/src/Engine/RecommendationEngine.php b/src/Engine/RecommendationEngine.php index da6df8f..2893cd0 100644 --- a/src/Engine/RecommendationEngine.php +++ b/src/Engine/RecommendationEngine.php @@ -11,77 +11,59 @@ namespace GraphAware\Reco4PHP\Engine; -use GraphAware\Common\Type\Node; use GraphAware\Reco4PHP\Context\Context; +use GraphAware\Reco4PHP\Filter\BlackListBuilder; +use GraphAware\Reco4PHP\Filter\Filter; use GraphAware\Reco4PHP\Persistence\DatabaseService; +use GraphAware\Reco4PHP\Post\PostProcessor; +use GraphAware\Reco4PHP\Result\Recommendations; +use Laudis\Neo4j\Types\Node; interface RecommendationEngine { - /** - * @return string - */ - public function name() : string; - - /** - * @return \GraphAware\Reco4PHP\Engine\DiscoveryEngine[] - */ - public function discoveryEngines() : array; - - /** - * @return \GraphAware\Reco4PHP\Filter\BlackListBuilder[] - */ - public function blacklistBuilders() : array; + public function name(): string; /** - * @return \GraphAware\Reco4PHP\Post\PostProcessor[] + * @return DiscoveryEngine[] */ - public function postProcessors() : array; + public function discoveryEngines(): array; /** - * @return \GraphAware\Reco4PHP\Filter\Filter[] + * @return BlackListBuilder[] */ - public function filters() : array; + public function blacklistBuilders(): array; /** - * @return \Psr\Log\LoggerInterface[] + * @return PostProcessor[] */ - public function loggers() : array; + public function postProcessors(): array; /** - * @return \GraphAware\Reco4PHP\Engine\DiscoveryEngine[] + * @return Filter[] */ - public function getDiscoveryEngines() : array; + public function filters(): array; /** - * @return \GraphAware\Reco4PHP\Filter\BlackListBuilder[] + * @return DiscoveryEngine[] */ - public function getBlacklistBuilders() : array; + public function getDiscoveryEngines(): array; /** - * @return \GraphAware\Reco4PHP\Filter\Filter[] + * @return BlackListBuilder[] */ - public function getFilters() : array; + public function getBlacklistBuilders(): array; /** - * @return \GraphAware\Reco4PHP\Post\PostProcessor[] + * @return Filter[] */ - public function getPostProcessors() : array; + public function getFilters(): array; /** - * @return \Psr\Log\LoggerInterface[] + * @return PostProcessor[] */ - public function getLoggers() : array; + public function getPostProcessors(): array; - /** - * @param Node $input - * @param Context $context - * - * @return \GraphAware\Reco4PHP\Result\Recommendations - */ - public function recommend(Node $input, Context $context); + public function recommend(Node $input, Context $context): Recommendations; - /** - * @param \GraphAware\Reco4PHP\Persistence\DatabaseService $databaseService - */ public function setDatabaseService(DatabaseService $databaseService); } diff --git a/src/Engine/SingleDiscoveryEngine.php b/src/Engine/SingleDiscoveryEngine.php index 7d74d6c..0a002f0 100644 --- a/src/Engine/SingleDiscoveryEngine.php +++ b/src/Engine/SingleDiscoveryEngine.php @@ -1,6 +1,6 @@ hasValue($this->scoreResultName()) ? $record->value($this->scoreResultName()) : $this->defaultScore(); - $reason = $record->hasValue($this->reasonResultName()) ? $record->value($this->reasonResultName()) : null; + $score = $result->hasKey($this->scoreResultName()) ? $result->get($this->scoreResultName()) : $this->defaultScore(); + $reason = $result->hasKey($this->reasonResultName()) ? $result->get($this->reasonResultName()) : null; return new SingleScore($score, $reason); } /** * {@inheritdoc} - * - * @param Node $input - * @param ResultCollection $resultCollection - * @param Context $context - * - * @return \GraphAware\Reco4PHP\Result\Recommendations */ - final public function produceRecommendations(Node $input, ResultCollection $resultCollection, Context $context) : Recommendations + final public function produceRecommendations(Node $input, ResultCollection $resultCollection, Context $context): Recommendations { - $result = $resultCollection->get($this->name()); + $results = $resultCollection->get($this->name()); $recommendations = new Recommendations($context); - foreach ($result->records() as $record) { - if ($record->hasValue($this->recoResultName())) { - $recommendations->add($record->get($this->recoResultName()), $this->name(), $this->buildScore($input, $record->get($this->recoResultName()), $record, $context)); + /** @var CypherMap $result */ + foreach ($results as $result) { + if ($result->hasKey($this->recoResultName())) { + /** @var Node $node */ + $node = $result->get($this->recoResultName()); + $recommendations->add($node, $this->name(), $this->buildScore($input, $node, $result, $context)); } } @@ -70,7 +60,7 @@ final public function produceRecommendations(Node $input, ResultCollection $resu /** * {@inheritdoc} */ - public function recoResultName() : string + public function recoResultName(): string { return self::$DEFAULT_RECO_NAME; } @@ -78,7 +68,7 @@ public function recoResultName() : string /** * {@inheritdoc} */ - public function scoreResultName() : string + public function scoreResultName(): string { return self::$DEFAULT_SCORE_NAME; } @@ -86,7 +76,7 @@ public function scoreResultName() : string /** * {@inheritdoc} */ - public function reasonResultName() : string + public function reasonResultName(): string { return self::$DEFAULT_REASON_NAME; } @@ -94,7 +84,7 @@ public function reasonResultName() : string /** * {@inheritdoc} */ - public function defaultScore() : float + public function defaultScore(): float { return 1.0; } diff --git a/src/Executor/DiscoveryPhaseExecutor.php b/src/Executor/DiscoveryPhaseExecutor.php index 625f2b8..8ecb82d 100644 --- a/src/Executor/DiscoveryPhaseExecutor.php +++ b/src/Executor/DiscoveryPhaseExecutor.php @@ -11,21 +11,19 @@ namespace GraphAware\Reco4PHP\Executor; -use GraphAware\Common\Type\Node; use GraphAware\Reco4PHP\Context\Context; +use GraphAware\Reco4PHP\Engine\DiscoveryEngine; +use GraphAware\Reco4PHP\Filter\BlackListBuilder; use GraphAware\Reco4PHP\Persistence\DatabaseService; +use GraphAware\Reco4PHP\Result\ResultCollection; +use Laudis\Neo4j\Types\Node; class DiscoveryPhaseExecutor { - /** - * @var \GraphAware\Reco4PHP\Persistence\DatabaseService - */ - private $databaseService; + private DatabaseService $databaseService; /** * DiscoveryPhaseExecutor constructor. - * - * @param \GraphAware\Reco4PHP\Persistence\DatabaseService $databaseService */ public function __construct(DatabaseService $databaseService) { @@ -33,28 +31,28 @@ public function __construct(DatabaseService $databaseService) } /** - * @param Node $input - * @param \GraphAware\Reco4PHP\Engine\DiscoveryEngine[] $engines - * @param \GraphAware\Reco4PHP\Filter\BlackListBuilder[] $blacklists - * @param Context $context - * - * @return \GraphAware\Common\Result\ResultCollection + * @param DiscoveryEngine[] $engines + * @param BlackListBuilder[] $blacklists */ - public function processDiscovery(Node $input, array $engines, array $blacklists, Context $context) + public function processDiscovery(Node $input, array $engines, array $blacklists, Context $context): ResultCollection { - $stack = $this->databaseService->getDriver()->stack(); - foreach ($engines as $engine) { - $statement = $engine->discoveryQuery($input, $context); - $stack->push($statement->text(), $statement->parameters(), $engine->name()); + $statements = []; + $tags = []; + foreach (array_values($engines) as $engine) { + $statements[] = $engine->discoveryQuery($input, $context); + $tags[] = $engine->name(); } - foreach ($blacklists as $blacklist) { - $statement = $blacklist->blacklistQuery($input); - $stack->push($statement->text(), $statement->parameters(), $blacklist->name()); + foreach (array_values($blacklists) as $blacklist) { + $statements[] = $blacklist->blacklistQuery($input); + $tags[] = $blacklist->name(); } try { - $resultCollection = $this->databaseService->getDriver()->runStack($stack); + $resultCollection = new ResultCollection(); + foreach ($this->databaseService->getDriver()->runStatements($statements) as $key => $value) { + $resultCollection->add($value, $tags[$key]); + } return $resultCollection; } catch (\Exception $e) { diff --git a/src/Executor/PostProcessPhaseExecutor.php b/src/Executor/PostProcessPhaseExecutor.php index 95b7d3d..3f37df5 100644 --- a/src/Executor/PostProcessPhaseExecutor.php +++ b/src/Executor/PostProcessPhaseExecutor.php @@ -11,58 +11,50 @@ namespace GraphAware\Reco4PHP\Executor; -use GraphAware\Common\Type\Node; use GraphAware\Reco4PHP\Engine\RecommendationEngine; use GraphAware\Reco4PHP\Persistence\DatabaseService; use GraphAware\Reco4PHP\Post\CypherAwarePostProcessor; use GraphAware\Reco4PHP\Post\RecommendationSetPostProcessor; use GraphAware\Reco4PHP\Result\Recommendations; +use GraphAware\Reco4PHP\Result\ResultCollection; +use Laudis\Neo4j\Types\Node; class PostProcessPhaseExecutor { - /** - * @var \GraphAware\Reco4PHP\Persistence\DatabaseService - */ - protected $databaseService; + protected DatabaseService $databaseService; /** * PostProcessPhaseExecutor constructor. - * - * @param \GraphAware\Reco4PHP\Persistence\DatabaseService $databaseService */ public function __construct(DatabaseService $databaseService) { $this->databaseService = $databaseService; } - /** - * @param \GraphAware\Common\Type\Node $input - * @param \GraphAware\Reco4PHP\Result\Recommendations $recommendations - * @param \GraphAware\Reco4PHP\Engine\RecommendationEngine $recommendationEngine - * - * @return \GraphAware\Common\Result\ResultCollection - */ - public function execute(Node $input, Recommendations $recommendations, RecommendationEngine $recommendationEngine) + public function execute(Node $input, Recommendations $recommendations, RecommendationEngine $recommendationEngine): ResultCollection { - $stack = $this->databaseService->getDriver()->stack('post_process_'.$recommendationEngine->name()); + $statements = []; + $tags = []; foreach ($recommendationEngine->getPostProcessors() as $postProcessor) { if ($postProcessor instanceof CypherAwarePostProcessor) { foreach ($recommendations->getItems() as $recommendation) { - $tag = sprintf('post_process_%s_%d', $postProcessor->name(), $recommendation->item()->identity()); - $statement = $postProcessor->buildQuery($input, $recommendation); - $stack->push($statement->text(), $statement->parameters(), $tag); + $tags[] = sprintf('post_process_%s_%d', $postProcessor->name(), $recommendation->item()->identity()); + $statements[] = $postProcessor->buildQuery($input, $recommendation); } } elseif ($postProcessor instanceof RecommendationSetPostProcessor) { - $statement = $postProcessor->buildQuery($input, $recommendations); - $stack->push($statement->text(), $statement->parameters(), $postProcessor->name()); + $statements[] = $postProcessor->buildQuery($input, $recommendations); + $tags[] = $postProcessor->name(); } } try { - $results = $this->databaseService->getDriver()->runStack($stack); + $resultCollection = new ResultCollection(); + foreach ($this->databaseService->getDriver()->runStatements($statements) as $key => $value) { + $resultCollection->add($value, $tags[$key]); + } - return $results; + return $resultCollection; } catch (\Exception $e) { throw new \RuntimeException('PostProcess Query Exception - '.$e->getMessage()); } diff --git a/src/Executor/RecommendationExecutor.php b/src/Executor/RecommendationExecutor.php index c90dbd2..0abdcbb 100644 --- a/src/Executor/RecommendationExecutor.php +++ b/src/Executor/RecommendationExecutor.php @@ -11,34 +11,28 @@ namespace GraphAware\Reco4PHP\Executor; -use GraphAware\Common\Result\ResultCollection; -use GraphAware\Common\Type\Node; use GraphAware\Reco4PHP\Context\Context; +use GraphAware\Reco4PHP\Engine\RecommendationEngine; use GraphAware\Reco4PHP\Persistence\DatabaseService; +use GraphAware\Reco4PHP\Post\RecommendationSetPostProcessor; use GraphAware\Reco4PHP\Result\Recommendations; -use GraphAware\Reco4PHP\Engine\RecommendationEngine; -use Symfony\Component\Stopwatch\Stopwatch; +use GraphAware\Reco4PHP\Result\ResultCollection; +use Laudis\Neo4j\Types\CypherMap; +use Laudis\Neo4j\Types\Node; class RecommendationExecutor { - /** - * @var \GraphAware\Reco4PHP\Executor\DiscoveryPhaseExecutor - */ - protected $discoveryExecutor; + protected DiscoveryPhaseExecutor $discoveryExecutor; - /** - * @var \GraphAware\Reco4PHP\Executor\PostProcessPhaseExecutor - */ - protected $postProcessExecutor; + protected PostProcessPhaseExecutor $postProcessExecutor; public function __construct(DatabaseService $databaseService) { $this->discoveryExecutor = new DiscoveryPhaseExecutor($databaseService); $this->postProcessExecutor = new PostProcessPhaseExecutor($databaseService); - $this->stopwatch = new Stopwatch(); } - public function processRecommendation(Node $input, RecommendationEngine $engine, Context $context) + public function processRecommendation(Node $input, RecommendationEngine $engine, Context $context): Recommendations { $recommendations = $this->doDiscovery($input, $engine, $context); $this->doPostProcess($input, $recommendations, $engine); @@ -47,7 +41,7 @@ public function processRecommendation(Node $input, RecommendationEngine $engine, return $recommendations; } - private function doDiscovery(Node $input, RecommendationEngine $engine, Context $context) + private function doDiscovery(Node $input, RecommendationEngine $engine, Context $context): Recommendations { $recommendations = new Recommendations($context); $context->getStatistics()->startDiscovery(); @@ -69,22 +63,24 @@ private function doDiscovery(Node $input, RecommendationEngine $engine, Context return $recommendations; } - private function doPostProcess(Node $input, Recommendations $recommendations, RecommendationEngine $engine) + private function doPostProcess(Node $input, Recommendations $recommendations, RecommendationEngine $engine): void { $recommendations->getContext()->getStatistics()->startPostProcess(); $postProcessResult = $this->postProcessExecutor->execute($input, $recommendations, $engine); foreach ($engine->getPostProcessors() as $postProcessor) { $tag = $postProcessor->name(); - $result = $postProcessResult->get($tag); - $postProcessor->handleResultSet($input, $result, $recommendations); + $results = $postProcessResult->get($tag); + if ($postProcessor instanceof RecommendationSetPostProcessor) { + $postProcessor->handleResultSet($input, $results, $recommendations); + } } $recommendations->getContext()->getStatistics()->stopPostProcess(); } - private function removeIrrelevant(Node $input, RecommendationEngine $engine, Recommendations $recommendations, array $blacklist) + private function removeIrrelevant(Node $input, RecommendationEngine $engine, Recommendations $recommendations, array $blacklist): void { foreach ($recommendations->getItems() as $recommendation) { - if (array_key_exists($recommendation->item()->identity(), $blacklist)) { + if (array_key_exists($recommendation->item()->getId(), $blacklist)) { $recommendations->remove($recommendation); } else { foreach ($engine->filters() as $filter) { @@ -97,16 +93,17 @@ private function removeIrrelevant(Node $input, RecommendationEngine $engine, Rec } } - private function buildBlacklistedNodes(ResultCollection $result, RecommendationEngine $engine) + private function buildBlacklistedNodes(ResultCollection $resultCollection, RecommendationEngine $engine): array { $set = []; foreach ($engine->getBlacklistBuilders() as $blacklist) { - $res = $result->get($blacklist->name()); - foreach ($res->records() as $record) { - if ($record->hasValue($blacklist->itemResultName())) { - $node = $record->get($blacklist->itemResultName()); + $results = $resultCollection->get($blacklist->name()); + /** @var CypherMap $result */ + foreach ($results as $result) { + if ($result->hasKey($blacklist->itemResultName())) { + $node = $result->get($blacklist->itemResultName()); if ($node instanceof Node) { - $set[$node->identity()] = $node; + $set[$node->getId()] = $node; } } } diff --git a/src/Filter/BaseBlacklistBuilder.php b/src/Filter/BaseBlacklistBuilder.php index d1d1fab..f7cd4dd 100644 --- a/src/Filter/BaseBlacklistBuilder.php +++ b/src/Filter/BaseBlacklistBuilder.php @@ -11,29 +11,29 @@ namespace GraphAware\Reco4PHP\Filter; -use GraphAware\Common\Result\Result; -use GraphAware\Common\Type\Node; +use Laudis\Neo4j\Types\CypherList; +use Laudis\Neo4j\Types\CypherMap; +use Laudis\Neo4j\Types\Node; abstract class BaseBlacklistBuilder implements BlackListBuilder { /** - * @param \GraphAware\Common\Result\Result $result - * - * @return \GraphAware\Common\Type\Node[] + * @return Node[] */ - public function buildBlackList(Result $result) + public function buildBlackList(CypherList $results): array { $nodes = []; - foreach ($result->records() as $record) { - if ($record->hasValue($this->itemResultName()) && $record->value($this->itemResultName()) instanceof Node) { - $nodes[] = $record->get($this->itemResultName()); + /** @var CypherMap $result */ + foreach ($results as $result) { + if ($result->hasKey($this->itemResultName()) && $result->get($this->itemResultName()) instanceof Node) { + $nodes[] = $result->get($this->itemResultName()); } } return $nodes; } - public function itemResultName() + public function itemResultName(): string { return 'item'; } diff --git a/src/Filter/BlackListBuilder.php b/src/Filter/BlackListBuilder.php index 3ed9403..7bb44f3 100644 --- a/src/Filter/BlackListBuilder.php +++ b/src/Filter/BlackListBuilder.php @@ -11,32 +11,22 @@ namespace GraphAware\Reco4PHP\Filter; -use GraphAware\Common\Result\Result; -use GraphAware\Common\Type\Node; +use Laudis\Neo4j\Databags\Statement; +use Laudis\Neo4j\Types\CypherList; +use Laudis\Neo4j\Types\Node; interface BlackListBuilder { - /** - * @param \GraphAware\Common\Type\Node $input - * - * @return \GraphAware\Common\Cypher\Statement - */ - public function blacklistQuery(Node $input); + public function blacklistQuery(Node $input): Statement; /** - * @param \GraphAware\Common\Result\Result + * @param CypherList * - * @return \GraphAware\Common\Type\Node[] + * @return Node[] */ - public function buildBlackList(Result $result); + public function buildBlackList(CypherList $results): array; - /** - * @return string - */ - public function itemResultName(); + public function itemResultName(): string; - /** - * @return string - */ - public function name(); + public function name(): string; } diff --git a/src/Filter/ExcludeSelf.php b/src/Filter/ExcludeSelf.php index 13b73bc..00beeb9 100644 --- a/src/Filter/ExcludeSelf.php +++ b/src/Filter/ExcludeSelf.php @@ -11,12 +11,12 @@ namespace GraphAware\Reco4PHP\Filter; -use GraphAware\Common\Type\Node; +use Laudis\Neo4j\Types\Node; class ExcludeSelf implements Filter { - public function doInclude(Node $input, Node $item) + public function doInclude(Node $input, Node $item): bool { - return $input->identity() !== $item->identity(); + return $input->getId() !== $item->getId(); } } diff --git a/src/Filter/Filter.php b/src/Filter/Filter.php index 7863e48..cc368c4 100644 --- a/src/Filter/Filter.php +++ b/src/Filter/Filter.php @@ -11,17 +11,12 @@ namespace GraphAware\Reco4PHP\Filter; -use GraphAware\Common\Type\Node; +use Laudis\Neo4j\Types\Node; interface Filter { /** * Returns whether or not the recommended node should be included in the recommendation. - * - * @param \GraphAware\Common\Type\Node $input - * @param \GraphAware\Common\Type\Node $item - * - * @return bool */ - public function doInclude(Node $input, Node $item); + public function doInclude(Node $input, Node $item): bool; } diff --git a/src/Graph/Direction.php b/src/Graph/Direction.php index 214fefd..5493c06 100644 --- a/src/Graph/Direction.php +++ b/src/Graph/Direction.php @@ -13,9 +13,9 @@ final class Direction { - const BOTH = 'BOTH'; + public const BOTH = 'BOTH'; - const INCOMING = 'INCOMING'; + public const INCOMING = 'INCOMING'; - const OUTGOING = 'OUTGOING'; + public const OUTGOING = 'OUTGOING'; } diff --git a/src/Persistence/DatabaseService.php b/src/Persistence/DatabaseService.php index 92c6804..b730da7 100644 --- a/src/Persistence/DatabaseService.php +++ b/src/Persistence/DatabaseService.php @@ -11,34 +11,29 @@ namespace GraphAware\Reco4PHP\Persistence; -use GraphAware\Neo4j\Client\ClientBuilder; -use GraphAware\Neo4j\Client\ClientInterface; +use Laudis\Neo4j\ClientBuilder; +use Laudis\Neo4j\Contracts\ClientInterface; class DatabaseService { - private $driver; + private ClientInterface $driver; public function __construct($uri = null) { - if ($uri !== null) { + if (null !== $uri) { $this->driver = ClientBuilder::create() - ->addConnection('default', $uri) + ->withDriver('default', $uri) + ->withDefaultDriver('default') ->build(); } } - /** - * @return ClientInterface - */ - public function getDriver() + public function getDriver(): ClientInterface { return $this->driver; } - /** - * @param \GraphAware\Neo4j\Client\ClientInterface $driver - */ - public function setDriver(ClientInterface $driver) + public function setDriver(ClientInterface $driver): void { $this->driver = $driver; } diff --git a/src/Post/BasePostProcessor.php b/src/Post/BasePostProcessor.php deleted file mode 100644 index aac7179..0000000 --- a/src/Post/BasePostProcessor.php +++ /dev/null @@ -1,16 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace GraphAware\Reco4PHP\Post; - -abstract class BasePostProcessor implements PostProcessor -{ -} diff --git a/src/Post/CypherAwarePostProcessor.php b/src/Post/CypherAwarePostProcessor.php index f8d99e5..47b54ad 100644 --- a/src/Post/CypherAwarePostProcessor.php +++ b/src/Post/CypherAwarePostProcessor.php @@ -11,16 +11,14 @@ namespace GraphAware\Reco4PHP\Post; -use GraphAware\Common\Type\Node; use GraphAware\Reco4PHP\Result\Recommendation; +use Laudis\Neo4j\Databags\Statement; +use Laudis\Neo4j\Types\Node; interface CypherAwarePostProcessor extends PostProcessor { /** - * @param \GraphAware\Common\Type\Node $input - * @param \GraphAware\Reco4PHP\Result\Recommendation $recommendation - * - * @return \GraphAware\Common\Cypher\Statement the statement to be executed + * @return Statement the statement to be executed */ - public function buildQuery(Node $input, Recommendation $recommendation); + public function buildQuery(Node $input, Recommendation $recommendation): Statement; } diff --git a/src/Post/PostProcessor.php b/src/Post/PostProcessor.php index 1a10f91..7dfdf41 100644 --- a/src/Post/PostProcessor.php +++ b/src/Post/PostProcessor.php @@ -13,5 +13,5 @@ interface PostProcessor { - public function name(); + public function name(): string; } diff --git a/src/Post/RecommendationSetPostProcessor.php b/src/Post/RecommendationSetPostProcessor.php index be54632..10f4d97 100644 --- a/src/Post/RecommendationSetPostProcessor.php +++ b/src/Post/RecommendationSetPostProcessor.php @@ -11,46 +11,38 @@ namespace GraphAware\Reco4PHP\Post; -use GraphAware\Common\Result\Record; -use GraphAware\Common\Result\Result; -use GraphAware\Common\Type\Node; use GraphAware\Reco4PHP\Result\Recommendation; use GraphAware\Reco4PHP\Result\Recommendations; +use Laudis\Neo4j\Databags\Statement; +use Laudis\Neo4j\Types\CypherList; +use Laudis\Neo4j\Types\CypherMap; +use Laudis\Neo4j\Types\Node; abstract class RecommendationSetPostProcessor implements PostProcessor { - /** - * @param \GraphAware\Common\Type\Node $input - * @param \GraphAware\Reco4PHP\Result\Recommendations $recommendations - * - * @return \GraphAware\Common\Cypher\Statement - */ - abstract public function buildQuery(Node $input, Recommendations $recommendations); + abstract public function buildQuery(Node $input, Recommendations $recommendations): Statement; - abstract public function postProcess(Node $input, Recommendation $recommendation, Record $record); + abstract public function postProcess(Node $input, Recommendation $recommendation, CypherMap $result): void; - final public function handleResultSet(Node $input, Result $result, Recommendations $recommendations) + final public function handleResultSet(Node $input, CypherList $results, Recommendations $recommendations): void { - $recordsMap = []; - foreach ($result->records() as $i => $record) { - if (!$record->hasValue($this->idResultName())) { - throw new \InvalidArgumentException(sprintf( - 'The record does not contain a value with key "%s" in "%s"', - $this->idResultName(), - $this->name() - )); + $resultMap = []; + /** @var CypherMap $result */ + foreach ($results as $result) { + if (!$result->hasKey($this->idResultName())) { + throw new \InvalidArgumentException(sprintf('The record does not contain a value with key "%s" in "%s"', $this->idResultName(), $this->name())); } - $recordsMap[$record->get($this->idResultName())] = $record; + $resultMap[$result->get($this->idResultName())] = $result; } foreach ($recommendations->getItems() as $recommendation) { - if (array_key_exists($recommendation->item()->identity(), $recordsMap)) { - $this->postProcess($input, $recommendation, $recordsMap[$recommendation->item()->identity()]); + if (array_key_exists($recommendation->item()->getId(), $resultMap)) { + $this->postProcess($input, $recommendation, $resultMap[$recommendation->item()->getId()]); } } } - public function idResultName() + public function idResultName(): string { return 'id'; } diff --git a/src/Post/RewardSomethingShared.php b/src/Post/RewardSomethingShared.php index 2a7cd5c..afc048d 100644 --- a/src/Post/RewardSomethingShared.php +++ b/src/Post/RewardSomethingShared.php @@ -11,48 +11,48 @@ namespace GraphAware\Reco4PHP\Post; -use GraphAware\Common\Cypher\Statement; -use GraphAware\Common\Result\RecordCursorInterface; -use GraphAware\Common\Type\Node; use GraphAware\Reco4PHP\Graph\Direction; use GraphAware\Reco4PHP\Result\Recommendation; use GraphAware\Reco4PHP\Result\SingleScore; +use Laudis\Neo4j\Databags\Statement; +use Laudis\Neo4j\Types\CypherList; +use Laudis\Neo4j\Types\Node; abstract class RewardSomethingShared implements CypherAwarePostProcessor { abstract public function relationshipType(); - public function relationshipDirection() + public function relationshipDirection(): string { return Direction::BOTH; } - final public function buildQuery(Node $input, Recommendation $recommendation) + final public function buildQuery(Node $input, Recommendation $recommendation): Statement { $relationshipPatterns = [ - Direction::BOTH => array('-[:%s]-', '-[:%s]-'), - Direction::INCOMING => array('<-[:%s]-', '-[:%s]->'), - Direction::OUTGOING => array('-[:%s]->', '<-[:%s]-'), + Direction::BOTH => ['-[:%s]-', '-[:%s]-'], + Direction::INCOMING => ['<-[:%s]-', '-[:%s]->'], + Direction::OUTGOING => ['-[:%s]->', '<-[:%s]-'], ]; $relPattern = sprintf($relationshipPatterns[$this->relationshipDirection()][0], $this->relationshipType()); $inversedRelPattern = sprintf($relationshipPatterns[$this->relationshipDirection()][1], $this->relationshipType()); - $query = 'MATCH (input) WHERE id(input) = {inputId}, (item) WHERE id(item) = {itemId} + $query = 'MATCH (input) WHERE id(input) = $inputId, (item) WHERE id(item) = $itemId MATCH (input)'.$relPattern.'(shared)'.$inversedRelPattern.'(item) RETURN shared as sharedThing'; - return Statement::create($query, ['inputId' => $input->identity(), 'itemId' => $recommendation->item()->identity()]); + return Statement::create($query, ['inputId' => $input->getId(), 'itemId' => $recommendation->item()->getId()]); } - public function postProcess(Node $input, Recommendation $recommendation, RecordCursorInterface $result = null) + public function postProcess(Node $input, Recommendation $recommendation, ?CypherList $results = null): void { - if (null === $result) { + if (null === $results) { throw new \RuntimeException(sprintf('Expected a non-null result in %s::postProcess()', get_class($this))); } - if (count($result->records()) > 0) { - foreach ($result->records() as $record) { + if (count($results) > 0) { + foreach ($results as $result) { $recommendation->addScore($this->name(), new SingleScore(1)); } } diff --git a/src/RecommenderService.php b/src/RecommenderService.php index c97423a..b0c4436 100644 --- a/src/RecommenderService.php +++ b/src/RecommenderService.php @@ -11,55 +11,34 @@ namespace GraphAware\Reco4PHP; -use GraphAware\Common\Type\Node; -use GraphAware\Neo4j\Client\ClientInterface; -use GraphAware\Reco4PHP\Persistence\DatabaseService; -use Psr\Log\LoggerInterface; -use Psr\Log\NullLogger; -use Symfony\Component\EventDispatcher\EventDispatcher; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; use GraphAware\Reco4PHP\Engine\RecommendationEngine; +use GraphAware\Reco4PHP\Persistence\DatabaseService; +use InvalidArgumentException; +use Laudis\Neo4j\Contracts\ClientInterface; +use Laudis\Neo4j\Types\CypherList; +use Laudis\Neo4j\Types\Node; class RecommenderService { /** - * @var \GraphAware\Reco4PHP\Engine\RecommendationEngine[] + * @var RecommendationEngine[] */ - private $engines = []; + private array $engines = []; - /** - * @var \GraphAware\Reco4PHP\Persistence\DatabaseService - */ - private $databaseService; - - /** - * @var null|\Symfony\Component\EventDispatcher\EventDispatcher|\Symfony\Component\EventDispatcher\EventDispatcherInterface - */ - private $eventDispatcher; - - /** - * @var null|\Psr\Log\LoggerInterface|\Psr\Log\NullLogger - */ - private $logger; + private DatabaseService $databaseService; /** * RecommenderService constructor. - * - * @param \GraphAware\Reco4PHP\Persistence\DatabaseService $databaseService - * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface|null $eventDispatcher - * @param \Psr\Log\LoggerInterface|null $logger */ - public function __construct(DatabaseService $databaseService, EventDispatcherInterface $eventDispatcher = null, LoggerInterface $logger = null) + public function __construct(DatabaseService $databaseService) { $this->databaseService = $databaseService; - $this->eventDispatcher = null !== $eventDispatcher ? $eventDispatcher : new EventDispatcher(); - $this->logger = null !== $logger ? $logger : new NullLogger(); } /** * @param string $uri * - * @return \GraphAware\Reco4PHP\RecommenderService + * @return RecommenderService */ public static function create($uri) { @@ -67,9 +46,7 @@ public static function create($uri) } /** - * @param ClientInterface $client - * - * @return \GraphAware\Reco4PHP\RecommenderService + * @return RecommenderService */ public static function createWithClient(ClientInterface $client) { @@ -79,66 +56,46 @@ public static function createWithClient(ClientInterface $client) return new self($databaseService); } - /** - * @param $id - * - * @return \GraphAware\Bolt\Result\Type\Node|\GraphAware\Bolt\Result\Type\Path|\GraphAware\Bolt\Result\Type\Relationship|mixed - */ - public function findInputById($id) + public function findInputById(int $id): Node { - $id = (int) $id; - $result = $this->databaseService->getDriver()->run('MATCH (n) WHERE id(n) = {id} RETURN n as input', ['id' => $id]); + $result = $this->databaseService->getDriver()->run('MATCH (n) WHERE id(n) = $id RETURN n as input', ['id' => $id]); return $this->validateInput($result); } - /** - * @param string $label - * @param string $key - * @param mixed $value - * - * @return \GraphAware\Common\Type\Node - */ - public function findInputBy($label, $key, $value) + public function findInputBy(string $label, string $key, mixed $value): Node { - $query = sprintf('MATCH (n:%s {%s: {value} }) RETURN n as input', $label, $key); + $query = sprintf('MATCH (n:%s {%s: $value }) RETURN n as input', $label, $key); $result = $this->databaseService->getDriver()->run($query, ['value' => $value]); return $this->validateInput($result); } /** - * @param \GraphAware\Common\Result\Result $result - * - * @return \GraphAware\Common\Type\Node + * @throws InvalidArgumentException */ - public function validateInput($result) + public function validateInput(CypherList $results): Node { - if (count($result->records()) < 1 || !$result->getRecord()->value('input') instanceof Node) { - throw new \InvalidArgumentException(sprintf('Node not found')); + if (count($results) < 1 || !$results->first()->get('input') instanceof Node) { + throw new InvalidArgumentException(sprintf('Node not found')); } - return $result->getRecord()->value('input'); + return $results->first()->get('input'); } /** - * @param $name - * - * @return \GraphAware\Reco4PHP\Engine\RecommendationEngine + * @throws InvalidArgumentException */ - public function getRecommender($name) + public function getRecommender(string $name): RecommendationEngine { if (!array_key_exists($name, $this->engines)) { - throw new \InvalidArgumentException(sprintf('The Recommendation engine "%s" is not registered in the Recommender Service', $name)); + throw new InvalidArgumentException(sprintf('The Recommendation engine "%s" is not registered in the Recommender Service', $name)); } return $this->engines[$name]; } - /** - * @param \GraphAware\Reco4PHP\Engine\RecommendationEngine $recommendationEngine - */ - public function registerRecommendationEngine(RecommendationEngine $recommendationEngine) + public function registerRecommendationEngine(RecommendationEngine $recommendationEngine): void { $recommendationEngine->setDatabaseService($this->databaseService); $this->engines[$recommendationEngine->name()] = $recommendationEngine; diff --git a/src/Result/PartialScore.php b/src/Result/PartialScore.php deleted file mode 100644 index 452ddbe..0000000 --- a/src/Result/PartialScore.php +++ /dev/null @@ -1,83 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace GraphAware\Reco4PHP\Result; - -class PartialScore -{ - /** - * @var float - */ - protected $value; - - /** - * @var \GraphAware\Reco4PHP\Result\Reason[] - */ - protected $reasons = []; - - /** - * @param float $value - * @param mixed $reason - */ - public function __construct($value = 0, $reason = null) - { - $this->value = (float) $value; - $this->addReason($value, $reason); - } - - /** - * @param float $value - * @param array $details - */ - public function add($value, $reaon = null) - { - $this->value += (float) $value; - $this->addReason($value, $reaon); - } - - /** - * @return float - */ - public function getValue() - { - return $this->value; - } - - /** - * @param float $value - * @param array $details - */ - public function setNewValue($value, array $details = array()) - { - $this->add(-($this->value - $value), $details); - } - - /** - * @return \GraphAware\Reco4PHP\Result\Reason[] - */ - public function getReasons() - { - return $this->reasons; - } - - /** - * @param $value - * @param array $details - */ - private function addReason($value, $detail = null) - { - if (!$detail) { - return; - } - - $this->reasons[] = new Reason($value, $detail); - } -} diff --git a/src/Result/Reason.php b/src/Result/Reason.php deleted file mode 100644 index 6131f60..0000000 --- a/src/Result/Reason.php +++ /dev/null @@ -1,35 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace GraphAware\Reco4PHP\Result; - -class Reason -{ - protected $value; - - protected $detail; - - public function __construct($value, $detail) - { - $this->value = (float) $value; - $this->detail = $detail; - } - - public function getValue() - { - return $this->value; - } - - public function getDetail() - { - return $this->detail; - } -} diff --git a/src/Result/Recommendation.php b/src/Result/Recommendation.php index 58b8bee..ad5afed 100644 --- a/src/Result/Recommendation.php +++ b/src/Result/Recommendation.php @@ -11,29 +11,21 @@ namespace GraphAware\Reco4PHP\Result; -use GraphAware\Common\Type\Node; +use Laudis\Neo4j\Types\Node; class Recommendation { - /** - * @var \GraphAware\Common\Type\Node - */ - protected $item; + protected Node $item; /** - * @var \GraphAware\Reco4PHP\Result\Score[] + * @var Score[] */ - protected $scores = []; + protected array $scores = []; - /** - * @var float - */ - protected $totalScore = 0.0; + protected float $totalScore = 0.0; /** * Recommendation constructor. - * - * @param \GraphAware\Common\Type\Node $item */ public function __construct(Node $item) { @@ -41,19 +33,18 @@ public function __construct(Node $item) } /** - * @param string $name - * @param \GraphAware\Reco4PHP\Result\SingleScore $score + * @param string $name */ - public function addScore($name, SingleScore $score) + public function addScore($name, SingleScore $score): void { $this->getScoreOrCreate($name)->add($score); $this->totalScore += $score->getScore(); } /** - * @param \GraphAware\Reco4PHP\Result\SingleScore[] + * @param SingleScore[] */ - public function addScores(array $scores) + public function addScores(array $scores): void { foreach ($scores as $name => $singleScores) { foreach ($singleScores->getScores() as $score) { @@ -63,19 +54,14 @@ public function addScores(array $scores) } /** - * @return \GraphAware\Reco4PHP\Result\SingleScore[] + * @return SingleScore[] */ - public function getScores() + public function getScores(): array { return $this->scores; } - /** - * @param string $key - * - * @return \GraphAware\Reco4PHP\Result\Score - */ - public function getScore($key) + public function getScore(string $key): Score { if (!array_key_exists($key, $this->scores)) { throw new \InvalidArgumentException(sprintf('The recommendation does not contains a score named "%s"', $key)); @@ -84,7 +70,7 @@ public function getScore($key) return $this->scores[$key]; } - private function getScoreOrCreate($name) + private function getScoreOrCreate(string $name): Score { if (!array_key_exists($name, $this->scores)) { $this->scores[$name] = new Score($name); @@ -93,18 +79,12 @@ private function getScoreOrCreate($name) return $this->scores[$name]; } - /** - * @return float - */ - public function totalScore() + public function totalScore(): float { return $this->totalScore; } - /** - * @return \GraphAware\Common\Type\Node - */ - public function item() + public function item(): Node { return $this->item; } diff --git a/src/Result/Recommendations.php b/src/Result/Recommendations.php index 61b1dab..323b03d 100644 --- a/src/Result/Recommendations.php +++ b/src/Result/Recommendations.php @@ -11,78 +11,59 @@ namespace GraphAware\Reco4PHP\Result; -use GraphAware\Common\Type\Node; use GraphAware\Reco4PHP\Context\Context; +use Laudis\Neo4j\Types\Node; class Recommendations { - /** - * @var \GraphAware\Reco4PHP\Context\Context - */ - protected $context; + protected Context $context; /** - * @var \GraphAware\Reco4PHP\Result\Recommendation[] + * @var Recommendation[] */ - protected $recommendations = []; + protected array $recommendations = []; - /** - * @param \GraphAware\Reco4PHP\Context\Context $context - */ public function __construct(Context $context) { $this->context = $context; } - /** - * @param \GraphAware\Common\Type\Node $item - * - * @return \GraphAware\Reco4PHP\Result\Recommendation - */ - public function getOrCreate(Node $item) + public function getOrCreate(Node $item): Recommendation { - if (array_key_exists($item->identity(), $this->recommendations)) { - return $this->recommendations[$item->identity()]; + if (array_key_exists($item->getId(), $this->recommendations)) { + return $this->recommendations[$item->getId()]; } $recommendation = new Recommendation($item); - $this->recommendations[$item->identity()] = $recommendation; + $this->recommendations[$item->getId()] = $recommendation; return $recommendation; } - /** - * @param \GraphAware\Common\Type\Node $item - * @param string $name - * @param \GraphAware\Reco4PHP\Result\SingleScore $singleScore - */ - public function add(Node $item, $name, SingleScore $singleScore) + public function add(Node $item, string $name, SingleScore $singleScore): void { $this->getOrCreate($item)->addScore($name, $singleScore); } - /** - * @param \GraphAware\Reco4PHP\Result\Recommendations $recommendations - */ - public function merge(Recommendations $recommendations) + public function merge(Recommendations $recommendations): void { foreach ($recommendations->getItems() as $recommendation) { $this->getOrCreate($recommendation->item())->addScores($recommendation->getScores()); } } - public function remove(Recommendation $recommendation) + public function remove(Recommendation $recommendation): void { - if (!array_key_exists($recommendation->item()->identity(), $this->recommendations)) { + if (!array_key_exists($recommendation->item()->getId(), $this->recommendations)) { return; } - unset($this->recommendations[$recommendation->item()->identity()]); + unset($this->recommendations[$recommendation->item()->getId()]); } /** - * @return \GraphAware\Reco4PHP\Result\Recommendation[] + * @return Recommendation[] */ - public function getItems($size = null) : array + public function getItems(?int $size = null): array { if (is_int($size) && $size > 0) { return array_slice($this->recommendations, 0, $size); @@ -91,33 +72,20 @@ public function getItems($size = null) : array return array_values($this->recommendations); } - /** - * @param $position - * - * @return \GraphAware\Reco4PHP\Result\Recommendation - */ - public function get($position) : Recommendation + public function get(int $position): Recommendation { return array_values($this->recommendations)[$position]; } - /** - * @return int - */ - public function size() : int + public function size(): int { return count($this->recommendations); } - /** - * @param string $key - * @param mixed $value - * @return \GraphAware\Reco4PHP\Result\Recommendation|null - */ - public function getItemBy($key, $value) + public function getItemBy(string $key, mixed $value): ?Recommendation { foreach ($this->getItems() as $recommendation) { - if ($recommendation->item()->hasValue($key) && $recommendation->item()->value($key) === $value) { + if ($recommendation->item()->getProperties()->hasKey($key) && $recommendation->item()->getProperty($key) === $value) { return $recommendation; } } @@ -125,14 +93,10 @@ public function getItemBy($key, $value) return null; } - /** - * @param int $id - * @return \GraphAware\Reco4PHP\Result\Recommendation|null - */ - public function getItemById($id) + public function getItemById(int $id): ?Recommendation { foreach ($this->getItems() as $item) { - if ($item->item()->identity() === $id) { + if ($item->item()->getId() === $id) { return $item; } } @@ -140,17 +104,14 @@ public function getItemById($id) return null; } - public function sort() + public function sort(): void { usort($this->recommendations, function (Recommendation $recommendationA, Recommendation $recommendationB) { return $recommendationA->totalScore() <= $recommendationB->totalScore(); }); } - /** - * @return \GraphAware\Reco4PHP\Context\Context - */ - public function getContext() : Context + public function getContext(): Context { return $this->context; } diff --git a/src/Result/ResultCollection.php b/src/Result/ResultCollection.php new file mode 100644 index 0000000..408c48d --- /dev/null +++ b/src/Result/ResultCollection.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace GraphAware\Reco4PHP\Result; + +use InvalidArgumentException; +use Laudis\Neo4j\Types\CypherList; + +class ResultCollection +{ + /** + * @var CypherList[] + */ + protected $resultsMap = []; + + public function add(CypherList $results, string $tag) + { + $this->resultsMap[$tag] = $results; + } + + /** + * @param mixed $default + * + * @throws InvalidArgumentException + */ + public function get(string $tag, mixed $default = null): CypherList + { + if (array_key_exists($tag, $this->resultsMap)) { + return $this->resultsMap[$tag]; + } + + if (2 === func_num_args()) { + return $default; + } + + throw new InvalidArgumentException(sprintf('This result collection does not contains a results for tag "%s"', $tag)); + } +} diff --git a/src/Result/Score.php b/src/Result/Score.php index 9c5feba..89a792d 100644 --- a/src/Result/Score.php +++ b/src/Result/Score.php @@ -13,37 +13,28 @@ class Score { - /** - * @var float - */ - protected $score = 0.0; + protected float $score = 0.0; /** - * @var \GraphAware\Reco4PHP\Result\SingleScore[] + * @var SingleScore[] */ - protected $scores = []; + protected array $scores = []; - /** - * @param \GraphAware\Reco4PHP\Result\SingleScore $score - */ - public function add(SingleScore $score) + public function add(SingleScore $score): void { $this->scores[] = $score; - $this->score += (float) $score->getScore(); + $this->score += $score->getScore(); } - /** - * @return float - */ - public function score() + public function score(): float { return $this->score; } /** - * @return \GraphAware\Reco4PHP\Result\SingleScore[] + * @return SingleScore[] */ - public function getScores() + public function getScores(): array { return $this->scores; } diff --git a/src/Result/SingleScore.php b/src/Result/SingleScore.php index 8a9878b..275dc2d 100644 --- a/src/Result/SingleScore.php +++ b/src/Result/SingleScore.php @@ -13,47 +13,32 @@ class SingleScore { - /** - * @var float - */ - private $score; + private float $score; - /** - * @var null|string - */ - private $reason; + private ?string $reason; /** * SingleScore constructor. - * - * @param float|$score - * @param null|string $reason */ - public function __construct($score, $reason = null) + public function __construct(float $score, ?string $reason = null) { - $this->score = (float) $score; + $this->score = $score; $this->addReason($reason); } - public function addReason($reason = null) + public function addReason(?string $reason = null): void { if (null !== $reason) { - $this->reason = (string) $reason; + $this->reason = $reason; } } - /** - * @return float - */ - public function getScore() + public function getScore(): float { return $this->score; } - /** - * @return null|string - */ - public function getReason() + public function getReason(): ?string { return $this->reason; } diff --git a/src/Transactional/BaseCypherAware.php b/src/Transactional/BaseCypherAware.php deleted file mode 100644 index 186dcf0..0000000 --- a/src/Transactional/BaseCypherAware.php +++ /dev/null @@ -1,37 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace GraphAware\Reco4PHP\Transactional; - -abstract class BaseCypherAware implements CypherAware -{ - private $parameters = []; - - final protected function addParameter($key, $value) - { - if (!is_scalar($value) && !is_array($value)) { - throw new \InvalidArgumentException(sprintf("Expected a scalar or array value for '%s'", $key)); - } - $this->parameters[$key] = $value; - } - - final protected function addParameters(array $parameters) - { - foreach ($parameters as $key => $parameter) { - $this->addParameter($key, $parameter); - } - } - - final public function parameters() - { - return $this->parameters; - } -} diff --git a/src/Transactional/CypherAware.php b/src/Transactional/CypherAware.php deleted file mode 100644 index cc01660..0000000 --- a/src/Transactional/CypherAware.php +++ /dev/null @@ -1,19 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace GraphAware\Reco4PHP\Transactional; - -interface CypherAware -{ - public function query(); - - public function parameters(); -} diff --git a/src/Util/NodeProxy.php b/src/Util/NodeProxy.php deleted file mode 100644 index fdd2f4c..0000000 --- a/src/Util/NodeProxy.php +++ /dev/null @@ -1,92 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace GraphAware\Reco4PHP\Util; - -use GraphAware\Common\Type\Node; - -class NodeProxy implements Node -{ - protected $id; - - protected $properties = []; - - protected $labels = []; - - public function __construct($id = null, array $properties = array(), array $labels = array()) - { - $this->id = $id; - $this->properties = $properties; - $this->labels = $labels; - } - - public function identity() - { - return $this->id; - } - - public function keys() - { - return array_keys($this->properties); - } - - public function containsKey($key) - { - return array_key_exists($key, $this->properties); - } - - public function get($key) - { - if (!$this->containsKey($key)) { - throw new \InvalidArgumentException(sprintf('This node doesn\'t contain the "%s" property'), $key); - } - - return $this->properties[$key]; - } - - public function hasValue($key) - { - return $this->containsKey($key); - } - - public function value($key, $default = null) - { - if (!$this->containsKey($key) && 1 === func_num_args()) { - throw new \InvalidArgumentException(sprintf('This node doesn\'t contain the "%s" property'), $key); - } - - return $this->containsKey($key) ? $this->properties[$key] : $default; - } - - public function values() - { - return $this->properties; - } - - public function asArray() - { - return [ - 'id' => $this->id, - 'labels' => $this->labels, - 'properties' => $this->properties, - ]; - } - - public function labels() - { - return $this->labels; - } - - public function hasLabel($label) - { - return in_array($label, $this->labels); - } -} diff --git a/tests/Algorithms/Model/KNNModelBuilderTest.php b/tests/Algorithms/Model/KNNModelBuilderTest.php index 697fbbf..f1b8c31 100644 --- a/tests/Algorithms/Model/KNNModelBuilderTest.php +++ b/tests/Algorithms/Model/KNNModelBuilderTest.php @@ -2,82 +2,83 @@ namespace GraphAware\Reco4PHP\Tests\Algorithms\Model; +use GraphAware\Reco4PHP\Algorithms\Model\KNNModelBuilder; +use GraphAware\Reco4PHP\Algorithms\Model\Rating; use GraphAware\Reco4PHP\Algorithms\Similarity\CosineSimilarity; use GraphAware\Reco4PHP\Common\ObjectSet; -use GraphAware\Reco4PHP\Algorithms\Model\Rating; use GraphAware\Reco4PHP\Tests\Helper\FakeNode; -use GraphAware\Reco4PHP\Algorithms\Model\KNNModelBuilder; +use PHPUnit\Framework\TestCase; /** * @group algorithms * @group knn */ -class KNNModelBuilderTest extends \PHPUnit_Framework_TestCase +class KNNModelBuilderTest extends TestCase { - public function testCreateVectors() + public function testCreateVectors(): void { - $instance = new KNNModelBuilder(); + $instance = new KNNModelBuilder(new CosineSimilarity()); $source = new ObjectSet(Rating::class); $destination = new ObjectSet(Rating::class); - $node1 = new FakeNode(1); - $node2 = new FakeNode(2); - $node3 = new FakeNode(3); - $node4 = new FakeNode(4); + $node1 = FakeNode::createDummy(1); + $node2 = FakeNode::createDummy(2); + $node3 = FakeNode::createDummy(3); + $node4 = FakeNode::createDummy(4); - $source->add(new Rating(1, $node1->identity())); - $source->add(new Rating(1, $node3->identity())); + $source->add(new Rating(1, $node1->getId())); + $source->add(new Rating(1, $node3->getId())); - $destination->add(new Rating(1, $node2->identity())); - $destination->add(new Rating(1, $node4->identity())); + $destination->add(new Rating(1, $node2->getId())); + $destination->add(new Rating(1, $node4->getId())); $vectors = $instance->createVectors($source, $destination); $xVector = $vectors[0]; $yVector = $vectors[1]; - $this->assertEquals(array(1,0,1,0), $xVector); - $this->assertEquals(array(0,1,0,1), $yVector); + $this->assertEquals([1, 0, 1, 0], $xVector); + $this->assertEquals([0, 1, 0, 1], $yVector); } - public function testComputeSimilarity() + public function testComputeSimilarity(): void { - $instance = new KNNModelBuilder(null, new CosineSimilarity()); + $instance = new KNNModelBuilder(new CosineSimilarity()); $source = new ObjectSet(Rating::class); $destination = new ObjectSet(Rating::class); - $node1 = new FakeNode(1); - $node2 = new FakeNode(2); - $node3 = new FakeNode(3); - $node4 = new FakeNode(4); + $node1 = FakeNode::createDummy(1); + $node2 = FakeNode::createDummy(2); + $node3 = FakeNode::createDummy(3); + $node4 = FakeNode::createDummy(4); - $source->add(new Rating(1, $node1->identity())); - $source->add(new Rating(1, $node3->identity())); + $source->add(new Rating(1, $node1->getId())); + $source->add(new Rating(1, $node3->getId())); - $destination->add(new Rating(1, $node2->identity())); - $destination->add(new Rating(1, $node4->identity())); + $destination->add(new Rating(1, $node2->getId())); + $destination->add(new Rating(1, $node4->getId())); $similarity = $instance->computeSimilarity($source, $destination); $this->assertEquals(0.0, $similarity); } - public function testComputeSimilarity2() + public function testComputeSimilarity2(): void { - $instance = new KNNModelBuilder(null, new CosineSimilarity()); + $instance = new KNNModelBuilder(new CosineSimilarity()); $source = new ObjectSet(Rating::class); $destination = new ObjectSet(Rating::class); - $node1 = new FakeNode(1); - $node2 = new FakeNode(2); - $node3 = new FakeNode(3); - $node4 = new FakeNode(4); - $node5 = new FakeNode(5); + $node1 = FakeNode::createDummy(1); + $node2 = FakeNode::createDummy(2); + $node3 = FakeNode::createDummy(3); + $node4 = FakeNode::createDummy(4); + $node5 = FakeNode::createDummy(5); - $source->add(new Rating(1, $node1->identity())); - $source->add(new Rating(3, $node4->identity())); + $source->add(new Rating(1, $node1->getId())); + $source->add(new Rating(3, $node4->getId())); - $destination->add(new Rating(1, $node2->identity())); - $destination->add(new Rating(2, $node4->identity())); - $destination->add(new Rating(5, $node5->identity())); + $destination->add(new Rating(1, $node2->getId())); + $destination->add(new Rating(2, $node4->getId())); + $destination->add(new Rating(5, $node5->getId())); $similarity = $instance->computeSimilarity($source, $destination); $this->assertTrue($similarity >= 0.34641016 && $similarity <= 0.346410161514); } -} \ No newline at end of file +} diff --git a/tests/Config/SimpleConfigUnitTest.php b/tests/Config/SimpleConfigUnitTest.php index 2ec2f9f..a376a00 100644 --- a/tests/Config/SimpleConfigUnitTest.php +++ b/tests/Config/SimpleConfigUnitTest.php @@ -4,9 +4,10 @@ use GraphAware\Reco4PHP\Config\SimpleConfig; use GraphAware\Reco4PHP\Tests\Helper\FakeNode; -use GraphAware\Common\Type\Node; +use Laudis\Neo4j\Types\Node; +use PHPUnit\Framework\TestCase; -class SimpleConfigUnitTest extends \PHPUnit_Framework_TestCase +class SimpleConfigUnitTest extends TestCase { public function testDefault() { @@ -31,4 +32,4 @@ public function testConfigExtendsKeyValue() $this->assertEquals('june', $config->get('month')); $this->assertInstanceOf(Node::class, $config->get('obj')); } -} \ No newline at end of file +} diff --git a/tests/Context/ContextUnitTest.php b/tests/Context/ContextUnitTest.php index b316bb6..2e943ad 100644 --- a/tests/Context/ContextUnitTest.php +++ b/tests/Context/ContextUnitTest.php @@ -2,11 +2,12 @@ namespace GraphAware\Reco4PHP\Tests\Context; +use GraphAware\Reco4PHP\Config\Config; use GraphAware\Reco4PHP\Context\SimpleContext; use GraphAware\Reco4PHP\Tests\Helper\FakeNode; -use GraphAware\Reco4PHP\Config\Config; +use PHPUnit\Framework\TestCase; -class ContextUnitTest extends \PHPUnit_Framework_TestCase +class ContextUnitTest extends TestCase { public function testDefault() { @@ -15,4 +16,3 @@ public function testDefault() $this->assertInstanceOf(Config::class, $context->config()); } } - diff --git a/tests/Engine/OverrideDiscoveryEngine.php b/tests/Engine/OverrideDiscoveryEngine.php index 0ef6876..cd00288 100644 --- a/tests/Engine/OverrideDiscoveryEngine.php +++ b/tests/Engine/OverrideDiscoveryEngine.php @@ -11,46 +11,37 @@ namespace GraphAware\Reco4PHP\Tests\Engine; -use GraphAware\Common\Cypher\Statement; -use GraphAware\Common\Cypher\StatementInterface; -use GraphAware\Common\Result\Record; -use GraphAware\Common\Type\Node; use GraphAware\Reco4PHP\Context\Context; -use GraphAware\Reco4PHP\Result\SingleScore; +use Laudis\Neo4j\Databags\Statement; +use Laudis\Neo4j\Types\Node; class OverrideDiscoveryEngine extends TestDiscoveryEngine { - public function discoveryQuery(Node $input, Context $context) : StatementInterface + public function discoveryQuery(Node $input, Context $context): Statement { - $query = "MATCH (n) WHERE id(n) <> {input} - RETURN n LIMIT {limit}"; + $query = 'MATCH (n) WHERE id(n) <> $input + RETURN n LIMIT $limit'; - return Statement::create($query, ['input' => $input->identity(), 'limit' => 300]); + return Statement::create($query, ['input' => $input->getId(), 'limit' => 300]); } - public function buildScore(Node $input, Node $item, Record $record, Context $context) : SingleScore + public function idParamName(): string { - return parent::buildScore($input, $item, $record, $context); + return 'source'; } - public function idParamName() : string + public function recoResultName(): string { - return "source"; + return 'recommendation'; } - public function recoResultName() : string + public function scoreResultName(): string { - return "recommendation"; + return 'rate'; } - public function scoreResultName() : string - { - return "rate"; - } - - public function defaultScore() : float + public function defaultScore(): float { return 10; } - -} \ No newline at end of file +} diff --git a/tests/Engine/RecommendationEngineTest.php b/tests/Engine/RecommendationEngineTest.php index b54ae21..28f07b9 100644 --- a/tests/Engine/RecommendationEngineTest.php +++ b/tests/Engine/RecommendationEngineTest.php @@ -3,11 +3,12 @@ namespace GraphAware\Reco4PHP\Tests\Engine; use GraphAware\Reco4PHP\Engine\BaseRecommendationEngine; +use PHPUnit\Framework\TestCase; -class RecommendationEngineTest extends \PHPUnit_Framework_TestCase +class RecommendationEngineTest extends TestCase { - public function testWiring() + public function testWiring(): void { $stub = $this->getMockForAbstractClass(BaseRecommendationEngine::class); } -} \ No newline at end of file +} diff --git a/tests/Engine/SingleDiscoveryEngineTest.php b/tests/Engine/SingleDiscoveryEngineTest.php index 95045ff..16fab0a 100644 --- a/tests/Engine/SingleDiscoveryEngineTest.php +++ b/tests/Engine/SingleDiscoveryEngineTest.php @@ -11,48 +11,49 @@ namespace GraphAware\Reco4PHP\Tests\Engine; -use GraphAware\Common\Cypher\Statement; use GraphAware\Reco4PHP\Config\SimpleConfig; use GraphAware\Reco4PHP\Context\SimpleContext; use GraphAware\Reco4PHP\Engine\SingleDiscoveryEngine; use GraphAware\Reco4PHP\Tests\Helper\FakeNode; +use Laudis\Neo4j\Databags\Statement; +use PHPUnit\Framework\TestCase; /** * @group engine */ -class SingleDiscoveryEngineTest extends \PHPUnit_Framework_TestCase +class SingleDiscoveryEngineTest extends TestCase { - public function testInit() + public function testInit(): void { $engine = new TestDiscoveryEngine(); $this->assertInstanceOf(SingleDiscoveryEngine::class, $engine); $input = FakeNode::createDummy(); $this->assertInstanceOf(Statement::class, $engine->discoveryQuery($input, new SimpleContext())); - $this->assertEquals("MATCH (n) WHERE id(n) <> {inputId} RETURN n", $engine->discoveryQuery($input, new SimpleContext())->text()); - $this->assertEquals("score", $engine->scoreResultName()); - $this->assertEquals("reco", $engine->recoResultName()); + $this->assertEquals('MATCH (n) WHERE id(n) <> $inputId RETURN n', $engine->discoveryQuery($input, new SimpleContext())->getText()); + $this->assertEquals('score', $engine->scoreResultName()); + $this->assertEquals('reco', $engine->recoResultName()); $this->assertEquals(1, $engine->defaultScore()); - $this->assertEquals("test_discovery", $engine->name()); + $this->assertEquals('test_discovery', $engine->name()); } - public function testParametersBuilding() + public function testParametersBuilding(): void { $engine = new TestDiscoveryEngine(); $input = FakeNode::createDummy(); - $this->assertEquals($input->identity(), $engine->discoveryQuery($input, new SimpleContext())->parameters()['inputId']); - $this->assertCount(1, $engine->discoveryQuery($input, new SimpleContext())->parameters()); + $this->assertEquals($input->getId(), $engine->discoveryQuery($input, new SimpleContext())->getParameters()['inputId']); + $this->assertCount(1, $engine->discoveryQuery($input, new SimpleContext())->getParameters()); } - public function testOverride() + public function testOverride(): void { $engine = new OverrideDiscoveryEngine(); $input = FakeNode::createDummy(); $context = new SimpleContext(new SimpleConfig()); - $this->assertCount(2, $engine->discoveryQuery($input, $context)->parameters()); - $this->assertEquals($input->identity(), $engine->discoveryQuery($input, $context)->parameters()['input']); - $this->assertEquals("recommendation", $engine->recoResultName()); - $this->assertEquals("rate", $engine->scoreResultName()); - $this->assertEquals("source", $engine->idParamName()); + $this->assertCount(2, $engine->discoveryQuery($input, $context)->getParameters()); + $this->assertEquals($input->getId(), $engine->discoveryQuery($input, $context)->getParameters()['input']); + $this->assertEquals('recommendation', $engine->recoResultName()); + $this->assertEquals('rate', $engine->scoreResultName()); + $this->assertEquals('source', $engine->idParamName()); $this->assertEquals(10, $engine->defaultScore()); } -} \ No newline at end of file +} diff --git a/tests/Engine/TestDiscoveryEngine.php b/tests/Engine/TestDiscoveryEngine.php index 652368f..b74739d 100644 --- a/tests/Engine/TestDiscoveryEngine.php +++ b/tests/Engine/TestDiscoveryEngine.php @@ -11,24 +11,22 @@ namespace GraphAware\Reco4PHP\Tests\Engine; -use GraphAware\Common\Cypher\Statement; -use GraphAware\Common\Cypher\StatementInterface; use GraphAware\Reco4PHP\Context\Context; use GraphAware\Reco4PHP\Engine\SingleDiscoveryEngine; -use GraphAware\Common\Type\Node; +use Laudis\Neo4j\Databags\Statement; +use Laudis\Neo4j\Types\Node; class TestDiscoveryEngine extends SingleDiscoveryEngine { - public function discoveryQuery(Node $input, Context $context) : StatementInterface + public function discoveryQuery(Node $input, Context $context): Statement { - $query = "MATCH (n) WHERE id(n) <> {inputId} RETURN n"; + $query = 'MATCH (n) WHERE id(n) <> $inputId RETURN n'; - return Statement::create($query, ['inputId' => $input->identity()]); + return Statement::create($query, ['inputId' => $input->getId()]); } - public function name() : string + public function name(): string { - return "test_discovery"; + return 'test_discovery'; } - -} \ No newline at end of file +} diff --git a/tests/Example/Discovery/FromSameGenreILike.php b/tests/Example/Discovery/FromSameGenreILike.php index 9cf7dbb..7d13b37 100644 --- a/tests/Example/Discovery/FromSameGenreILike.php +++ b/tests/Example/Discovery/FromSameGenreILike.php @@ -2,21 +2,21 @@ namespace GraphAware\Reco4PHP\Tests\Example\Discovery; -use GraphAware\Common\Cypher\Statement; -use GraphAware\Common\Type\Node; use GraphAware\Reco4PHP\Context\Context; use GraphAware\Reco4PHP\Engine\SingleDiscoveryEngine; +use Laudis\Neo4j\Databags\Statement; +use Laudis\Neo4j\Types\Node; class FromSameGenreILike extends SingleDiscoveryEngine { - public function name() + public function name(): string { return 'from_genre_i_like'; } - public function discoveryQuery(Node $input, Context $context) + public function discoveryQuery(Node $input, Context $context): Statement { - $query = 'MATCH (input) WHERE id(input) = {id} + $query = 'MATCH (input) WHERE id(input) = $id MATCH (input)-[r:RATED]->(movie)-[:HAS_GENRE]->(genre) WITH distinct genre, sum(r.rating) as score ORDER BY score DESC @@ -25,7 +25,6 @@ public function discoveryQuery(Node $input, Context $context) RETURN reco LIMIT 200'; - return Statement::create($query, ['id' => $input->identity()]); + return Statement::create($query, ['id' => $input->getId()]); } - -} \ No newline at end of file +} diff --git a/tests/Example/Discovery/RatedByOthers.php b/tests/Example/Discovery/RatedByOthers.php index 963907b..18c0ec2 100644 --- a/tests/Example/Discovery/RatedByOthers.php +++ b/tests/Example/Discovery/RatedByOthers.php @@ -2,28 +2,26 @@ namespace GraphAware\Reco4PHP\Tests\Example\Discovery; -use GraphAware\Common\Cypher\Statement; -use GraphAware\Common\Type\Node; use GraphAware\Reco4PHP\Context\Context; use GraphAware\Reco4PHP\Engine\SingleDiscoveryEngine; +use Laudis\Neo4j\Databags\Statement; +use Laudis\Neo4j\Types\Node; class RatedByOthers extends SingleDiscoveryEngine { - public function discoveryQuery(Node $input, Context $context) + public function discoveryQuery(Node $input, Context $context): Statement { - $query = 'MATCH (input:User) WHERE id(input) = {id} + $query = 'MATCH (input:User) WHERE id(input) = $id MATCH (input)-[:RATED]->(m)<-[:RATED]-(o) WITH distinct o MATCH (o)-[:RATED]->(reco) RETURN distinct reco LIMIT 500'; - return Statement::create($query, ['id' => $input->identity()]); + return Statement::create($query, ['id' => $input->getId()]); } - - public function name() + public function name(): string { - return "rated_by_others"; + return 'rated_by_others'; } - -} \ No newline at end of file +} diff --git a/tests/Example/ExampleRecommendationEngine.php b/tests/Example/ExampleRecommendationEngine.php index 7b78326..fc94944 100644 --- a/tests/Example/ExampleRecommendationEngine.php +++ b/tests/Example/ExampleRecommendationEngine.php @@ -4,44 +4,44 @@ use GraphAware\Reco4PHP\Engine\BaseRecommendationEngine; use GraphAware\Reco4PHP\Tests\Example\Discovery\FromSameGenreILike; +use GraphAware\Reco4PHP\Tests\Example\Discovery\RatedByOthers; use GraphAware\Reco4PHP\Tests\Example\Filter\AlreadyRatedBlackList; use GraphAware\Reco4PHP\Tests\Example\Filter\ExcludeOldMovies; use GraphAware\Reco4PHP\Tests\Example\PostProcessing\RewardWellRated; -use GraphAware\Reco4PHP\Tests\Example\Discovery\RatedByOthers; class ExampleRecommendationEngine extends BaseRecommendationEngine { - public function name() + public function name(): string { - return "example"; + return 'user_movie_reco'; } - public function discoveryEngines() + public function discoveryEngines(): array { - return array( + return [ new RatedByOthers(), - new FromSameGenreILike() - ); + new FromSameGenreILike(), + ]; } - public function blacklistBuilders() + public function blacklistBuilders(): array { - return array( - new AlreadyRatedBlackList() - ); + return [ + new AlreadyRatedBlackList(), + ]; } - public function postProcessors() + public function postProcessors(): array { - return array( - new RewardWellRated() - ); + return [ + new RewardWellRated(), + ]; } - public function filters() + public function filters(): array { - return array( - new ExcludeOldMovies() - ); + return [ + new ExcludeOldMovies(), + ]; } -} \ No newline at end of file +} diff --git a/tests/Example/ExampleRecommenderService.php b/tests/Example/ExampleRecommenderService.php index f2e78ec..b7250d9 100644 --- a/tests/Example/ExampleRecommenderService.php +++ b/tests/Example/ExampleRecommenderService.php @@ -4,33 +4,26 @@ use GraphAware\Reco4PHP\Context\SimpleContext; use GraphAware\Reco4PHP\RecommenderService; +use GraphAware\Reco4PHP\Result\Recommendations; class ExampleRecommenderService { - /** - * @var \GraphAware\Reco4PHP\RecommenderService - */ - protected $service; + protected RecommenderService $service; /** * ExampleRecommenderService constructor. - * @param string $databaseUri */ - public function __construct($databaseUri) + public function __construct(string $databaseUri) { $this->service = RecommenderService::create($databaseUri); $this->service->registerRecommendationEngine(new ExampleRecommendationEngine()); } - /** - * @param int $id - * @return \GraphAware\Reco4PHP\Result\Recommendations - */ - public function recommendMovieForUserWithId($id) + public function recommendMovieForUserWithId(int $id): Recommendations { $input = $this->service->findInputBy('User', 'id', $id); - $recommendationEngine = $this->service->getRecommender("user_movie_reco"); + $recommendationEngine = $this->service->getRecommender('user_movie_reco'); return $recommendationEngine->recommend($input, new SimpleContext()); } -} \ No newline at end of file +} diff --git a/tests/Example/Filter/AlreadyRatedBlacklist.php b/tests/Example/Filter/AlreadyRatedBlackList.php similarity index 50% rename from tests/Example/Filter/AlreadyRatedBlacklist.php rename to tests/Example/Filter/AlreadyRatedBlackList.php index 47c28e9..d1217bb 100644 --- a/tests/Example/Filter/AlreadyRatedBlacklist.php +++ b/tests/Example/Filter/AlreadyRatedBlackList.php @@ -2,23 +2,23 @@ namespace GraphAware\Reco4PHP\Tests\Example\Filter; -use GraphAware\Common\Cypher\Statement; -use GraphAware\Common\Type\Node; use GraphAware\Reco4PHP\Filter\BaseBlacklistBuilder; +use Laudis\Neo4j\Databags\Statement; +use Laudis\Neo4j\Types\Node; class AlreadyRatedBlackList extends BaseBlacklistBuilder { - public function blacklistQuery(Node $input) + public function blacklistQuery(Node $input): Statement { - $query = 'MATCH (input) WHERE id(input) = {inputId} + $query = 'MATCH (input) WHERE id(input) = $inputId MATCH (input)-[:RATED]->(movie) RETURN movie as item'; - return Statement::create($query, ['inputId' => $input->identity()]); + return Statement::create($query, ['inputId' => $input->getId()]); } - public function name() + public function name(): string { return 'already_rated'; } -} \ No newline at end of file +} diff --git a/tests/Example/Filter/ExcludeOldMovies.php b/tests/Example/Filter/ExcludeOldMovies.php index 6f3b0d0..04ca250 100644 --- a/tests/Example/Filter/ExcludeOldMovies.php +++ b/tests/Example/Filter/ExcludeOldMovies.php @@ -2,19 +2,19 @@ namespace GraphAware\Reco4PHP\Tests\Example\Filter; -use GraphAware\Common\Type\Node; use GraphAware\Reco4PHP\Filter\Filter; +use Laudis\Neo4j\Types\Node; class ExcludeOldMovies implements Filter { - public function doInclude(Node $input, Node $item) + public function doInclude(Node $input, Node $item): bool { - $title = $item->value("title"); + $title = (string) $item->getProperty('title'); preg_match('/(?:\()\d+(?:\))/', $title, $matches); if (isset($matches[0])) { - $y = str_replace('(','',$matches[0]); - $y = str_replace(')','', $y); + $y = str_replace('(', '', $matches[0]); + $y = str_replace(')', '', $y); $year = (int) $y; if ($year < 1999) { return false; @@ -25,5 +25,4 @@ public function doInclude(Node $input, Node $item) return false; } - -} \ No newline at end of file +} diff --git a/tests/Example/PostProcessing/RewardWellRated.php b/tests/Example/PostProcessing/RewardWellRated.php index b009ba3..6e1589e 100644 --- a/tests/Example/PostProcessing/RewardWellRated.php +++ b/tests/Example/PostProcessing/RewardWellRated.php @@ -2,39 +2,38 @@ namespace GraphAware\Reco4PHP\Tests\Example\PostProcessing; -use GraphAware\Common\Cypher\Statement; -use GraphAware\Common\Result\Record; -use GraphAware\Common\Type\Node; use GraphAware\Reco4PHP\Post\RecommendationSetPostProcessor; use GraphAware\Reco4PHP\Result\Recommendation; use GraphAware\Reco4PHP\Result\Recommendations; use GraphAware\Reco4PHP\Result\SingleScore; +use Laudis\Neo4j\Databags\Statement; +use Laudis\Neo4j\Types\CypherMap; +use Laudis\Neo4j\Types\Node; class RewardWellRated extends RecommendationSetPostProcessor { - public function buildQuery(Node $input, Recommendations $recommendations) + public function buildQuery(Node $input, Recommendations $recommendations): Statement { - $query = 'UNWIND {ids} as id + $query = 'UNWIND $ids as id MATCH (n) WHERE id(n) = id MATCH (n)<-[r:RATED]-(u) RETURN id(n) as id, sum(r.rating) as score'; $ids = []; foreach ($recommendations->getItems() as $item) { - $ids[] = $item->item()->identity(); + $ids[] = $item->item()->getId(); } return Statement::create($query, ['ids' => $ids]); } - public function postProcess(Node $input, Recommendation $recommendation, Record $record) + public function postProcess(Node $input, Recommendation $recommendation, CypherMap $result): void { - $recommendation->addScore($this->name(), new SingleScore($record->get('score'), 'total_ratings_relationships')); + $recommendation->addScore($this->name(), new SingleScore((float) $result->get('score'), 'total_ratings_relationships')); } - public function name() + public function name(): string { - return "reward_well_rated"; + return 'reward_well_rated'; } - -} \ No newline at end of file +} diff --git a/tests/Helper/FakeNode.php b/tests/Helper/FakeNode.php index db4c2c2..dfdff20 100644 --- a/tests/Helper/FakeNode.php +++ b/tests/Helper/FakeNode.php @@ -2,81 +2,16 @@ namespace GraphAware\Reco4PHP\Tests\Helper; -use GraphAware\Common\Type\Node; +use Laudis\Neo4j\Types\CypherList; +use Laudis\Neo4j\Types\CypherMap; +use Laudis\Neo4j\Types\Node; -class FakeNode implements Node +class FakeNode { - protected $identity; - - protected $labels = []; - - function __get($name) - { - return null; - } - - - public function __construct($identity, array $labels = array()) - { - $this->identity = $identity; - $this->labels = $labels; - } - - public static function createDummy($id = null) - { - $identity = null !== $id ? $id : rand(0,1000); - return new self($identity, array("Dummy")); - } - - function identity() - { - return $this->identity; - } - - function labels() - { - return $this->labels; - } - - function hasLabel($label) - { - return in_array($label, $this->labels); - } - - public function keys() - { - // TODO: Implement keys() method. - } - - public function containsKey($key) - { - // TODO: Implement containsKey() method. - } - - public function get($key) - { - // TODO: Implement get() method. - } - - public function values() + public static function createDummy(?int $id = null) { - // TODO: Implement values() method. - } - - public function asArray() - { - // TODO: Implement asArray() method. - } - - public function hasValue($key) - { - // TODO: Implement hasValue() method. - } + $id = $id ?? rand(0, 1000); - public function value($key, $default = null) - { - // TODO: Implement value() method. + return new Node($id, new CypherList(['Dummy']), new CypherMap([])); } - - -} \ No newline at end of file +} diff --git a/tests/Integration/Model/FriendsEngine.php b/tests/Integration/Model/FriendsEngine.php index 01d145f..0a79b1d 100644 --- a/tests/Integration/Model/FriendsEngine.php +++ b/tests/Integration/Model/FriendsEngine.php @@ -2,27 +2,25 @@ namespace GraphAware\Reco4PHP\Tests\Integration\Model; -use GraphAware\Common\Cypher\Statement; -use GraphAware\Common\Type\Node; use GraphAware\Reco4PHP\Context\Context; use GraphAware\Reco4PHP\Engine\SingleDiscoveryEngine; -use GraphAware\Common\Cypher\StatementInterface; +use Laudis\Neo4j\Databags\Statement; +use Laudis\Neo4j\Types\Node; class FriendsEngine extends SingleDiscoveryEngine { - public function name() : string + public function name(): string { return 'friends_discovery'; } - public function discoveryQuery(Node $input, Context $context) : StatementInterface + public function discoveryQuery(Node $input, Context $context): Statement { - $query = 'MATCH (n) WHERE id(n) = {id} + $query = 'MATCH (n) WHERE id(n) = $id MATCH (n)-[:FRIEND]->(friend)-[:FRIEND]->(reco) WHERE NOT (n)-[:FRIEND]->(reco) RETURN reco, count(*) as score'; - return Statement::prepare($query, ['id' => $input->identity()]); + return Statement::create($query, ['id' => $input->getId()]); } - -} \ No newline at end of file +} diff --git a/tests/Integration/Model/RecoEngine.php b/tests/Integration/Model/RecoEngine.php index dcb2058..ea2e82b 100644 --- a/tests/Integration/Model/RecoEngine.php +++ b/tests/Integration/Model/RecoEngine.php @@ -3,28 +3,25 @@ namespace GraphAware\Reco4PHP\Tests\Integration\Model; use GraphAware\Reco4PHP\Engine\BaseRecommendationEngine; -use GraphAware\Reco4PHP\Filter\ExcludeSelf; class RecoEngine extends BaseRecommendationEngine { - public function name() : string + public function name(): string { return 'find_friends'; } - public function discoveryEngines() : array + public function discoveryEngines(): array { - return array( - new FriendsEngine() - ); + return [ + new FriendsEngine(), + ]; } - public function blacklistBuilders() : array + public function blacklistBuilders(): array { - return array( - new SimpleBlacklist() - ); + return [ + new SimpleBlacklist(), + ]; } - - -} \ No newline at end of file +} diff --git a/tests/Integration/Model/SimpleBlacklist.php b/tests/Integration/Model/SimpleBlacklist.php index 8b6c778..474abd1 100644 --- a/tests/Integration/Model/SimpleBlacklist.php +++ b/tests/Integration/Model/SimpleBlacklist.php @@ -2,22 +2,21 @@ namespace GraphAware\Reco4PHP\Tests\Integration\Model; -use GraphAware\Common\Cypher\Statement; -use GraphAware\Common\Type\Node; use GraphAware\Reco4PHP\Filter\BaseBlacklistBuilder; +use Laudis\Neo4j\Databags\Statement; +use Laudis\Neo4j\Types\Node; class SimpleBlacklist extends BaseBlacklistBuilder { - public function blacklistQuery(Node $input) + public function blacklistQuery(Node $input): Statement { $query = 'MATCH (n) WHERE n.name = "Zoe" RETURN n as item'; - return Statement::prepare($query, ['id' => $input->identity()]); + return Statement::create($query); } - public function name() + public function name(): string { return 'simple_blacklist'; } - -} \ No newline at end of file +} diff --git a/tests/Integration/SimpleFriendsRecoEngineTest.php b/tests/Integration/SimpleFriendsRecoEngineTest.php index 057ead7..03c2f9d 100644 --- a/tests/Integration/SimpleFriendsRecoEngineTest.php +++ b/tests/Integration/SimpleFriendsRecoEngineTest.php @@ -2,44 +2,36 @@ namespace GraphAware\Reco4PHP\Tests\Integration; -use GraphAware\Neo4j\Client\ClientBuilder; use GraphAware\Reco4PHP\Context\SimpleContext; -use GraphAware\Reco4PHP\Tests\Integration\Model\RecoEngine; +use GraphAware\Reco4PHP\Persistence\DatabaseService; use GraphAware\Reco4PHP\RecommenderService; -use GraphAware\Neo4j\Client\Client; +use GraphAware\Reco4PHP\Tests\Integration\Model\RecoEngine; +use Laudis\Neo4j\Types\Node; +use PHPUnit\Framework\TestCase; /** - * Class SimpleFriendsRecoEngineTest - * @package GraphAware\Reco4PHP\Tests\Integration + * Class SimpleFriendsRecoEngineTest. * * @group integration */ -class SimpleFriendsRecoEngineTest extends \PHPUnit_Framework_TestCase +class SimpleFriendsRecoEngineTest extends TestCase { - /** - * @var RecommenderService - */ - protected $recoService; + protected RecommenderService $recoService; - /** - * @var Client - */ - protected $client; + protected DatabaseService $databaseService; /** * @setUp() */ - public function setUp() + public function setUp(): void { - $this->recoService = RecommenderService::create('http://localhost:7474'); + $this->databaseService = new DatabaseService('bolt://localhost:7687'); + $this->recoService = new RecommenderService($this->databaseService); $this->recoService->registerRecommendationEngine(new RecoEngine()); - $this->client = ClientBuilder::create() - ->addConnection('default', 'http://localhost:7474') - ->build(); $this->createGraph(); } - public function testRecoForJohn() + public function testRecoForJohn(): void { $engine = $this->recoService->getRecommender('find_friends'); $john = $this->getUserNode('John'); @@ -47,23 +39,23 @@ public function testRecoForJohn() $recommendations->sort(); $this->assertEquals(2, $recommendations->size()); $this->assertNull($recommendations->getItemBy('name', 'John')); - $recoForMarc = $recommendations->getItemBy('name','marc'); + $recoForMarc = $recommendations->getItemBy('name', 'marc'); $this->assertEquals(1, $recoForMarc->totalScore()); $recoForBill = $recommendations->getItemBy('name', 'Bill'); $this->assertEquals(2, $recoForBill->totalScore()); } - private function getUserNode($name) + private function getUserNode(string $name): Node { - $q = 'MATCH (n:User) WHERE n.name = {name} RETURN n'; - $result = $this->client->run($q, ['name' => $name]); + $q = 'MATCH (n:User) WHERE n.name = $name RETURN n'; + $results = $this->databaseService->getDriver()->run($q, ['name' => $name]); - return $result->firstRecord()->get('n'); + return $results->first()->get('n'); } - private function createGraph() + private function createGraph(): void { - $this->client->run('MATCH (n) DETACH DELETE n'); + $this->databaseService->getDriver()->run('MATCH (n) DETACH DELETE n'); $query = 'CREATE (john:User {name:"John"})-[:FRIEND]->(judith:User {name:"Judith"}), (john)-[:FRIEND]->(paul:User {name:"paul"}), (paul)-[:FRIEND]->(marc:User {name:"marc"}), @@ -72,6 +64,6 @@ private function createGraph() (judith)-[:FRIEND]->(sofia), (john)-[:FRIEND]->(sofia), (sofia)-[:FRIEND]->(:User {name:"Zoe"})'; - $this->client->run($query); + $this->databaseService->getDriver()->run($query); } -} \ No newline at end of file +} diff --git a/tests/Result/RecommendationsListTest.php b/tests/Result/RecommendationsListTest.php index ec2b6a3..56444a4 100644 --- a/tests/Result/RecommendationsListTest.php +++ b/tests/Result/RecommendationsListTest.php @@ -4,19 +4,18 @@ use GraphAware\Reco4PHP\Context\SimpleContext; use GraphAware\Reco4PHP\Result\Recommendations; -use GraphAware\Reco4PHP\Result\Score; use GraphAware\Reco4PHP\Result\SingleScore; use GraphAware\Reco4PHP\Tests\Helper\FakeNode; +use PHPUnit\Framework\TestCase; /** - * Class RecommendationsListTest - * @package GraphAware\Reco4PHP\Tests\Result + * Class RecommendationsListTest. * * @group result */ -class RecommendationsListTest extends \PHPUnit_Framework_TestCase +class RecommendationsListTest extends TestCase { - public function testResultGetTwoScoresIfDiscoveredTwice() + public function testResultGetTwoScoresIfDiscoveredTwice(): void { $node = FakeNode::createDummy(); $list = new Recommendations(new SimpleContext()); @@ -31,7 +30,7 @@ public function testResultGetTwoScoresIfDiscoveredTwice() $this->assertArrayHasKey('e2', $list->get(0)->getScores()); } - public function testTotalScoreIsIncremented() + public function testTotalScoreIsIncremented(): void { $node = FakeNode::createDummy(); $list = new Recommendations(new SimpleContext()); @@ -43,7 +42,7 @@ public function testTotalScoreIsIncremented() $this->assertEquals(2, $reco->totalScore()); } - public function testReasons() + public function testReasons(): void { $node = FakeNode::createDummy(); $list = new Recommendations(new SimpleContext()); @@ -59,4 +58,4 @@ public function testReasons() $this->assertArrayHasKey('disc3', $reco->getScores()); $this->assertCount(10, $reco->getScore('disc3')->getScores()); } -} \ No newline at end of file +}