Facets in GraphQL

I am really liking this suggestion out of the whole thread so far. This is really the summarization of the problem and the solution.

Furthermore, I believe that a facet should also be tied to a parent type and not just it’s type. A facet is information stored on the edge, so in GraphQL edges exist 1) between two types and 2) between a type and it’s predicates.

So a NickName type could be a child of many different Types. All of these parent types might not have the same facets on their edges as it just doesn’t make sense.

Take for example the friend facet data:

type Person {
  id: ID
  name: String!
  friends: [Person]
  parents: [Person]
  children: [Person]
  close: Boolean @dgraph(facet: "close", on: ["Person.friends"])
}
type Business {
  id: ID
  name: String!
  employees: [Person]
}

This would then notate that the Person.close would only be applicable to query if the parent edge was coming from Person.friends, but not on Person.parents, Person.children, nor Business.employees.

If this is implemented well, this could go great measures to simplify (and complicate) schemas with a use case such as keeping a type of relationship on the edge.

enum RelationshipType {
  friend
  parent
  child
  coworker
}
type Person {
  id: ID
  name: String!
  relationships: [Person]
  close: Boolean @dgraph(facet: "close", on: ["Person.relationships"])
  relationshipType: RelationshipType @dgraph(facet: "type", on: ["Person.relationships"])
}

Another thing that quickly becomes problematic without the on data is how to enforce a required facet. If the Person is queries at the root then the facet will always be null, so a required facet will always be missing here. And if a Person is subQueried under Business.employees they would not have the same facet requirements as if they were under Person.friend

yeah, we have discussed this point and find out that facets inside Animal type are easy to handle and solve many problems. We need to store the facets somewhere and it’s not a good idea to store them inside types which reference Animal.

The next thing is representing facets in graphql. I think we can change the @facets directive with @dgraph(facet:) and rest of the things will be same.

And we will convert the Dgraph response to a valid Graphql response as discussed in detail in previous comments.

We still have similar way to field on which field is specified. For example, we specify this in field argument to @facet.

 type Animal {
    id: ID!
    name: String!
    color: String
    petClose: Boolean! @facet(field: "pet" ,name: "close")
    petSince: DateTime @facet(field: "pet" ,name: "since")
}

We need to explicitly query the facet in order for them to included in the query response. So, null can only be returned from the response if the predicate doesn’t have queried facet.

Fair point. @JatinDevDG is going to provide an example of how this modelling might look if done through the @dgraph directive. The [scalar] list case is a bit tricky because in the example that you mentioned above, it looks like we are creating a new node for nickname. Hence there is no way of differentiating between [uid] and [scalar] for facets.

If we do provide an on argument, what is the expected behaviour? Do you expect the API to throw an error if a user queries or mutates the facet along an edge it is not supposed to? We don’t really need the on argument as we expect the user to know which edge a facet belongs to and expect them to only provide it on mutations/queries along that edge.

Given that the user would know that they don’t have facets at root, they won’t be querying them I suppose. That’s a bad query I would say?

If we use @dgraph directive with facets, it will work the same way as we described @facets directive.
I am just providing the schema examples here:

UID

type Person {
    name: String!
    pet: Animal
}
type Animal {
    id: ID!
    name: String!
    color: String
    petClose: Boolean! @dgraph(facet: "close")
    petSince: DateTime @dgraph(facet: "since")
}

We don’t need to provide a field name in this case and will assume that whenever type Animal is referenced.

[UID]

type Person {
    name: String!
    pet: [Animal]
}
type Animal {
    id: ID!
    name: String!
    color: String
    petClose: Boolean!  @dgraph(facet: "close")
    petSince: DateTime @dgraph(facet: "since")
}

Similarly, for this case also, we don’t need to provide a field name and will assume it whenever type Animal is referenced.

Scalar

type Person {
    name: String!
    pet: String
    petClose: Boolean! @dgraph(facet: "close",field: "pet")
    petSince: DateTime @facet(facet: "since",field: "pet")   
}

[Scalar]

Similarly for the scalar list, if we are going to represent facets as lists in the same type.

type Person {
    name: String!
    pet: [String]
    petClose: [Boolean]   @dgraph(facet: "close",field: "pet")
    petSince: [DateTime] @dgraph(facet: "since",field: "pet")
}

