{"id":13529739,"url":"https://github.com/graphaware/neo4j-reco","last_synced_at":"2025-09-30T10:30:43.844Z","repository":{"id":21411450,"uuid":"24729476","full_name":"graphaware/neo4j-reco","owner":"graphaware","description":"Neo4j-based recommendation engine module with real-time and pre-computed recommendations.","archived":true,"fork":false,"pushed_at":"2021-05-05T19:26:05.000Z","size":919,"stargazers_count":374,"open_issues_count":6,"forks_count":77,"subscribers_count":60,"default_branch":"master","last_synced_at":"2024-09-27T21:41:46.237Z","etag":null,"topics":["graphaware-recommendation-engine","java","neo4j","neo4j-graphaware-framework","recommendation-engine"],"latest_commit_sha":null,"homepage":null,"language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/graphaware.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2014-10-02T17:41:36.000Z","updated_at":"2024-09-16T10:32:29.000Z","dependencies_parsed_at":"2022-08-20T21:50:13.835Z","dependency_job_id":null,"html_url":"https://github.com/graphaware/neo4j-reco","commit_stats":null,"previous_names":[],"tags_count":65,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/graphaware%2Fneo4j-reco","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/graphaware%2Fneo4j-reco/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/graphaware%2Fneo4j-reco/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/graphaware%2Fneo4j-reco/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/graphaware","download_url":"https://codeload.github.com/graphaware/neo4j-reco/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":234724892,"owners_count":18877279,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["graphaware-recommendation-engine","java","neo4j","neo4j-graphaware-framework","recommendation-engine"],"created_at":"2024-08-01T07:00:38.983Z","updated_at":"2025-09-30T10:30:43.275Z","avatar_url":"https://github.com/graphaware.png","language":"Java","funding_links":[],"categories":["Extensions","人工智能","REST API"],"sub_categories":["REST API","Other"],"readme":"GraphAware Neo4j Recommendation Engine - RETIRED\n======================================\n\n## GraphAware Neo4j Recommendation Engine Has Been Retired\nAs of May 2021, this [repository has been retired](https://graphaware.com/framework/2021/05/06/from-graphaware-framework-to-graphaware-hume.html).\n\n\nGraphAware Neo4j Recommendation Engine is a library for building high-performance complex recommendation engines atop Neo4j.\nIt is in production at a number of \u003ca href=\"http://graphaware.com\" target=\"_blank\"\u003eGraphAware\u003c/a\u003e's clients producing real-time recommendations on graphs with hundreds of millions of nodes.\n\nKey Features:\n\n* Clean and flexible design\n* High performance\n* Ability to trade off recommendation quality for speed\n* Ability to pre-compute recommendations\n* Built-in algorithms and functions\n* Ability to measure recommendation quality\n* Ability to easily run in A/B test environments\n\nThe library imposes a specific recommendation engine architecture, which has emerged from our experience building recommendation\nengines on top of Neo4j. In return, it offers high performance and handles most of the plumbing so that you only write\nthe recommendation business logic specific to your use case.\n\nBesides computing recommendations in real-time, it also allows for pre-computing recommendations that are perhaps too complex\nto compute in real-time. The pre-computing happens on best-effort basis during quiet periods, so that it does not interfere\nwith regular transaction processing that your Neo4j database is performing.\n\n## Community vs Enterprise\n\nThis open-source (GPL) version of the module is compatible with GraphAware Framework Community (GPL), which in turn \nis compatible with Neo4j Community Edition (GPL) only. It *will not work* with Neo4j Enterprise Edition, which is a \nproprietary and commercial software product of Neo4j, Inc..\n\nGraphAware offers an Enterprise version of the GraphAware Framework to licensed users of Neo4j Enterprise Edition.\nPlease [get in touch](mailto:info@graphaware.com) to receive access.\n\nGetting the Software\n--------------------\n\n### Server Mode\n\nWhen using Neo4j in the \u003ca href=\"http://docs.neo4j.org/chunked/stable/server-installation.html\" target=\"_blank\"\u003estandalone server\u003c/a\u003e mode,\nyou will need the \u003ca href=\"https://github.com/graphaware/neo4j-framework\" target=\"_blank\"\u003eGraphAware Neo4j Framework\u003c/a\u003e and GraphAware Neo4j Recommendation Engine .jar files (both of which you can \u003ca href=\"http://graphaware.com/products/\" target=\"_blank\"\u003edownload here\u003c/a\u003e) dropped\ninto the `plugins` directory of your Neo4j installation.\n\nUnlike with other GraphAware Framework Modules, you will need to write at least a few lines of your own Java code (read on).\n\n### Embedded Mode / Java Development\n\nJava developers that use Neo4j in \u003ca href=\"http://docs.neo4j.org/chunked/stable/tutorials-java-embedded.html\" target=\"_blank\"\u003eembedded mode\u003c/a\u003e\nand those developing Neo4j \u003ca href=\"http://docs.neo4j.org/chunked/stable/server-plugins.html\" target=\"_blank\"\u003eserver plugins\u003c/a\u003e,\n\u003ca href=\"http://docs.neo4j.org/chunked/stable/server-unmanaged-extensions.html\" target=\"_blank\"\u003eunmanaged extensions\u003c/a\u003e,\nGraphAware Runtime Modules, or Spring MVC Controllers can include the module as a dependency for their Java project.\n\n#### Releases\n\nReleases are synced to \u003ca href=\"http://search.maven.org/#search%7Cga%7C1%7Ca%3A%22reco%22\" target=\"_blank\"\u003eMaven Central repository\u003c/a\u003e. When using Maven for dependency management, include the following dependency in your pom.xml and edit the version number.\n\n    \u003cdependencies\u003e\n        ...\n        \u003cdependency\u003e\n            \u003cgroupId\u003ecom.graphaware.neo4j\u003c/groupId\u003e\n            \u003cartifactId\u003erecommendation-engine\u003c/artifactId\u003e\n            \u003cversion\u003eA.B.C.D.E\u003c/version\u003e\n        \u003c/dependency\u003e\n        ...\n    \u003c/dependencies\u003e\n\n#### Snapshots\n\nTo use the latest development version, just clone this repository, run `mvn clean install` and change the version in the\ndependency above to A.B.C.D.E-SNAPSHOT.\n\n#### Note on Versioning Scheme\n\nThe version number has two parts. The first four numbers indicate compatibility with Neo4j GraphAware Framework.\n The last number is the version of the Recommendation Engine library. For example, version 2.1.6.26.1 is version 1 of the Recommendation Engine\n compatible with GraphAware Neo4j Framework 2.1.6.26.\n\nIntroduction to GraphAware Recommendation Engine\n------------------------------------------------\n\nThe purpose of a recommendation engine is (unsurprisingly) to recommend something to users. This could be products they\nshould buy, users they should connect with, artists they should follow, etc. It turns out graph is a really good data\nstructure for representing users' interests, behaviours, and other characteristics that might be useful for finding\nrecommendations. More importantly, graph databases, and Neo4j especially, provide a natural way of expressing queries on\nthis data in order to find relevant recommendations, and executing these queries very fast.\n\nThere are three main challenges when building a recommendation engine. The first is to **discover** the items to recommend.\nThe second is to **choose the most relevant ones** to present to the user. Finally, the third challenge is to find relevant\nrecommendations **as quickly as possible**. Preferably, this should happen in real-time, i.e. using the most up to date information\nwe have. The last thing we want to do is to recommend something the user has already purchased, or a person we know she\nisn't interested in.\n\nThe first two points above are business rather than technical challenges. Typically, when you start building a recommendation\nengine, you have some idea about how the recommended items will be discovered. For instance, you might want to recommend\nitems that other people with similar interests have bought. You also know, which items you absolutely do not want to recommend,\nfor example, items the user has already purchased, or people that we know are married as a potential match for a date.\n\nThe issue with recommendation relevance is usually something that needs to be experimented with. When building the first\nrecommendation engine, or perhaps even a proof of concept, one feature that shouldn't be missing is the ability to configure\nhow the recommendation relevance is computed and, perhaps more importantly, measure how users react to recommendations\nproduced by different relevance-computing configurations.\n\nFinally, let's address the issue of speed, which is of a technical nature. When serving real-time recommendations, users\nshouldn't need to wait for more than, let's say, a couple hundred milliseconds. With Neo4j, we will be able to build many\ndifferent recommendation queries that take milliseconds to execute. However, there are situations (large graphs with some\nvery dense nodes) where we will need to take extra care in order not to slow the recommendation process down. Finally,\nin situations where the recommendation logic and the size of the graph simply don't allow real-time computation, we will\nneed to look at pre-computing some recommendations, whilst avoiding the dangers of serving out of date recommendations.\n\nRecommendation Engine Architecture\n----------------------------------\n\nThe architecture of GraphAware Neo4j Recommendation Engine has been designed to address, or easily allow you to address,\nall of the above challenges. The library works with the following concepts:\n\n#### Recommendation Engines and Recommendations\n\nA **Recommendation Engine** is a component that produces **Recommendations**, given an **Input**. Whilst the architecture is\ngeneric enough to support other persistence mechanisms, we focus on Neo4j and so the input will typically be a Neo4j `Node`\nrepresenting a user for whom we want to find recommendations, a product for which we want to find buyers, etc.\n\nA `RecommendationEngine`, as in the case of `DelegatingRecommendationEngine` can be composed of other `RecommendationEngine`s\nthat it delegates to. Usually, however, a `RecommendationEngine` will encapsulate the querying and relevance-computing\nlogic for discovering recommendations based on a single logical criterion. Such engine typically extends\n`SingleScoreRecommendationEngine`. For example, we could have one engine that discovers items a user may want to buy based\non what other users with similar tastes have bought. Another engine would discover items based on user's expressed\npreferences. Yet another one could discover items to be recommended based on what is currently trending.\n\nFor performance reasons as well as to achieve good encapsulation, `RecommendationEngines` are only concerned with discovering\nand scoring all potential recommendations, without caring about the fact that some recommendations discovered this way may\nnot be suitable, perhaps because the user has already purchased the discovered item. Removing irrelevant recommendations\nwill be discussed shortly.\n\n#### Scores and Score Transformers\n\n`Recommendations` are a collection of tuples/pairs, where each pair is composed of a recommended item (again, typically a `Node`)\nand associated relevance **Score**. The `Score` is composed of named **Partial Scores**. Each _Partial Score_ has a float\nvalue and optionally some extra details about how and why it has been computed that we with to expose to the users. Typically,\na single `SingleScoreRecommendationEngine`, as the name suggest, is responsible for a single _Partial Score_.\n\nWhen an item has been discovered as a potential recommendation by multiple `SingleScoreRecommendationEngine`s, its _Parial Scores_\nwill be tallied by the `Score` object. For example, an item that is currently trending and matches the user's preferred\ntastes will have a total relevance `Score` composed of two _Partial Scores_, one due to the fact that it is trending, and\nanother one because it is a preferred item.\n\nIn some cases, an item might be discovered multiple times by the same `SingleScoreRecommendationEngine`. For example,\nwe may have an engine that suggests people a user should be friends with based on the fact that they have some friends in\ncommon. Assuming an easy-to-imagine graph traversal that discovers these recommendations, a potential friend will be discovered\nthree times if he has three friends in common with the user we're computing recommendations for. However, each additional\nfriend in common might not bear the same relevance for the recommendation. Thus, each _Partial Score_ can have a\n**Score Transformer** applied to it. A ScoreTransformer can apply an arbitrary mathematical function to the _Partial Score_\ncomputed by a `SingleScoreRecommendationEngine`.\n\n#### Context\n\n`Recommendations` are always computed within a **Context**. Whilst each recommendation-computing process for a single input\nmight involve multiple `RecommendationEngine`s and other components, there is usually a single `Context` per computation\nthat encapsulates information relevant to the process. For example, the `Context` knows whether a potential\nrecommendation discovered by a `RecommendationEngine` is allowed\nto be served to the user. For each computation, a new `Context` is produced by `TopLevelRecommendationEngine`.\n\n#### Config\n\nThe `Context` also encapsulates a **Config** for each recommendation-computing process. This is a set of user-defined values.\nBy default, a `Config` knows how many recommendations should be produce and what is the maximum time the\nrecommendation-computing process should take. Optionally, arbitrary key-value pairs can be passed in, which is useful\n for scenarios when score values, rewards, penalties, and other variables should not be hard-coded and differ per computation.\n\n#### Blacklist Builders and Filters\n\nRather than requiring all `RecommendationEngine`s to know how to detect irrelevant recommendations (thus slowing the computation\ndown and scattering a single concern), the logic is centralised into **Blacklist Builders** and **Filters**. `BlacklistBuilder`s,\nas the name suggests, are responsible for building \"blacklists\" of items that must not be recommended for a given input.\n\nAssuming that the input is a `Node` representing a person, an example of a `BlacklistBuilder` could be `AlreadyPurchasedItems`\nwhich builds a blacklist of items that the person has already purchased. `BlacklistBuilder`s are most efficient in situations\nwhere a small number of irrelevant recommendations (let's say up to 100) can be discovered with a single query before the\nrecommendation process begins.\n\n`Filter`s, on the other hand, can tell whether a recommendation is relevant or not by looking at the recommendation\nitself once it has been discovered. An obvious example of a `Filter` could be a class called `ExcludeSelf`, which would\nmake sure that (for example) a recommended friend isn't the same `Node` that the recommendations are being computed for.\nAnother example of a `Filter` could be `ExcludeItemsOutOfStock`, or `ExcludeMarriedPeople`.\n\nBlacklists produced by `BlacklistBuilder`s and `Filter`s are typically passed to an instance of `Context` (usually\n`FilteringContext`), which uses them to exclude irrelevant recommendations.\n\n#### Post Processors\n\nIn the presence of \"supernodes\", i.e. nodes with disproportionately many relationships, it would too expensive to compute\nrecommendations using dedicated `RecommendationEngine`s. Imagine, for example, that we would like to boost the score of\npeople living in the same city as the person we're computing recommendations for. Rather than implementing a `RecommendationEngine`\nthat discovers all people living in the same city (which could be millions!), we can implement a **PostProcessor** which\nmodifies the score of already computed recommendations. In the example above, a `PostProcessor` called `RewardSameCity`\ncould add 50 points to each recommendation if the person we're recommending to and the recommended person live in the same city.\nIt is much quicker to perform this check for each recommendation than discovering all people living in the same city.\n\nOther examples of a `PostProcessor` could include `RewardSameGender`, `PenalizeAgeDifference`, etc.\n\n#### Pre-Computation\n\nOnce we've built a `RecommendationEngine`, we could use it to continuously\npre-compute recommendations when the database isn't busy, using \u003ca href=\"https://github.com/graphaware/neo4j-framework/tree/master/runtime#building-a-timer-driven-graphaware-runtime-module\" target=\"_blank\"\u003eGraphAware Timer-Driven Module\u003c/a\u003e. For each potential\ninput, we could pre-compute a number of recommendations and link them to the input using a _RECOMMEND_ relationship.\nWhen serving recommendations, we could read them directly from the database, rather than computing them in real-time.\nBlacklists and `Filter`s are still consulted in case the situation has changed since the time recommendations were pre-computed.\n\n#### Logging\n\nEach produced `Recommendation` has a String UUID, so that it can be uniquely identified. This is useful, for example,\nwhen we want to measure the quality of recommendations. We can store the `Score`s of different `Recommendation`s as well\nas how users reacted to them against their UUIDs. For this purpose, we can use a `Logger` implementation. A `Logger`\nrecords recommendations for later analysis. There are provided implementation for logging using slf4j, but you can create\nyour own to store the data in Cassandra or wherever you want.\n\nUsing GraphAware Neo4j Recommendation Engine\n--------------------------------------------\n\nThe best place to start is by having a look at the `ModuleIntegrationTest` class and the other classes it uses.\nAlso, the classes in this library have a decent \u003ca href=\"http://graphaware.com/site/reco/latest/apidocs/\" target=\"_blank\"\u003eJavadoc\u003c/a\u003e,\nwhich should help you get building your first recommendation engine. Feel free to get in touch for support (info@graphaware.com).\n\nWe will illustrate how easy it is to build a recommendation using an example. Let's say we have a graph of people, i.e.\n`Node`s with label `:Person`. Moreover, each `:Person` also has a `:Male` or a `:Female` label, and two properties: a `name` (String) and\nan `age` (integer). We will also have `Node`s with label `:City` and a `name` property.\n\nThe only two relationship types in our simple graph will be `FRIEND_OF` and `LIVES_IN` and we will assume friendships are mutual,\nthus ignore the direction of the `FRIEND_OF` relationship. A sample graph, expressed in Cypher, could look like this:\n\n```\n    CREATE\n    (m:Person:Male {name:'Michal', age:30}),\n    (d:Person:Female {name:'Daniela', age:20}),\n    (v:Person:Male {name:'Vince', age:40}),\n    (a:Person:Male {name:'Adam', age:30}),\n    (b:Person:Female {name:'Britney', age:12}),\n    (l:Person:Female {name:'Luanne', age:25}),\n    (c:Person:Male {name:'Christophe', age:60}),\n    (j:Person:Male {name:'Jim', age:40}),\n\n    (lon:City {name:'London'}),\n    (mum:City {name:'Mumbai'}),\n    (br:City {name:'Bruges'}),\n\n    (m)-[:FRIEND_OF]-\u003e(d),\n    (m)-[:FRIEND_OF]-\u003e(l),\n    (m)-[:FRIEND_OF]-\u003e(a),\n    (m)-[:FRIEND_OF]-\u003e(b),\n    (m)-[:FRIEND_OF]-\u003e(v),\n    (d)-[:FRIEND_OF]-\u003e(v),\n    (b)-[:FRIEND_OF]-\u003e(v),\n    (j)-[:FRIEND_OF]-\u003e(v),\n    (j)-[:FRIEND_OF]-\u003e(m),\n    (j)-[:FRIEND_OF]-\u003e(a),\n    (a)-[:LIVES_IN]-\u003e(lon),\n    (d)-[:LIVES_IN]-\u003e(lon),\n    (v)-[:LIVES_IN]-\u003e(lon),\n    (m)-[:LIVES_IN]-\u003e(lon),\n    (j)-[:LIVES_IN]-\u003e(lon),\n    (c)-[:LIVES_IN]-\u003e(br),\n    (b)-[:LIVES_IN]-\u003e(br),\n    (l)-[:LIVES_IN]-\u003e(mum);\n```\n\nOur intention will be recommending people a person should be friends with, based on the following requirements:\n\n1. The more friends in common two people have, the more likely it is they should become friends\n2. The difference between zero and one friends in common should be significant and each additional friend in common should\n increase the recommendation relevance by a smaller magnitude.\n3. If people live in the same city, the chance of them becoming friends increases\n4. If people are of the same gender, the chance of them becoming friends is greater than if they are of opposite genders\n5. The bigger the age difference between two people, the lower the chance they will become friends\n6. People should not be friends with themselves\n7. People who are already friends should not be recommended as potential friends\n8. Young users should not be recommended to anyone as potential friends. The definition of \"young\" should be configurable per computation\n9. If we don't have enough recommendations, we will recommend some random people, but only if there is enough time\n\nLet's start tackling the requirements one by one.\n\n### Real-Time Recommendations\n\n#### FriendsInCommon\n\nFirst, we will build a `RecommendationEngine` that finds recommendations based on friends in common. For each friend in\ncommon, the relevance score will increase by 1. Since this is a single-criterion `RecommendationEngine`, we will extend\n`SingleScoreRecommendationEngine` as follows:\n\n```java\n/**\n * {@link com.graphaware.reco.generic.engine.RecommendationEngine} that finds recommendation based on friends in common.\n */\npublic class FriendsInCommon extends SomethingInCommon {\n\n    @Override\n    protected String name() {\n        return \"friendsInCommon\";\n    }\n\n    @Override\n    protected RelationshipType getType() {\n        return Relationships.FRIEND_OF;\n    }\n\n    @Override\n    protected Direction getDirection() {\n        return Direction.BOTH;\n    }\n}\n```\n\nThe code above tackles requirement (1). Let's modify the code to account for requirement (2) as well by providing an\nexponential `ScoreTransformer`, called the `ParetoScoreTransformer`. Please read the Javadoc of the class to find out\nexactly how it works. For now, it is sufficient to say that it will transform the number of friends in common to a score\nwith a theoretical upper value of 100, with 80% of the total score being achieved by having 10 friends in common.\n\n```java\n/**\n * {@link com.graphaware.reco.generic.engine.RecommendationEngine} that finds recommendation based on friends in common.\n * \u003cp/\u003e\n * The score is increasing by Pareto function, achieving 80% score with 10 friends in common. The maximum score is 100.\n */\npublic class FriendsInCommon extends SomethingInCommon {\n\n    private ScoreTransformer scoreTransformer = new ParetoScoreTransformer(100, 10);\n\n    @Override\n    public String name() {\n        return \"friendsInCommon\";\n    }\n\n    @Override\n    protected ScoreTransformer scoreTransformer() {\n        return scoreTransformer;\n    }\n\n    @Override\n    protected RelationshipType getType() {\n        return Relationships.FRIEND_OF;\n    }\n\n    @Override\n    protected Direction getDirection() {\n        return BOTH;\n    }\n\n    @Override\n    protected Map\u003cString, Object\u003e details(Node thingInCommon, Relationship withInput, Relationship withOutput) {\n        return Collections.singletonMap(\"name\", thingInCommon.getProperty(\"name\"));\n    }\n}\n```\n\n#### FriendsInCommon\n\nWhilst we're at it, we will also build the other `SingleScoreRecommendationEngine` that we'll need to satisfy requirement (8).\nNotice that we are overriding the `participationPolicy` method to specify that this engine should only be employed if\nthere aren't enough results and there is time left.\n\n```java\n /**\n  * {@link com.graphaware.reco.neo4j.engine.RandomRecommendations} selecting random nodes with \"Person\" label.\n  */\n public class RandomPeople extends RandomRecommendations {\n\n     @Override\n     public String name() {\n         return \"random\";\n     }\n\n     @Override\n     protected NodeInclusionPolicy getPolicy() {\n         return new BaseNodeInclusionPolicy() {\n             @Override\n             public boolean include(Node node) {\n                 return node.hasLabel(DynamicLabel.label(\"Person\"));\n             }\n         };\n     }\n\n     @Override\n     public ParticipationPolicy\u003cNode, Node\u003e participationPolicy(Context context) {\n         return ParticipationPolicy.IF_MORE_RESULTS_NEEDED_AND_ENOUGH_TIME;\n     }\n }\n```\n\n#### RewardSameLocation and RewardSameLabels\n\nWe will tackle requirements (3) and (4) by implementing some `PostProcessors` rather than separate `RecommendationEngine`s.\nThe reason is mainly performance; we do not want to suggest everyone who lives in the same city or who is of the same gender.\nInstead, we will reward already discovered recommendations for living in the same city or being of the same gender, by\nthe following two classes:\n\n```java\n/**\n * Rewards same location by 10 points.\n */\npublic class RewardSameLocation extends RewardSomethingShared {\n\n    @Override\n    protected String name() {\n        return \"sameLocation\";\n    }\n\n    @Override\n    protected RelationshipType type() {\n        return LIVES_IN;\n    }\n\n    @Override\n    protected Direction direction() {\n        return OUTGOING;\n    }\n\n    @Override\n    protected PartialScore partialScore(Node recommendation, Node input, Node sharedThing) {\n        return new PartialScore(10, Collections.singletonMap(\"location\", sharedThing.getProperty(\"name\")));\n    }\n}\n```\n\n```java\n/**\n * Rewards same gender (exactly the same labels) by 10 points.\n */\npublic class RewardSameLabels extends BasePostProcessor\u003cNode, Node\u003e {\n\n    @Override\n    protected String name() {\n        return \"sameGender\";\n    }\n\n    @Override\n    protected void doPostProcess(Recommendations\u003cNode\u003e recommendations, Node input, Context\u003cNode, Node\u003e context) {\n        Label[] inputLabels = toArray(Label.class, input.getLabels());\n\n        for (Recommendation\u003cNode\u003e recommendation : recommendations.get()) {\n            if (Arrays.equals(inputLabels, toArray(Label.class, recommendation.getItem().getLabels()))) {\n                recommendation.add(name(), 10);\n            }\n        }\n    }\n}\n```\n\nPlease note that we have chosen to provide the shared location's name as details to `PartialScore`, so that it can\nbe eventually exposed to users.\n\n#### PenalizeAgeDifference\n\nAnother `PostProcessor` will take care of requirement (5). We will subtract a maximum of 10 points from the relevance\nscore with 80% being subtracted when the difference in age is 20 years.\n\n```java\n/**\n * Subtracts points for difference in age. The maximum number of points subtracted is 10 and 80% of that is achieved\n * when the difference is 20 years.\n */\npublic class PenalizeAgeDifference extends BasePostProcessor\u003cNode, Node\u003e {\n\n    private final TransformationFunction function = new ParetoFunction(10, 20);\n\n    @Override\n    protected String name() {\n        return \"ageDifference\";\n    }\n\n    @Override\n    protected void doPostProcess(Recommendations\u003cNode\u003e recommendations, Node input, Context\u003cNode, Node\u003e context) {\n        int age = getInt(input, \"age\", 40);\n\n        for (Recommendation\u003cNode\u003e reco : recommendations.get()) {\n            int diff = Math.abs(getInt(reco.getItem(), \"age\", 40) - age);\n            reco.add(name(), -function.transform(diff));\n        }\n    }\n}\n\n```\n\n#### Blacklist Builders and Filters\n\nWe could build custom `BlacklistBuilder`s and `Filter`s as well to satisfy requirements (6) and (7), but we will just use classes\nalready provided by the library, as we will see shortly.\n\n#### Putting it all together\n\nNow that we have all the components that satisfy all 8 requirements, we just need to combine them into a `TopLevelRecommendationEngine`:\n\n```java\n/**\n * {@link com.graphaware.reco.neo4j.engine.Neo4jTopLevelDelegatingRecommendationEngine} that computes friend recommendations.\n */\npublic class FriendsComputingEngine extends Neo4jTopLevelDelegatingRecommendationEngine {\n\n    @Override\n    protected List\u003cRecommendationEngine\u003cNode, Node\u003e\u003e engines() {\n        return Arrays.\u003cRecommendationEngine\u003cNode, Node\u003e\u003easList(\n                new FriendsInCommon(),\n                new RandomPeople()\n        );\n    }\n\n    @Override\n    protected List\u003cPostProcessor\u003cNode, Node\u003e\u003e postProcessors() {\n        return Arrays.\u003cPostProcessor\u003cNode, Node\u003e\u003easList(\n                new RewardSameLabels(),\n                new RewardSameLocation(),\n                new PenalizeAgeDifference()\n        );\n    }\n\n    @Override\n    protected List\u003cBlacklistBuilder\u003cNode, Node\u003e\u003e blacklistBuilders() {\n        return Arrays.\u003cBlacklistBuilder\u003cNode, Node\u003e\u003easList(\n                new ExistingRelationshipBlacklistBuilder(FRIEND_OF, BOTH)\n        );\n    }\n\n    @Override\n    protected List\u003cFilter\u003cNode, Node\u003e\u003e filters() {\n        return Arrays.\u003cFilter\u003cNode, Node\u003e\u003easList(\n                new ExcludeSelf()\n        );\n    }\n}\n```\n\n#### A quick integration test\n\nIn this example, we have neglected unit testing altogether, which, of course, you shouldn't do. We will build a simple\nintegration test though in order to smoke-test our brand new recommendation engine.\n\n```java\npublic class ModuleIntegrationTest extends WrappingServerIntegrationTest {\n\n    private Neo4jTopLevelDelegatingEngine recommendationEngine;\n    private RecommendationsRememberingLogger rememberingLogger = new RecommendationsRememberingLogger();\n\n    @Override\n    public void setUp() throws Exception {\n        super.setUp();\n        recommendationEngine = new FriendsRecommendationEngine();\n        rememberingLogger.clear();\n    }\n\n    @Override\n    protected void populateDatabase(GraphDatabaseService database) {\n        database.execute(\n                \"CREATE \" +\n                        \"(m:Person:Male {name:'Michal', age:30}),\" +\n                        \"(d:Person:Female {name:'Daniela', age:20}),\" +\n                        \"(v:Person:Male {name:'Vince', age:40}),\" +\n                        \"(a:Person:Male {name:'Adam', age:30}),\" +\n                        \"(l:Person:Female {name:'Luanne', age:25}),\" +\n                        \"(b:Person:Male {name:'Christophe', age:60}),\" +\n                        \"(j:Person:Male {name:'Jim', age:38}),\" +\n\n                        \"(lon:City {name:'London'}),\" +\n                        \"(mum:City {name:'Mumbai'}),\" +\n                        \"(br:City {name:'Bruges'}),\" +\n\n                        \"(m)-[:FRIEND_OF]-\u003e(d),\" +\n                        \"(m)-[:FRIEND_OF]-\u003e(l),\" +\n                        \"(m)-[:FRIEND_OF]-\u003e(a),\" +\n                        \"(m)-[:FRIEND_OF]-\u003e(v),\" +\n                        \"(d)-[:FRIEND_OF]-\u003e(v),\" +\n                        \"(b)-[:FRIEND_OF]-\u003e(v),\" +\n                        \"(j)-[:FRIEND_OF]-\u003e(v),\" +\n                        \"(j)-[:FRIEND_OF]-\u003e(m),\" +\n                        \"(j)-[:FRIEND_OF]-\u003e(a),\" +\n                        \"(a)-[:LIVES_IN]-\u003e(lon),\" +\n                        \"(d)-[:LIVES_IN]-\u003e(lon),\" +\n                        \"(v)-[:LIVES_IN]-\u003e(lon),\" +\n                        \"(m)-[:LIVES_IN]-\u003e(lon),\" +\n                        \"(j)-[:LIVES_IN]-\u003e(lon),\" +\n                        \"(c)-[:LIVES_IN]-\u003e(br),\" +\n                        \"(l)-[:LIVES_IN]-\u003e(mum)\");\n    }\n\n    @Test\n    public void shouldRecommendRealTime() {\n        try (Transaction tx = getDatabase().beginTx()) {\n\n            //verify Vince\n\n            List\u003cRecommendation\u003cNode\u003e\u003e recoForVince = recommendationEngine.recommend(getPersonByName(\"Vince\"), new SimpleConfig(2));\n\n            String expectedForVince = \"Computed recommendations for Vince: (Adam {total:41.99417, ageDifference:-5.527864, friendsInCommon: {value:27.522034, {value:1.0, name:Jim}, {value:1.0, name:Michal}}, sameGender:10.0, sameLocation: {value:10.0, {value:10.0, location:London}}}), (Luanne {total:7.856705, ageDifference:-7.0093026, friendsInCommon: {value:14.866008, {value:1.0, name:Michal}}})\";\n\n            assertEquals(expectedForVince, rememberingLogger.toString(getPersonByName(\"Vince\"), recoForVince, null));\n            assertEquals(expectedForVince, rememberingLogger.get(getPersonByName(\"Vince\")));\n\n            //verify Adam\n\n            List\u003cRecommendation\u003cNode\u003e\u003e recoForAdam = recommendationEngine.recommend(getPersonByName(\"Adam\"), new SimpleConfig(2));\n\n            String expectedForAdam = \"Computed recommendations for Adam: (Vince {total:41.99417, ageDifference:-5.527864, friendsInCommon: {value:27.522034, {value:1.0, name:Jim}, {value:1.0, name:Michal}}, sameGender:10.0, sameLocation: {value:10.0, {value:10.0, location:London}}}), (Daniela {total:19.338144, ageDifference:-5.527864, friendsInCommon: {value:14.866008, {value:1.0, name:Michal}}, sameLocation: {value:10.0, {value:10.0, location:London}}})\";\n\n            assertEquals(expectedForAdam, rememberingLogger.toString(getPersonByName(\"Adam\"), recoForAdam, null));\n            assertEquals(expectedForAdam, rememberingLogger.get(getPersonByName(\"Adam\")));\n\n            //verify Luanne\n\n            List\u003cRecommendation\u003cNode\u003e\u003e recoForLuanne = recommendationEngine.recommend(getPersonByName(\"Luanne\"), new SimpleConfig(4));\n\n            assertEquals(\"Daniela\", recoForLuanne.get(0).getItem().getProperty(\"name\"));\n            assertEquals(22, recoForLuanne.get(0).getScore().getTotalScore(), 0.5);\n\n            assertEquals(\"Adam\", recoForLuanne.get(1).getItem().getProperty(\"name\"));\n            assertEquals(12, recoForLuanne.get(1).getScore().getTotalScore(), 0.5);\n\n            assertEquals(\"Jim\", recoForLuanne.get(2).getItem().getProperty(\"name\"));\n            assertEquals(8, recoForLuanne.get(2).getScore().getTotalScore(), 0.5);\n\n            assertEquals(\"Vince\", recoForLuanne.get(3).getItem().getProperty(\"name\"));\n            assertEquals(8, recoForLuanne.get(3).getScore().getTotalScore(), 0.5);\n\n            tx.success();\n        }\n    }\n\n    private Node getPersonByName(String name) {\n        return getDatabase().findNode(DynamicLabel.label(\"Person\"), \"name\", name);\n    }\n}\n```\n\n### Pre-Computed Recommendations\n\nWith `FriendsComputingEngine`, we have a full-blown recommendation engine and could have stopped right there. However,\nwe would like to demonstrate the capability of using the very same engine to pre-compute recommendations.\n\nIt is worth mentioning that in this simple example, the exact same recommendations will be pre-computed as would have\nbeen computed in real-time. However, in real-life scenarios, `RecommendationEngine`s can choose to perform a quicker\ncomputation in real-time scenarios, but take a more accurate and slower approach for batch computations. The information\nabout how long a computation can take can be passed into the `recommend` method of a `RecommendationEngine` as another\nparameter. It is then available from the `Context` object, where we can also find the total time already elapsed.\n\n#### Pre-Computing\n\nIn order for our `FriendsComputingEngine` to be used to pre-compute recommendations when the database isn't busy, the\nonly thing we need to do is modify *neo4j.properties*. We're assuming that we are running in server mode and that the\nthe following .jar files have been placed into the _plugins_ directory of your Neo4j installation:\n\n* GraphAware Framework Server (Community / Enterprise)\n* GraphAware Neo4j Reco (this library)\n* Your code developed as part of this tutorial\n\nAdd the following lines to *neo4j.properties*:\n\n```\n#Enable GraphAware Runtime\ncom.graphaware.runtime.enabled=true\n\n#Register the Recommendation Module\ncom.graphaware.module.reco.1=com.graphaware.reco.neo4j.module.RecommendationModuleBootstrapper\n\n#Express for which nodes recommendations should be computed\ncom.graphaware.module.reco.node=hasLabel('Person')\n\n#Define which Recommendation Engine to use\ncom.graphaware.module.reco.engine=com.graphaware.reco.integration.FriendsComputingEngine\n\n#Optionally, specify how many recommendation to compute (default is 10)\ncom.graphaware.module.reco.maxRecommendations=5\n\n#Optionally, specify the Relationship Type of the relationship linking people with their recommended friends (default is RECOMMEND)\ncom.graphaware.module.reco.relationshipType=RECOMMEND\n```\n\nThat's all. You can tweak how often the pre-computation kicks in and what it means for your database to be busy. Please\nrefer to the documentation of \u003ca href=\"https://github.com/graphaware/neo4j-framework/tree/master/runtime#building-a-timer-driven-graphaware-runtime-module\" target=\"_blank\"\u003eGraphAware Timer-Driven Modules\u003c/a\u003e to learn how to do that.\n\n#### Using Pre-Computed Recommendations\n\nIn order for the pre-computed recommendations to be served first, before we start computing them in real-time, we need\nto make a few tweaks to our recommendation engine setup. First, we will override one more method in `FriendsComputingEngine`\nin order to indicate that it should only be used if there aren't enough pre-computed recommendations:\n\n```java\n/**\n * {@link com.graphaware.reco.neo4j.engine.Neo4jTopLevelDelegatingRecommendationEngine} that computes friend recommendations.\n */\npublic class FriendsComputingEngine extends Neo4jTopLevelDelegatingEngine {\n\n    @Override\n    protected List\u003cRecommendationEngine\u003cNode, Node\u003e\u003e engines() {\n        return Arrays.\u003cRecommendationEngine\u003cNode, Node\u003e\u003easList(\n                new FriendsInCommon(),\n                new RandomPeople()\n        );\n    }\n\n    @Override\n    protected List\u003cPostProcessor\u003cNode, Node\u003e\u003e postProcessors() {\n        return Arrays.\u003cPostProcessor\u003cNode, Node\u003e\u003easList(\n                new RewardSameLabels(),\n                new RewardSameLocation(),\n                new PenalizeAgeDifference()\n        );\n    }\n\n    @Override\n    protected List\u003cBlacklistBuilder\u003cNode, Node\u003e\u003e blacklistBuilders() {\n        return Arrays.\u003cBlacklistBuilder\u003cNode, Node\u003e\u003easList(\n                new ExistingRelationshipBlacklistBuilder(FRIEND_OF, BOTH)\n        );\n    }\n\n    @Override\n    protected List\u003cFilter\u003cNode, Node\u003e\u003e filters() {\n        return Arrays.\u003cFilter\u003cNode, Node\u003e\u003easList(\n                new ExcludeSelf()\n        );\n    }\n\n    @Override\n    public ParticipationPolicy\u003cNode, Node\u003e participationPolicy(Context\u003cNode, Node\u003e context) {\n        return ParticipationPolicy.IF_MORE_RESULTS_NEEDED;\n    }\n}\n```\n\nFinally, we need a new top-level `RecommendationEngine` that is exposed to our controllers or whatever component of your\napplication is consuming the recommendations. The new top-level engine will first delegate to a `Neo4jPrecomputedEngine`,\nthen to our `FriendsComputingEngine`. `BlacklistBuilder`s and `Filter`s have to be provided to this engine as well, because\nit will now be responsible for constructing `Context`s, since it is a top-level engine.\n\n```java\n/**\n * {@link com.graphaware.reco.neo4j.engine.Neo4jTopLevelDelegatingRecommendationEngine} that recommends friends by first trying to\n * read pre-computed recommendations from the graph, then (if there aren't enough results) by computing the friends in\n * real-time using {@link FriendsComputingEngine}.\n */\npublic final class FriendsRecommendationEngine extends Neo4jTopLevelDelegatingEngine {\n\n    @Override\n    protected List\u003cRecommendationEngine\u003cNode, Node\u003e\u003e engines() {\n        return Arrays.\u003cRecommendationEngine\u003cNode, Node\u003e\u003easList(\n                new Neo4jPrecomputedEngine(),\n                new FriendsComputingEngine()\n        );\n    }\n\n    @Override\n    protected List\u003cBlacklistBuilder\u003cNode, Node\u003e\u003e blacklistBuilders() {\n        return Arrays.asList(\n                new ExistingRelationshipBlacklistBuilder(FRIEND_OF, BOTH)\n        );\n    }\n\n    @Override\n    protected List\u003cFilter\u003cNode, Node\u003e\u003e filters() {\n        return Arrays.\u003cFilter\u003cNode, Node\u003e\u003easList(\n                new ExcludeSelf()\n        );\n    }\n}\n```\n\n### Logging\n\nIn order to record produced recommendations, you can add provided or your own `Logger` implementations to the top-level\nengine, e.g.:\n\n```java\n/**\n * {@link com.graphaware.reco.neo4j.engine.Neo4jTopLevelDelegatingRecommendationEngine} that recommends friends by first trying to\n * read pre-computed recommendations from the graph, then (if there aren't enough results) by computing the friends in\n * real-time using {@link FriendsComputingEngine}.\n */\npublic final class FriendsRecommendationEngine extends Neo4jTopLevelDelegatingRecommendationEngine {\n\n    @Override\n    protected List\u003cRecommendationEngine\u003cNode, Node\u003e\u003e engines() {\n        return Arrays.\u003cRecommendationEngine\u003cNode, Node\u003e\u003easList(\n                new Neo4jPrecomputedEngine(),\n                new FriendsComputingEngine()\n        );\n    }\n\n    @Override\n    protected List\u003cBlacklistBuilder\u003cNode, Node\u003e\u003e blacklistBuilders() {\n        return Arrays.asList(\n                new ExistingRelationshipBlacklistBuilder(FRIEND_OF, BOTH)\n        );\n    }\n\n    @Override\n    protected List\u003cFilter\u003cNode, Node\u003e\u003e filters() {\n        return Arrays.\u003cFilter\u003cNode, Node\u003e\u003easList(\n                new ExcludeSelf()\n        );\n    }\n\n    @Override\n    protected List\u003cLogger\u003cNode, Node\u003e\u003e loggers() {\n        return Arrays.\u003cLogger\u003cNode, Node\u003e\u003easList(\n                new Slf4jRecommendationLogger\u003cNode, Node\u003e(),\n                new Slf4jStatisticsLogger\u003cNode, Node\u003e()\n        );\n    }\n}\n```\n\nJob done!\n\nLicense\n-------\n\nCopyright (c) 2020 GraphAware\n\nGraphAware is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License\nas published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\nThis program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied\nwarranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\nYou should have received a copy of the GNU General Public License along with this program.\nIf not, see \u003chttp://www.gnu.org/licenses/\u003e.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgraphaware%2Fneo4j-reco","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgraphaware%2Fneo4j-reco","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgraphaware%2Fneo4j-reco/lists"}