Cooking recipes app

Hi ! I am making a cooking recipes management app and was giving a go to dgraph. My GraphQL schema is the following (simplified version):

type Ingredient {
  id: String! @id
  name: String!
}
type IngredientWithQuantity {
  ingredient: Ingredient!
  quantity: Float!
}
type Recipe {
  id: ID!
  name: String!
  ingredientsWithQuantity: [IngredientWithQuantity!]!
}

I have the following use case (that I thought very good for testing out the database capabilities):

A user can provide N constraints [(ingredientId1, quantity1), ..., (ingredientIdN, quantityN)] such as [("carrot", 0.5), ("salad", 1.3)]

I want to query the DB such as it fetches the recipes maximizing the use of constrained ingredients with respect to quantities

The (very simplified) pseudo algorithm would be the following:

FOR EACH recipe
   score = 0
   FOR EACH (ingredient, quantity) IN recipe.ingredientsWithQuantity
      FOR EACH (cstIngredient, cstQuantity) IN constraints // the N user constraints
         IF cstIngredient.id == ingredient.id AND cstQuantity > quantity
            score += 1
   score = score / COUNT(recipe.ingredientsWithQuantity)
SORT recipes BY score DESC
RETURN recipes

I cannot make the score computation part in GraphQL so I have to use custom DQL resolvers.

At first, DQL seemed to be a perfect fit for this, but I am discovering in the docs that I cannot send an array of N constraints to this resolver. I have no idea how to handle this list of tuples [(ingredient, quantity)] on DQL side.

So I am currently stuck. Is there really a way to accomplish that with DGraph ? Would be cool because I really liked the doc ! Any hint is very welcome.

@MichelDiz might have an idea here.

I know how to write a query that would get all of the recipes that use less than the given ingredients. But you want the one that uses the most of the ingredients, and you want to do it as much as possible in query language and not client side language.

Thank you for your response, yes I think I can fetch the valid recipes at client side by iterating over constraints and then building the GraphQL query string, if this is what you have in mind. But this way I cannot compute any score for these recipes. That is why I think it is a DQL problem, not a GraphQL one.

But you want the one that uses the most of the ingredients

Not exactly: I want all the recipes sorted by this score, to eventually paginate this result later. But yeah it is quite the same problem.

Coming from Neo4j and ArangoDB, I am surprised there is no way to pass an object or a array of objects as a DQL query parameter, like in Cypher or in AQL. The way DQL is designed (pattern matching) does not seem to allow this sort of operation.

If I am not missing anything, this would mean this type of query cannot be done with DQL, right ?

The only workaround I see would be to get the constraints in resolver at API side, iterating over constraints and building the DQL query string without using parameters, but it seems very hackish… And btw I don’t see any mean to inject this logic in resolvers in the doc.

maybe a lambda? Then you could get the data and process it with javascript then return the data with your custom rankings. That is the purpose of the lambda hooks. But I am still interested if this can be done somehow within DQL.

1 Like

This sounds like a job for aggregation.

Is this dataset similar to your structure?

{
   "set": [
      {
         "name": "Recipe 1",
         "ingredientsWithQuantity": [
            {
               "quantity": 1.1,
               "ingredient": {
                  "uid": "_:Ingredient_X1",
                  "name": "Ingredient X1"
               }
            },
            {
               "quantity": 2.01,
               "ingredient": {
                  "uid": "_:Ingredient_X2",
                  "name": "Ingredient X2"
               }
            },
            {
               "quantity": 0.1,
               "ingredient": {
                  "uid": "_:Ingredient_X4",
                  "name": "Ingredient X4"
               }
            }
         ]
      },
      {
         "name": "Recipe 2",
         "ingredientsWithQuantity": [
            {
               "quantity": 0.1,
               "ingredient": {
                  "uid": "_:Ingredient_X1",
                  "name": "Ingredient X1"
               }
            },
            {
               "quantity": 0.121,
               "ingredient": {
                  "uid": "_:Ingredient_X2",
                  "name": "Ingredient X2"
               }
            },
            {
               "quantity": 4.1,
               "ingredient": {
                  "uid": "_:Ingredient_X4",
                  "name": "Ingredient X4"
               }
            }
         ]
      },
      {
         "name": "Recipe 3",
         "ingredientsWithQuantity": [
            {
               "quantity": 3.3,
               "ingredient": {
                  "uid": "_:Ingredient_X1",
                  "name": "Ingredient X1"
               }
            },
            {
               "quantity": 0.21,
               "ingredient": {
                  "uid": "_:Ingredient_X2",
                  "name": "Ingredient X2"
               }
            },
            {
               "quantity": 1.1,
               "ingredient": {
                  "uid": "_:Ingredient_X4",
                  "name": "Ingredient X4"
               }
            }
         ]
      }
   ]
}

