You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by sp...@apache.org on 2016/10/13 15:45:26 UTC

tinkerpop git commit: Add "recommendation" recipe CTR

Repository: tinkerpop
Updated Branches:
  refs/heads/master a250be507 -> b7cf48e65


Add "recommendation" recipe CTR


Project: http://git-wip-us.apache.org/repos/asf/tinkerpop/repo
Commit: http://git-wip-us.apache.org/repos/asf/tinkerpop/commit/b7cf48e6
Tree: http://git-wip-us.apache.org/repos/asf/tinkerpop/tree/b7cf48e6
Diff: http://git-wip-us.apache.org/repos/asf/tinkerpop/diff/b7cf48e6

Branch: refs/heads/master
Commit: b7cf48e653344a6bfb6548aec7b063b7ca297381
Parents: a250be5
Author: Stephen Mallette <sp...@genoprime.com>
Authored: Thu Oct 13 11:42:40 2016 -0400
Committer: Stephen Mallette <sp...@genoprime.com>
Committed: Thu Oct 13 11:45:11 2016 -0400

----------------------------------------------------------------------
 CHANGELOG.asciidoc                            |   1 +
 docs/preprocessor/awk/init-code-blocks.awk    | 108 +++++-----
 docs/src/index.asciidoc                       |   2 +
 docs/src/recipes/index.asciidoc               |   2 +
 docs/src/recipes/recommendation.asciidoc      | 225 +++++++++++++++++++++
 docs/src/recipes/shortest-path.asciidoc       |   2 -
 docs/static/images/gremlin-recommendation.png | Bin 0 -> 61405 bytes
 docs/static/images/recommendation-alice-1.png | Bin 0 -> 11028 bytes
 docs/static/images/recommendation-alice-2.png | Bin 0 -> 28186 bytes
 docs/static/images/recommendation-alice-3.png | Bin 0 -> 54869 bytes
 docs/static/images/recommendation-alice-4.png | Bin 0 -> 46685 bytes
 11 files changed, 285 insertions(+), 55 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/b7cf48e6/CHANGELOG.asciidoc
----------------------------------------------------------------------
diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index 714abc5..37c6da4 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -89,6 +89,7 @@ TinkerPop 3.2.3 (Release Date: NOT OFFICIALLY RELEASED YET)
 * New build options for `gremlin-python` where `-DglvPython` is no longer required.
 * Added missing `InetAddress` to GraphSON extension module.
 * Added new recipe for "Pagination".
+* Added new recipe for "Recommendation".
 * Added functionality to Gremlin-Server REST endpoint to forward Exception Messages and Class in HTTP Response
 * Gremlin Server `TraversalOpProcessor` now returns confirmation upon `Op` `close`.
 * Added `close` method Java driver and Python driver `DriverRemoteTraversalSideEffects`.

