首页  »  AWS 教程

使用 Amazon Neptune 构建游戏好友推荐引擎

设计数据模型并准备初始数据

本模块将为您的推荐引擎设计数据模型。

图形数据库可能与您过去使用的数据库(比如关系型数据库)有所区别。关于图形数据库,您需要了解下列几个重要术语:

  • Graph(图):指代整个数据库。可以类比为其他数据库里的“表”。
  • Vertex(顶点):顶点(Vertex,也称为 Node)代表图形中的实体对象。通常用于表示名词或概念,例如人、地点、术语等。其复数形式为 vertices。您将在下文中看到该术语。
  • Edge(边):代表两个顶点间的连接关系。边通常表示实体之间的关联。比如,两个一起工作的人可能有一条 WorksWith(共事)的边相连。
  • Label(标签):用于指示添加的顶点或边所属的类型。比如,一些顶点可能带有 User(用户)标签,代表应用程序中的用户;另一些顶点则可能带有 Interest(兴趣)标签,代表用户可以关注的兴趣点。
  • Property(属性):您可以为顶点和边添加键值对。这些键值对就称为属性。比如,代表用户的顶点会有 username(用户名)属性。

查询图形时,通常从一个特定顶点出发,沿着边遍历,找出与初始顶点相关联的其他顶点。在本例场景中,要找出某个特定用户关注的所有其他用户,就从该指定用户开始,沿着从该用户出发、带有 Follow(关注)标签的所有边进行遍历搜索。

在后续步骤中,您将完成一些基本的图形查询操作。首先向集群中加载一些示例数据。然后演示如何查询用户当前感兴趣的内容。最后展示一个查询,用于为特定用户生成推荐结果。

 完成时间