Also, I’m a bit confused if you wanna count the ingredients itself or the quantity in the ingredients. Is this quantity value a weight? like kilograms?

Yes this is similar, except there is an additional id field on ingredient (like “carrot”). And yes here the quantity is a weight for simplification. I was not very clear in my use case:

  • One user has, say, 2g of carrots and 3g of salad. He wants a recipe that use the most of his stock.
  • So he sends to the API a list of tuples [("carrot", 2), ("salad", 3)].
  • The API should send back a list of recipes names sorted by the “score” value computed with the above algo in pseudocode (the score is a coefficient indicating the pertinence of the recipe with regard to the user ingredients)

My problem does not concern the design of the query (even if I am a newcomer in DQL). For example, I think the previous example could be queried this way (still didn’t validate it though):

{ 
  var(func: eq(Ingredient.id, "carrot")) { carrotId as uid }
  var(func: eq(Ingredient.id, "salad")) { saladId as uid }
  
  var(func: type(Recipe)) {
    recipesIds as uid
    nbValids as count(Recipe.ingredientsWithQuantity @filter(
      	 (uid_in(IngredientWithQuantity.ingredient, uid(carrotId))) AND le(IngredientWithQuantity.quantity, 2)
    	OR (uid_in(IngredientWithQuantity.ingredient, uid(saladId))) AND le(IngredientWithQuantity.quantity, 3)
    ))
  }
        
  var(func: uid(recipesIds)) {
    nbTotal as count(Recipe.ingredientsWithQuantity)
    score as math((nbValids * 1.) / nbTotal) // * 1. because I don't know how to cast to float
  }
  
  me(func: uid(score), orderdesc: val(score)) {
    name: Recipe.name
    score: val(score)
  }
}

My real problem is that the list of user ingredients is dynamic with a size N. I don’t know how to modifiy the above code to work with that. In Cypher and AQL, there is a way to pass list of object as parameters.

But thanks to @verneleem suggestion, I just set a lambda server and now I am able to build the query string with a JS template string without using parameters, like:

{
  ${ constraints.map(({ ingredient }) => `var(func: eq(Ingredient.id, "${ ingredient.name }")) { ${ ingredient.name }Id as uid }`).join('\n') }
  
  var(func: type(Recipe)) {
    recipesIds as uid
    nbValids as count(Recipe.ingredientsWithQuantity @filter(
      	 ${ constraints.map(({ ingredient, quantity }) =>
            uid_in(IngredientWithQuantity.ingredient, uid(${ ingredient.name }Id))) AND le(IngredientWithQuantity.quantity, ${ quantity })`).join(' OR ') }
    ))
  }
  ...
}

And now I can send a list of constraints [("carrot", 2), ("salad", 3)] to the API. I expect I am more clear now :slight_smile:. Is this the way to settle things ? Or is there a full DQL-way to do this ?

Thank you for your time !

1 Like

Sorry to bump this thread, should probably have created another one, since it is another subject. I am trying to scale the previous request (with “carrot” and “salad”) with indexes. I have some troubles understanding how it works.

With ~500.000 IngredientWithQuantity, the following simple query timeouts without index:

{ 
  me(func: type(IngredientWithQuantity), orderdesc: IngredientWithQuantity.quantity, first: 10) {
  	uid
  }
}

Now I created a float index on IngredientWithQuantity.quantity and the query takes ~3 seconds. This is much better, but for a SORT/LIMIT request on an index, I was expecting an instant response, not in seconds.

I am probably misleading on indexes, but I thought it allowed IngredientWithQuantity items to be intrinsically sorted by quantity, thus allowing O(log(n)) fetching. Is 3 seconds a natural time for this simple request ?

Thanks !