http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/b7cf48e6/docs/preprocessor/awk/init-code-blocks.awk
----------------------------------------------------------------------
diff --git a/docs/preprocessor/awk/init-code-blocks.awk b/docs/preprocessor/awk/init-code-blocks.awk
index 381c231..4f1c184 100644
--- a/docs/preprocessor/awk/init-code-blocks.awk
+++ b/docs/preprocessor/awk/init-code-blocks.awk
@@ -37,59 +37,61 @@ BEGIN {
   print d[1] "; '[source," lang "]'"
   print "'+EVALUATED'"
   print "'+IGNORE'"
-  if (graph) {
-    print "graph = TinkerFactory.create" capitalize(graph) "()"
-  } else {
-    print "graph = TinkerGraph.open()"
-  }
-  print "g = graph.traversal()"
-  print "marko = g.V().has('name', 'marko').tryNext().orElse(null)"
-  print "f = new File('/tmp/neo4j')"
-  print "if (f.exists()) f.deleteDir()"
-  print "f = new File('/tmp/tinkergraph.kryo')"
-  print "if (f.exists()) f.deleteDir()"
-  print ":set max-iteration 100"
-  if (lang == "python") {
-    print "import static javax.script.ScriptContext.*"
-    print "import org.apache.tinkerpop.gremlin.python.jsr223.JythonTranslator"
-    print "jython = new org.apache.tinkerpop.gremlin.python.jsr223.GremlinJythonScriptEngine()"
-    print "jython.eval('import os')"
-    print "jython.eval('os.chdir(\"" TP_HOME "\")')"
-    print "jython.eval('import sys')"
-    print "jython.eval('sys.path.append(\"" PYTHONPATH "\")')"
-    print "jython.eval('sys.path.append(\"" TP_HOME "/gremlin-python/target/test-classes/Lib\")')"
-    print "jython.eval('from gremlin_python import statics')"
-    print "jython.eval('from gremlin_python.process.traversal import *')"
-    print "jython.eval('from gremlin_python.process.strategies import *')"
-    print "jython.eval('from gremlin_python.structure.graph import Graph')"
-    print "jython.eval('from gremlin_python.structure.io.graphson import GraphSONWriter')"
-    print "jython.eval('from gremlin_python.structure.io.graphson import GraphSONReader')"
-    # print "jython.eval('from gremlin_python.structure.io.graphson import serializers')"
-    # print "jython.eval('from gremlin_python.driver.driver_remote_connection import DriverRemoteConnection')"
-    print "jython.eval('statics.load_statics(globals())')"
-    print "jythonBindings = jython.createBindings()"
-    print "jythonBindings.put('g', jython.eval('Graph().traversal()'))"
-    print "jythonBindings.put('h', g)"
-    print "jython.getContext().setBindings(jythonBindings, GLOBAL_SCOPE)"
-    print "groovy = new org.apache.tinkerpop.gremlin.groovy.jsr223.GremlinGroovyScriptEngine()"
-    print "groovyBindings = groovy.createBindings()"
-    print "groovyBindings.put('g', g)"
-    print "groovyBindings.put('TinkerGraphComputer', TinkerGraphComputer)"
-    print "groovy.getContext().setBindings(groovyBindings, GLOBAL_SCOPE)"
-    print "def processTraversal(t, jython, groovy) {"
-    print "  jython.getContext().getBindings(GLOBAL_SCOPE).put('j', jython.eval(t.replace('.toList()','')))"
-    print "  if(jython.eval('isinstance(j, Traversal)')) {"
-    print "    mapper = GraphSONMapper.build().version(GraphSONVersion.V2_0).create().createMapper()"
-    print "    bytecode = mapper.readValue(jython.eval('GraphSONWriter().writeObject(j)').toString(), Bytecode.class)"
-    print "    language = BytecodeHelper.getLambdaLanguage(bytecode).orElse('gremlin-groovy')"
-    print "    result = language.equals('gremlin-groovy') ? groovy.eval(GroovyTranslator.of(\"g\").translate(bytecode) + '.toList()').toString() : jython.eval(JythonTranslator.of(\"h\").translate(bytecode) + '.toList()').toString()"
-    print "    jython.getContext().getBindings(GLOBAL_SCOPE).put('json', mapper.writeValueAsString(result))"
-    print "    return jython.eval('GraphSONReader().readObject(json)').toString()"
-    print "  } else {"
-    print "    j = jython.getContext().getBindings(GLOBAL_SCOPE).get('j')"
-    print "    return null == j ? 'null' : j.toString()"
-    print "  }"
-    print "}"
+  if (graph != "existing") {
+    if (graph) {
+      print "graph = TinkerFactory.create" capitalize(graph) "()"
+    } else {
+      print "graph = TinkerGraph.open()"
+    }
+    print "g = graph.traversal()"
+    print "marko = g.V().has('name', 'marko').tryNext().orElse(null)"
+    print "f = new File('/tmp/neo4j')"
+    print "if (f.exists()) f.deleteDir()"
+    print "f = new File('/tmp/tinkergraph.kryo')"
+    print "if (f.exists()) f.deleteDir()"
+    print ":set max-iteration 100"
+    if (lang == "python") {
+      print "import static javax.script.ScriptContext.*"
+      print "import org.apache.tinkerpop.gremlin.python.jsr223.JythonTranslator"
+      print "jython = new org.apache.tinkerpop.gremlin.python.jsr223.GremlinJythonScriptEngine()"
+      print "jython.eval('import os')"
+      print "jython.eval('os.chdir(\"" TP_HOME "\")')"
+      print "jython.eval('import sys')"
+      print "jython.eval('sys.path.append(\"" PYTHONPATH "\")')"
+      print "jython.eval('sys.path.append(\"" TP_HOME "/gremlin-python/target/test-classes/Lib\")')"
+      print "jython.eval('from gremlin_python import statics')"
+      print "jython.eval('from gremlin_python.process.traversal import *')"
+      print "jython.eval('from gremlin_python.process.strategies import *')"
+      print "jython.eval('from gremlin_python.structure.graph import Graph')"
+      print "jython.eval('from gremlin_python.structure.io.graphson import GraphSONWriter')"
+      print "jython.eval('from gremlin_python.structure.io.graphson import GraphSONReader')"
+      # print "jython.eval('from gremlin_python.structure.io.graphson import serializers')"
+      # print "jython.eval('from gremlin_python.driver.driver_remote_connection import DriverRemoteConnection')"
+      print "jython.eval('statics.load_statics(globals())')"
+      print "jythonBindings = jython.createBindings()"
+      print "jythonBindings.put('g', jython.eval('Graph().traversal()'))"
+      print "jythonBindings.put('h', g)"
+      print "jython.getContext().setBindings(jythonBindings, GLOBAL_SCOPE)"
+      print "groovy = new org.apache.tinkerpop.gremlin.groovy.jsr223.GremlinGroovyScriptEngine()"
+      print "groovyBindings = groovy.createBindings()"
+      print "groovyBindings.put('g', g)"
+      print "groovyBindings.put('TinkerGraphComputer', TinkerGraphComputer)"
+      print "groovy.getContext().setBindings(groovyBindings, GLOBAL_SCOPE)"
+      print "def processTraversal(t, jython, groovy) {"
+      print "  jython.getContext().getBindings(GLOBAL_SCOPE).put('j', jython.eval(t.replace('.toList()','')))"
+      print "  if(jython.eval('isinstance(j, Traversal)')) {"
+      print "    mapper = GraphSONMapper.build().version(GraphSONVersion.V2_0).create().createMapper()"
+      print "    bytecode = mapper.readValue(jython.eval('GraphSONWriter().writeObject(j)').toString(), Bytecode.class)"
+      print "    language = BytecodeHelper.getLambdaLanguage(bytecode).orElse('gremlin-groovy')"
+      print "    result = language.equals('gremlin-groovy') ? groovy.eval(GroovyTranslator.of(\"g\").translate(bytecode) + '.toList()').toString() : jython.eval(JythonTranslator.of(\"h\").translate(bytecode) + '.toList()').toString()"
+      print "    jython.getContext().getBindings(GLOBAL_SCOPE).put('json', mapper.writeValueAsString(result))"
+      print "    return jython.eval('GraphSONReader().readObject(json)').toString()"
+      print "  } else {"
+      print "    j = jython.getContext().getBindings(GLOBAL_SCOPE).get('j')"
+      print "    return null == j ? 'null' : j.toString()"
+      print "  }"
+      print "}"
+    }
   }
   print "'-IGNORE'"
 }

http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/b7cf48e6/docs/src/index.asciidoc
----------------------------------------------------------------------
diff --git a/docs/src/index.asciidoc b/docs/src/index.asciidoc
index 09ffc2f..d122529 100644
--- a/docs/src/index.asciidoc
+++ b/docs/src/index.asciidoc
@@ -68,6 +68,8 @@ Publications
 
 Unless otherwise noted, all "publications" are externally managed:
 
+* Rodriguez, M.A., link:http://www.datastax.com/dev/blog/gremlins-time-machine["Gremlin's Time Machine,"] DataStax Engineering Blog, September 2016.
+* Rodriguez, M.A., link:http://www.slideshare.net/slidarko/gremlins-graph-traversal-machinery["Gremlin's Graph Traversal Machinery,"] Cassandra Summit, September 2016.
 * Rodriguez, M.A., link:http://www.datastax.com/dev/blog/the-mechanics-of-gremlin-olap["The Mechanics of Gremlin OLAP,"] DataStax Engineering Blog, April 2016.
 * Rodriguez, M.A., link:http://www.slideshare.net/slidarko/quantum-processes-in-graph-computing["Quantum Processes in Graph Computing,"] GraphDay '16 Presentation, Austin Texas, January 2016. [video presentation]
 * Rodriguez, M.A., Watkins, J.H., link:http://arxiv.org/abs/1511.06278["Quantum Walks with Gremlin,"] GraphDay '16 Proceedings, Austin Texas, January 2016.

http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/b7cf48e6/docs/src/recipes/index.asciidoc
----------------------------------------------------------------------
diff --git a/docs/src/recipes/index.asciidoc b/docs/src/recipes/index.asciidoc
index 0cd1ce1..cb93674 100644
--- a/docs/src/recipes/index.asciidoc
+++ b/docs/src/recipes/index.asciidoc
@@ -46,6 +46,8 @@ include::if-then-based-grouping.asciidoc[]
 
 include::pagination.asciidoc[]
 
+include::recommendation.asciidoc[]
+
 include::shortest-path.asciidoc[]
 
 include::traversal-induced-values.asciidoc[]

http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/b7cf48e6/docs/src/recipes/recommendation.asciidoc
----------------------------------------------------------------------
diff --git a/docs/src/recipes/recommendation.asciidoc b/docs/src/recipes/recommendation.asciidoc
new file mode 100644
index 0000000..81b2c96
--- /dev/null
+++ b/docs/src/recipes/recommendation.asciidoc
@@ -0,0 +1,225 @@
+////
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+////
+[[recommendation]]
+Recommendation
+--------------
+
+image:gremlin-recommendation.png[float=left,width=180]One of the more common use cases for a graph database is the
+development of link:https://en.wikipedia.org/wiki/Recommender_system[recommendation systems] and a simple approach to
+doing that is through link:https://en.wikipedia.org/wiki/Collaborative_filtering[collaborative filtering].
+Collaborative filtering assumes that if a person shares one set of opinions with a different person, they are likely to
+have similar taste with respect to other issues. With that basis in mind, it is then possible to make predictions for a
+specific person as to what their opinions might be.
+
+As a simple example, consider a graph that contains "person" and "product" vertices connected by "bought" edges. The
+following script generates some data for the graph using that basic schema:
+
+[gremlin-groovy]
+----
+g.addV("person").property("name","alice").
+  addV("person").property("name","bob").
+  addV("person").property("name","jon").
+  addV("person").property("name","jack").
+  addV("person").property("name","jill")iterate()
+(1..10).each {
+  g.addV("product").property("name","product #${it}").iterate()
+}; []
+(3..7).each {
+  g.V().has("person","name","alice").as("p").
+    V().has("product","name","product #${it}").addE("bought").from("p").iterate()
+}; []
+(1..5).each {
+  g.V().has("person","name","bob").as("p").
+    V().has("product","name","product #${it}").addE("bought").from("p").iterate()
+}; []
+(6..10).each {
+  g.V().has("person","name","jon").as("p").
+    V().has("product","name","product #${it}").addE("bought").from("p").iterate()
+}; []
+1.step(10, 2) {
+  g.V().has("person","name","jack").as("p").
+    V().has("product","name","product #${it}").addE("bought").from("p").iterate()
+}; []
+2.step(10, 2) {
+  g.V().has("person","name","jill").as("p").
+    V().has("product","name","product #${it}").addE("bought").from("p").iterate()
+}; []
+----
+
+The first step to making a recommedation to "alice" using collaborative filtering is to understand what she bought:
+
+[gremlin-groovy,existing]
+----
+g.V().has('name','alice').out('bought').values('name')
+----
+
+The following diagram depicts one of the edges traversed in the above example between "alice" and "product #5".
+Obviously, the other products "alice" bought would have similar relations, but this diagram and those to follow will
+focus on the neighborhood around that product.
+
+image:recommendation-alice-1.png[width=500]
+
+The next step is to determine who else purchased those products:
+
+[gremlin-groovy,existing]
+----
+g.V().has('name','alice').out('bought').in('bought').dedup().values('name')
+----
+
+It is worth noting that "alice" is in the results above. She should really be excluded from the list as the
+interest is in what individuals other than herself purchased:
+
+[gremlin-groovy,existing]
+----
+g.V().has('name','alice').as('her').
+      out('bought').
+      in('bought').where(neq('her')).
+      dedup().values('name')
+----
+
+The following diagram shows "alice" and those others who purchased "product #5".
+
+image:recommendation-alice-2.png[width=600]
+
+The knowledge of the people who bought the same things as "alice" can then be used to find the set of products that
+they bought:
+
+[gremlin-groovy,existing]
+----
+g.V().has('name','alice').as('her').
+      out('bought').
+      in('bought').where(neq('her')).
+      out('bought').
+      dedup().values('name')
+----
+
+image:recommendation-alice-3.png[width=800]
+
+This set of products could be the basis for recommendation, but it is important to remember that "alice" may have
+already purchased some of these products and it would be better to not pester her with recommedations for products
+that she already owns. Those products she already purchased can be excluded as follows:
+
+[gremlin-groovy,existing]
+----
+g.V().has('name','alice').as('her').
+      out('bought').aggregate('self').
+      in('bought').where(neq('her')).
+      out('bought').where(without('self')).
+      dedup().values('name')
+----
+
+image:recommendation-alice-4.png[width=800]
+
+The final step would be to group the remaining products (instead of `dedup()` which was mostly done for demonstration
+purposes) to form a ranking:
+
+[gremlin-groovy,existing]
+----
+g.V().has('person','name','alice').as('her').     <1>
+      out('bought').aggregate('self').            <2>
+      in('bought').where(neq('her')).             <3>
+      out('bought').where(without('self')).       <4>
+      groupCount().order(local).by(values, decr)  <5>
+----
+
+<1> Find "alice" who is the person for whom the product recommendation is being made.
+<2> Traverse to the products that "alice" bought and gather them for later use in the traversal.
+<3> Traverse to the "person" vertices who bought the products that "alice" bought and exclude "alice" herself from that list.
+<4> Given those people who bought similar products to "alice", find the products that they bought and exclude those that she already bought.
+<5> Group the products and count the number of times they were purchased by others to come up with a ranking of products to recommend to "alice".
+
+The previous example was already described as "basic" and obviously could take into account whatever data is available
+to further improve the quality of the recommendation (e.g. product ratings, times of purchase, etc.).  One option to
+improve the quality of what is recommended (without expanding the previous dataset) might be to choose the person
+vertices that make up the recommendation to "alice" who have the largest common set of purchases.
+
+Looking back to the previous code example, consider its more strip down representation that shows those individuals
+who have at least one product in common:
+
+[gremlin-groovy,existing]
+----
+g.V().has("person","name","alice").as("alice").
+      out("bought").aggregate("self").
+      in("bought").where(neq("alice")).dedup()
+----
+
+Next, do some grouping to find count how many products they have in common:
+
+[gremlin-groovy,existing]
+----
+g.V().has("person","name","alice").as("alice").
+      out("bought").aggregate("self").
+      in("bought").where(neq("alice")).dedup().
+      group().by().by(out("bought").where(within("self")).count())
+----
+
+The above output shows that the best that can be expected is three common products. The traversal needs to be aware of
+that maximum:
+
+[gremlin-groovy,existing]
+----
+g.V().has("person","name","alice").as("alice").
+      out("bought").aggregate("self").
+      in("bought").where(neq("alice")).dedup().
+      group().by().by(out("bought").
+        where(within("self")).count()).
+      select(values).order(local).by(decr).limit(local, 1)
+----
+
+With the maximum value available, it can be used to chose those "person" vertices that have the three products in
+common:
+
+[gremlin-groovy,existing]
+----
+g.V().has("person","name","alice").as("alice").
+      out("bought").aggregate("self").
+      in("bought").where(neq("alice")).dedup().
+      group().by().by(out("bought").
+        where(within("self")).count()).as("g").
+      select(values).order(local).by(decr).limit(local, 1).as("m").
+      select("g").unfold().where(select(values).as("m")).select(keys)
+----
+
+Now that there is a list of "person" vertices to base the recommendation on, traverse to the products that they
+purchased:
+
+[gremlin-groovy,existing]
+----
+g.V().has("person","name","alice").as("alice").
+      out("bought").aggregate("self").
+      in("bought").where(neq("alice")).dedup().
+      group().by().by(out("bought").
+        where(within("self")).count()).as("g").
+      select(values).order(local).by(decr).limit(local, 1).as("m").
+      select("g").unfold().where(select(values).as("m")).select(keys).
+      out("bought").where(without("self"))
+----
+
+The above output shows that one product is held in common making it the top recommendation:
+
+[gremlin-groovy,existing]
+----
+g.V().has("person","name","alice").as("alice").
+      out("bought").aggregate("self").
+      in("bought").where(neq("alice")).dedup().
+      group().by().by(out("bought").
+        where(within("self")).count()).as("g").
+      select(values).order(local).by(decr).limit(local, 1).as("m").
+      select("g").unfold().where(select(values).as("m")).select(keys).
+      out("bought").where(without("self")).
+      groupCount().order(local).by(values, decr).by(select(keys).values("name")).unfold().select(keys).values("name")
+----
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/b7cf48e6/docs/src/recipes/shortest-path.asciidoc
----------------------------------------------------------------------
diff --git a/docs/src/recipes/shortest-path.asciidoc b/docs/src/recipes/shortest-path.asciidoc
index 3f380f1..933eebf 100644
--- a/docs/src/recipes/shortest-path.asciidoc
+++ b/docs/src/recipes/shortest-path.asciidoc
@@ -26,7 +26,6 @@ is a simple example that identifies the shortest path between vertex "1" and ver
 
 [gremlin-groovy]
 ----
-graph = TinkerGraph.open()
 v1 = graph.addVertex(T.id, 1)
 v2 = graph.addVertex(T.id, 2)
 v3 = graph.addVertex(T.id, 3)
@@ -58,7 +57,6 @@ but includes a "weight" on the edges, that will be used to help determine the "c
 
 [gremlin-groovy]
 ----
-graph = TinkerGraph.open()
 v1 = graph.addVertex(T.id, 1)
 v2 = graph.addVertex(T.id, 2)
 v3 = graph.addVertex(T.id, 3)

http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/b7cf48e6/docs/static/images/gremlin-recommendation.png
----------------------------------------------------------------------
diff --git a/docs/static/images/gremlin-recommendation.png b/docs/static/images/gremlin-recommendation.png
new file mode 100755
index 0000000..32507a9
Binary files /dev/null and b/docs/static/images/gremlin-recommendation.png differ

http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/b7cf48e6/docs/static/images/recommendation-alice-1.png
----------------------------------------------------------------------
diff --git a/docs/static/images/recommendation-alice-1.png b/docs/static/images/recommendation-alice-1.png
new file mode 100755
index 0000000..6009ece
Binary files /dev/null and b/docs/static/images/recommendation-alice-1.png differ

http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/b7cf48e6/docs/static/images/recommendation-alice-2.png
----------------------------------------------------------------------
diff --git a/docs/static/images/recommendation-alice-2.png b/docs/static/images/recommendation-alice-2.png
new file mode 100755
index 0000000..a51a75e
Binary files /dev/null and b/docs/static/images/recommendation-alice-2.png differ

http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/b7cf48e6/docs/static/images/recommendation-alice-3.png
----------------------------------------------------------------------
diff --git a/docs/static/images/recommendation-alice-3.png b/docs/static/images/recommendation-alice-3.png
new file mode 100755
index 0000000..b7bdab7
Binary files /dev/null and b/docs/static/images/recommendation-alice-3.png differ

http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/b7cf48e6/docs/static/images/recommendation-alice-4.png
----------------------------------------------------------------------
diff --git a/docs/static/images/recommendation-alice-4.png b/docs/static/images/recommendation-alice-4.png
new file mode 100755
index 0000000..f963788
Binary files /dev/null and b/docs/static/images/recommendation-alice-4.png differ