30 分钟


  • 首先,我们向 Neptune 数据库中加载一些示例数据。

    在本例的数据模型中,有两种顶点类型:User(用户)和 Interest(兴趣)。同时还有两种边的类型:Follow(关注)代表一个用户关注另一个用户,InterestedIn(感兴趣)代表一个用户对某个预定义的兴趣主题感兴趣。

    在 scripts/ 目录中,vertices.json 文件包含了 50 个示例 User(用户)和 6 个示例 Interest(兴趣主题)。另有一个名为 insertVertices.js 的脚本,用于读取示例顶点数据并加载至 Neptune 中。

    insertVertices.js 脚本内容如下所示:

    const fs = require('fs');
    const path = require('path');
    
    const gremlin = require('gremlin');
    const DriverRemoteConnection = gremlin.driver.DriverRemoteConnection;
    const Graph = gremlin.structure.Graph;
    
    const connection = new DriverRemoteConnection(`wss://${process.env.NEPTUNE_ENDPOINT}:8182/gremlin`,{});
    
    const graph = new Graph();
    const g = graph.traversal().withRemote(connection);
    
    const createUser = async (username) => {
      return g.addV('User').property('username', username).next()
    }
    
    const createInterest = async (interest) => {
      return g.addV('Interest').property('interest', interest).next()
    }
    
    const raw = fs.readFileSync(path.resolve( __dirname, 'vertices.json'));
    const vertices = JSON.parse(raw)
    
    const vertexPromises = vertices.map((vertex) => {
      if (vertex.label === 'User') {
        return createUser(vertex.username)
      } else if (vertex.label === 'Interest') {
        return createInterest(vertex.name)
      }
    })
    
    Promise.all(vertexPromises).then(() => {
      console.log('Loaded vertices successfully!')
      connection.close()
    })

    该脚本将导入所需的库,初始化 Neptune 数据库连接,就像之前测试连接的脚本那样。并且其中定义了两个函数:createUser 和 createInterest,分别用于创建 User(用户)顶点和 Interest(兴趣)顶点。脚本读取示例用户数据,遍历示例数据,创建 User(用户)和 Interest(兴趣)。

    您可以使用以下命令运行该脚本:

    node scripts/insertVertices.js

    您应当会在终端看到以下输出结果:

    Loaded vertices successfully!

    scripts 文件夹中还包含一个名为 insertEdges.js 的脚本。它会从 edges.json 文件加载一些示例边数据,并将其插入至您的 Neptune 数据库。

    您可以使用以下命令运行该脚本:

    ode scripts/insertEdges.js

    您应当会在终端看到以下输出结果:

    Edges loaded successfully!

    至此,示例顶点和边数据都已加载完毕。如果您现在尝试运行上一个模块的 testDatabase.js 脚本,会发现图形中已经有一些顶点了:

    { value: 56, done: false }

    成功了!一共找到了 56 个顶点:50 个用户,6 个兴趣主题。

    下一步,您将学习如何查询某个特定用户的所有兴趣。

  • 现在 Neptune 中已经有了一些数据,您可以通过查询来回答一些问题。

    Gremlin 的查询语言一开始可能不太好理解,我们来看一个例子。假设您的应用程序要获取并返回某个特定用户的所有兴趣数据。

    scripts/ 目录中的 findUserInterests.js 文件内容如下所示:

    const gremlin = require('gremlin');
    const DriverRemoteConnection = gremlin.driver.DriverRemoteConnection;
    const Graph = gremlin.structure.Graph;
    
    const connection = new DriverRemoteConnection(`wss://${process.env.NEPTUNE_ENDPOINT}:8182/gremlin`,{});
    
    const graph = new Graph();
    const g = graph.traversal().withRemote(connection);
    
    const findUserInterests = async (username) => {
      return g.V()
        .has('User', 'username', username)
        .out('InterestedIn')
        .values('interest')
        .toList()
    }
    
    findUserInterests('amy81').then((resp) => {
      console.log(resp)
      connection.close()
    })

    跟之前一样,文件开头部分是一些导入语句和初始化图形数据库连接的代码。这里的核心部分是 findUserInterests 函数。这个函数和您将在应用程序中实现的函数类似。该函数接收一个 username 参数,返回该用户感兴趣的内容。

    让我们仔细看看函数中的实际查询语句。一共有 5 行代码。我们来分析每一行的作用。

    第一行是 g.V()。其中变量 g 代表您的图形实例。使用 V() 操作符表示您要操作顶点(而非)。

    下一行代码为 .has('User', 'username', username)。这一部分把查询范围缩小到一个特定顶点,而不是图形中的全部顶点。它指定了需要查找带有 User 标签的顶点(has() 操作符的第一个参数表示标签名)。同时它还指明,所需顶点的 username 属性值(对应 has() 操作符的第二和第三个参数)要与传入的 username 变量值相同。

    点击 User(用户)顶点后,您就可以查找用户的兴趣。这一步可以通过 out() 操作符来实现。out() 操作符会遍历从给定顶点出发、指向其他顶点的所有边。在本查询中,我们将遍历的边限定为那些带有 InterestedIn 标签的边。

    此时,查询会得到一个数组,包含所有与指定用户之间存在 InterestedIn 关系的顶点。我们希望返回这些顶点的易读名称,因此使用了 values() 操作符。告诉查询要返回每个顶点的 interest 属性值。

    最后,调用 toList() 操作符触发遍历操作的执行,并将结果汇总为一个数组。

    在脚本的最后,我们用一个示例用户来调用 findUserInterests 函数。您可以使用以下命令执行该脚本:

    node scripts/findUserInterests.js

    您应当会在终端看到以下输出结果:

    [ 'Nature', 'Sports', 'Woodworking', 'Cooking' ]

    成功了!结果显示,用户 amy81 对 Sports(运动)、Woodworking(木工)、Cooking(烹饪)和 Nature(自然)这四个主题感兴趣。

    下一步中,您将学习如何在应用程序中生成好友推荐。

  • 现在您已经掌握了一些基本的图形遍历方法,下面我们来试试更复杂一点的。

    在本应用程序中,我们希望能向用户推荐那些他们可能感兴趣的其他用户。生成此类推荐的一种常见方法,是找出那些与您已关注的人相似的其他用户。如果这些相似的用户还共同关注了另外一些用户,那很可能您也会对那些用户感兴趣。

    例如,请看下图。

    图中,椭圆形表示 Users(用户),从一个椭圆指向另一个椭圆的箭头表示 Follow(关注)关系。在本例中,最左边的用户 MyUser 关注了最上面的用户 PopularPolly。可以看到,另外两个用户(SimilarSam 和 MirrorMax)也关注了 PopularPolly。而这两个用户都还关注了另一个名为 InterestingIngrid 的用户。既然 SimilarSam 和 MirrorMax 与 MyUser 有共同关注的人,那么 MyUser 可能也会想关注 InterestingIngrid

     scripts/ 目录中,有一个名为 findFriendsOfFriends.js 的文件。其内容如下所示:

    const gremlin = require('gremlin');
    const DriverRemoteConnection = gremlin.driver.DriverRemoteConnection;
    const Graph = gremlin.structure.Graph;
    const neq = gremlin.process.P.neq
    const without = gremlin.process.P.without
    const order = gremlin.process.order
    const local = gremlin.process.scope.local
    const values = gremlin.process.column.values
    const desc = gremlin.process.order.desc
    
    const connection = new DriverRemoteConnection(`wss://${process.env.NEPTUNE_ENDPOINT}:8182/gremlin`,{});
    
    const graph = new Graph();
    const g = graph.traversal().withRemote(connection);
    
    const findFriendsOfFriends = async (username) => {
      return g.V()
        .has('User', 'username', username).as('user')
        .out('Follows').aggregate('friends')
        .in_('Follows')
        .out('Follows').where(without('friends'))
        .where(neq('user'))
        .values('username')
        .groupCount()
        .order(local)
        .by(values, desc)
        .limit(local, 10)
        .next()
    }
    
    findFriendsOfFriends('davidmiller').then((resp) => {
      console.log(resp.value)
      connection.close()
    })

    跟之前一样,文件开头部分是一些导入语句和初始化的代码。值得关注的是文件中定义的 findFriendsOfFriends 函数。该函数与应用程序中通过查找“好友的好友”来生成推荐的函数类似。

    我们再来逐步分析这一复杂的图形查询。

    首先,g.V().has('User', 'username', username).as('user') 这一部分用指定的用户名在图形中找到目标用户顶点。这部分代码使用指定的用户名来定位正确的 User 顶点。然后使用 as 函数将找到的顶点保存为 user,方便后续查询步骤中引用。

    接下来,我们要找出该用户当前关注的所有其他用户。这可以通过 .out('Follows').aggregate('friends') 来实现。该语句遍历从 User 顶点出发的所有 Follow(关注)边,找到该用户关注的所有其他 User。然后把找到的这些用户汇总到一个名为 friends 的变量中,供后续查询使用。

    接下来,我们要找出还关注了这些相同用户的其他用户。这可以用 .in_('Follows') 来实现,它会找出所有具有指向这些用户的 Follows 边的顶点。

    至此我们找到了那些与查询目标用户相似的用户。下一步是找出这些相似用户还关注的其他用户,因为这些用户可能也会引起原始用户的兴趣。我们可以用 .out('Follows').where(without('friends')) 来实现这一点。该语句会遍历从相似用户顶点出发、带有 Follows 标签的外向边。注意这里的 where 子句,它使用原始用户的 friends 变量来排除掉该用户已经关注的人。我们当然不希望把用户已经关注的人也作为推荐结果呈现给他们!

    接下来,.where(neq('user')) 子句将原始用户自己也排除掉,这样就不会出现向其推荐关注自己的荒谬情形了。之后,.values('username') 子句获取每个找到的用户顶点的用户名属性值。

    此时的查询结果中,类似用户关注的每个其他用户都对应着一条记录。也就是说结果中可能会有重复,如果有两个相似用户关注了同一个用户,那这个被关注的用户会出现两次。这种重复是有意义的,因为我们可以根据被相似用户关注的次数来对被推荐用户进行分组排序。被更多相似用户关注的人,往往与原始用户的相关度更高。

    您可以在终端中执行以下命令来运行该脚本:

    node scripts/findFriendsOfFriends.js

    您应当会在终端看到以下输出结果:

    Map {
      'paullaurie' => 23,
      'thardy' => 20,
      'ocarrillo' => 18,
      'toddjones' => 18,
      'michaelunderwood' => 17,
      'ihensley' => 17,
      'paulacruz' => 17,
      'annette32' => 17,
      'morenojason' => 16,
      'bergjames' => 16 }

    太棒了!结果展示了基于给定用户的前十名推荐用户,以及关注他们的相似用户数量。

  • 在前一步骤中,我们学习了如何为已关注了一些人的用户生成推荐。但推荐引擎的一大难题是如何引导新用户,也就是说,如何为还没有关注任何人的用户提供推荐?

    上一步中的查询对于还没有关注他人的用户,无法给出任何推荐结果。为了给这些用户生成一些推荐结果,我们可以改为利用他们指定的兴趣爱好。

    在 scripts/ 目录中,有一个名为 findFriendsWithInterests.js 的脚本。该脚本的内容如下所示:

    const gremlin = require('gremlin');
    const DriverRemoteConnection = gremlin.driver.DriverRemoteConnection;
    const Graph = gremlin.structure.Graph;
    const neq = gremlin.process.P.neq
    const order = gremlin.process.order
    const local = gremlin.process.scope.local
    const values = gremlin.process.column.values
    const desc = gremlin.process.order.desc
    
    const connection = new DriverRemoteConnection(`wss://${process.env.NEPTUNE_ENDPOINT}:8182/gremlin`,{});
    
    const graph = new Graph();
    const g = graph.traversal().withRemote(connection);
    
    const findFriendsWithInterests = async (username) => {
      return g.V()
        .has('User', 'username', username).as('user')
        .out('InterestedIn')
        .in_('InterestedIn')
        .out('Follows')
        .where(neq('user'))
        .values('username')
        .groupCount()
        .order(local)
        .by(values, desc)
        .limit(local, 10)
        .next()
    }
    
    findFriendsWithInterests('alistephanie').then((resp) => {
      console.log(resp.value)
      connection.close()
    })

    该脚本与上一步骤的脚本类似。其中定义了一个与应用程序内部函数类似的 findFriendsWithInterests 函数。对于给定的用户,该函数会搜索有相同兴趣爱好的其他用户。然后,返回这些相似用户中被关注最多的用户。

    脚本的末尾处使用了一个目前尚未关注任何其他用户的用户对象来调用上述函数。

    您可以在终端中使用以下命令执行该脚本:

    node scripts/findFriendsWithInterests.js

    您应当会看到以下输出结果:

    Map {
      'thardy' => 27,
      'paullaurie' => 26,
      'michaelunderwood' => 26,
      'paulacruz' => 20,
      'petersonchristina' => 19,
      'annette32' => 19,
      'ocarrillo' => 18,
      'evanewing' => 18,
      'hortonamy' => 18,
      'rodriguezjoseph' => 18 }

    成功了!即便该用户还没有关注任何人,我们也能为其生成一些相关的推荐用户。


总结

在本模块中,您学习了图形数据库的术语和查询机制。您通过向数据库中加载数据并完成基本查询,巩固了所学知识。您还学习了如何通过图形遍历和查找相似用户来为用户生成推荐。 

在下一个模块中,您将使用 Amazon Cognito 为应用程序配置身份验证。