Hi, sorry for barging in, but I think this (Traverse on different types) is a good example of a usecase where facets could shine in GraphQL (assuming we get the sorting and first: thing of the facets fixed)

In that post the OP wants to compute a path, and a graph database should be able to do that trivially, without resorting to the kinds of data structure that the OP has created. But because of the limitations of GraphQL, the extended tuple data structure (i.e. encoding edge data into a vertex, leaving the edges unlabelled) was used .

2 Likes

Maybe this is something I can be more onboard with

Ok, fair enough, so I can move the facet into a type and do a more natural modelling. Great!

For Scalar and [Scalar], I don’t like it as much … because

For me, this is technically ok:

it gives you a way to export a Dgraph facet into a GraphQL type, sure, but I wouldn’t do that modelling: why is petClose a property of a person? Regardless of how it’s implemented in the underlying database, I’d want to model this more like

type Person {
    name: String!
    ownsPet: PetOwnership
}

type PetOwnership {
    pet: String
    close: Boolean! 
    since: DateTime 
}

or, if pet is really worthy of it’s own type, put pet into it’s own type and also model the pet ownership relation.

So I’d always go with the UID option and model it that way. But I can’t here cause the UID options and Scalar options are different in their GraphQL representation… so if I did have facets on scalars in my DB, then I wouldn’t be able to get the modelling I want … but then I probably wouldn’t have have done it that way anyway, so maybe I don’t mind? In the end, I’d like to be able to model scalar facets the ‘right’ :slight_smile: way, but maybe this works first up.

1 Like

Very interesting read - where did things land in the end?

4 Likes

Any update on this?

Neo4J’s GraphQL adapter solves relationship properties (facets) by creating an extra level of nesting between the parent and the child. I think this is the only way that will work with complex graph structures.

type Movie {
  title: String
  year: Int
  ratings: [Rated]
}

type User {
  userId: ID
  name: String
  rated: [Rated]
}

type Rated @relation(name: "RATED") {
  from: User
  to: Movie
  rating: Float
  created: DateTime
}

More information:

2 Likes

Hi all, this feature is being worked out and we will have it soon.

2 Likes

Hi, Is there a PR we can track?

Hi,
There is no PR to track currently. This is assigned to me in current sprint and is being worked upon. Last 2 weeks we were busy with 21.03 bug fixes and testing and didn’t get time for this.

1 Like

I have kind of been thinking in the interim, we could do something like that with one caveat:

type Movie {
  title: String!
  year: Int!
  ratings: [Rating]
}

type User {
  userId: ID!
  name: String!
  ratings: [Rating]
}

type Rating {
  id: String! @id
  user: User! @hasInverse(field: ratings)
  movie: Movie! @hasInverse(field: ratings)
  rating: Float!
  createdAt: DateTime
}

Basically, when we would create a rating, we would create the id as user_movie forcing a unique (composite) identifier based on the relationship.

This may even be better than using facets. I am wondering if there is even a speed difference between a facet and a new type, considering it is all stored as a triple in the database anyway.

J

From what I understand adding the facets to graphql (and hence into generated filters) would allow you to mutate/delete with the capability of filtering based on and/or altering the relationships much easier (removal if you’re deleting the object) compared to having a type defined for that relationship (since types are not searchable or added to filters), correct? Would neatly give a work around for a lot of the requests for nested filters/cascading deletes for many use-cases if that’s the case.

@rajas A couple of things I was thinking about:

J

Hi @rajas , is there any update on adding support for facets in the GQL interface? It’s something I’m excited abiut

I beleve @rajas opened a PR for this feature a while back: Feat(GraphQL): Add support for facets in GraphQL (Schema changes) by vmrajas · Pull Request #7855 · dgraph-io/dgraph · GitHub

Unfortunately, @rajas closed it 7 days ago. @rajas I didn’t find any explanation following the review on why the PR closed. Could you share more?

@rajas Any news here? Should we be expecting this in the upcoming release?

The PR mentioned here was just for schema changes. This is an involved undertaking, we don’t have a timeline for this feature. I’d recommend running DQL queries / lambdas to get facets from GraphQL.

https://dgraph.io/docs/graphql/custom/dql/#sidebar