Social graph tutorial

Social graphs are a common feature of many modern mobile, gaming, and web applications. This page will walk you through the process of creating a simple social graph. The example we’ll build here can support features such as timelines, suggested followers, and ErdÅ‘s numbers.

This tutorial assumes that you have completed the FaunaDB Shell quick start.
  1. Start FaunaDB Shell

    In a terminal, start FaunaDB Shell by running:

    fauna shell my_db
    Starting shell for database my_db
    Connected to https://db.fauna.com
    Type Ctrl+D or .exit to exit the shell
     
  2. Create a collection and index to represent people

    The first collection that we need to create is people, to represent the users in our social graph. Users could be players in a game, subscribers to an author’s articles, or coworkers in a professional network.

    CreateCollection({ name: "people" })

    Then, we need to create an index on peoples' names. The index allows us to refer to people with names like "alice" rather than refs like Ref(Collection("people"), 1). Also, let’s use the unique constraint to ensure that multiple users don’t have the same name.

    CreateIndex({
      name: "people_by_name",
      source: Collection("people"),
      terms: [{ field: ["data", "name"] }],
      unique: true
    })
  3. Create a collection and index to represent relationships

    The connection between people is called a "relationship". Relationships are directional: a person may follow another person without requiring the second person to reciprocate the relationship. Let’s call the first person a "follower" of the "followee".

    CreateCollection({ name: "relationships" })

    Then, we need to create an index on the relationships collection which is the core of our social graph. This index allows us to easily answer questions such as "who follows person A?" or "who follows person A, but not person B?"

    CreateIndex({
      name: "followers_by_followee",
      source: Collection("relationships"),
      terms: [{ field: ["data", "followee"] }],
      values: [{ field: ["data", "follower"] }]
    })
  4. Populate the graph

    Now that we have collections representing the people in our graph (people) and their relationships to each other (relationships), we can begin populating the graph.

    First, let’s create four users: Alice, Bob, Carol, and Dave. Notice that we added an index on the field ["data", "name"] for the people collection. Later on, we can use that index to find the four people in our graph.

    Foreach(
      ["Alice", "Bob", "Carol", "Dave"],
      Lambda("name",
        Create(Collection("people"), { data: { name: Var("name") } })
      )
    )

    In the following queries, we use this query pattern:

    Get(Match(Index("people_by_name"), "Alice"))

    This query uses the index people_by_name to find the first person with the name "Alice". We can be sure that the first person returned is the one we’re looking for because of the uniqueness constraint we set when we created the index.

    The first relationship that we are going to create is between Alice and Bob. We’ll create a relationship with Alice as the follower, and Bob as the followee — in plain English, this says "Alice follows Bob."

    Create(
      Collection("relationships"),
      {
        data: {
          follower: Select("ref", Get(Match(Index("people_by_name"), "Alice"))),
          followee: Select("ref", Get(Match(Index("people_by_name"), "Bob")))
        }
      }
    )

    Next, let’s add a relationship in the other direction: Bob follows Alice.

    Create(
      Collection("relationships"),
      {
        data: {
          follower: Select("ref", Get(Match(Index("people_by_name"), "Bob"))),
          followee: Select("ref", Get(Match(Index("people_by_name"), "Alice")))
        }
      }
    )

    Let’s use the index people_by_name to find all users named either "Alice" or "Bob" and add a relationship to Carol — i.e. Carol follows both Alice and Bob.

    Let(
      {
        follower: Select("ref", Get(Match(Index("people_by_name"), "Carol")))
      },
      Foreach(
        Paginate(
          Union(
            Match(Index("people_by_name"), "Alice"),
            Match(Index("people_by_name"), "Bob")
          )
        ),
        Lambda("followee",
          Create(
            Collection("relationships"),
            {
              data: {
                followee: Var("followee"),
                follower: Var("follower")
              }
            }
          )
        )
      )
    )

    Finally, let’s add a relationship meaning, "Dave follows Alice."

    Create(
      Collection("relationships"),
      {
        data: {
          followee: Select("ref", Get(Match(Index("people_by_name"), "Alice"))),
          follower: Select("ref", Get(Match(Index("people_by_name"), "Dave")))
        }
      }
    )
  5. Explore the graph

    We now have four users and relationships among them in our social graph:

    • Alice follows Bob,

    • Bob follows Alice,

    • Carol follows both Alice and Bob,

    • Dave follows Alice.

    Using our index followers_by_followee, we can now answer questions about these relationships.

    To find a person’s follower list, we use a Select query to extract the ref of each follower, like this:

    Select("ref", Get(Match(Index("people_by_name"), "Alice")))
    1. Find Alice’s followers

      We can use a ref as the term in a Match query on our relationship index. This query returns all of the refs of Alice’s followers:

      Paginate(Match(Index("people_by_name"), "Alice"))

      The result should look something like this (the identifiers for your documents are distinct):

      { data: [ Ref(Collection("people"), "236350059641307648") ] }

      Our application might be able to interpret refs, but they’re hard for a human to comprehend, so let’s Map over the set of refs, get each person’s document, and select their name out of the document.

      Map(
        Paginate(Match(Index("people_by_name"), "Alice")),
        Lambda("person",
          Select(["data", "name"], Get(Var("person")))
        )
      )

      The result should be:

      { data: [ 'Alice' ] }

      Putting it all together gives us a human-friendly list of the names of Alice’s followers:

      Map(
        Paginate(
          Match(
            Index("followers_by_followee"),
            Select("ref", Get(Match(Index("people_by_name"), "Alice")))
          )
        ),
        Lambda("person",
          Select(["data", "name"], Get(Var("person")))
        )
      )

      The result should be similar to (the order might differ):

      { data: [ 'Bob', 'Dave', 'Carol' ] }
    2. Find Alice’s OR Bob’s followers

      Now that we’ve seen how to list a single person’s followers, we can use that knowledge to ask questions about the connections between follower lists.

      For example, the union of the follower lists tells us who follows either person. Here, we ask who follows Alice or Bob:

      Map(
        Paginate(
          Union(
            Match(
              Index("followers_by_followee"),
              Select("ref", Get(Match(Index("people_by_name"), "Alice")))
            ),
            Match(
              Index("followers_by_followee"),
              Select("ref", Get(Match(Index("people_by_name"), "Bob")))
            )
          )
        ),
        Lambda("person",
          Select(["data", "name"], Get(Var("person")))
        )
      )

      The result should be similar to:

      { data: [ 'Alice', 'Bob', 'Dave', 'Carol' ] }
    3. Find Alice’s AND Bob’s followers

      The intersection of follower lists finds common followers among people. This query asks who follows Alice and Bob:

      Map(
        Paginate(
          Intersection(
            Match(
              Index("followers_by_followee"),
              Select("ref", Get(Match(Index("people_by_name"), "Alice")))
            ),
            Match(
              Index("followers_by_followee"),
              Select("ref", Get(Match(Index("people_by_name"), "Bob")))
            )
          )
        ),
        Lambda("person",
          Select(["data", "name"], Get(Var("person")))
        )
      )

      The result should be:

      { data: [ 'Carol' ] }
    4. Find people who follow Alice, but NOT Bob

      The difference of follower lists tells us who follows some people, but not others. This is how we ask for followers of Alice, but not Bob:

      Map(
        Paginate(
          Difference(
            Match(
              Index("followers_by_followee"),
              Select("ref", Get(Match(Index("people_by_name"), "Alice")))
            ),
            Match(
              Index("followers_by_followee"),
              Select("ref", Get(Match(Index("people_by_name"), "Bob")))
            )
          )
        ),
        Lambda("person",
          Select(["data", "name"], Get(Var("person")))
        )
      )

      The result should be similar to:

      { data: [ 'Bob', 'Dave' ] }

Conclusion

In this tutorial, we have modeled a simple social graph using FaunaDB’s collections and indexes. We have used the query language to ask questions about the connections in the graph that are useful building blocks of social features in mobile apps, games, and web applications.

Was this article helpful?

We're sorry to hear that.
Tell us how we can improve! documentation@fauna.com

Thank you for your feedback!