cancel
Showing results for 
Search instead for 
Did you mean: 

Head's Up! These forums are read-only. All users and content have migrated. Please join us at community.neo4j.com.

Ecommerce Schema design

So I have an ecommerce use case. I'm trying to figure out how to abstract the steps of generating the variants of each product.

I need at least one type to signify the option with category and selection fields.

Then I think these [Options] can be aggregated using collect(Option.category) and Option.selection could be combined into a larger than average cartesian product which represents all of the Products-Models->Variants

Are there are any strategies or tools for generating variants like these when using cypher/apoc? Are there are examples?

My current approach schema example:

type Product {
  id: ID!
  variants: [Variant] @relation(name: "MODEL", direction: "OUT")
  name: String!
  handle: String!
  description: String
  options: [Option] @relation(name: "KIND", direction: "OUT")
}

type Variant {
  id: ID!
  name: String
  options: [String!]
  production: Int
  price: Int
  sku: String
  stock: Int
  sold: Int
  images: [Image] @relation(name: "VARIANTIMAGE", direction: "OUT")
  product: Product @relation(name: "MODEL", direction: "IN")
}

type Option {
  id: ID!
  category: String           //ex. 'Color' //ex. 'Material'
  selection: String          //ex. 'Red'   //ex. 'Denim'
  image: Image @relation(name: "OPTIONIMAGE", direction: "OUT")
  product: Product @relation(name: "KIND", direction: "IN")
}
5 REPLIES 5

Getting warmer?

UNWIND {first} as first
UNWIND {second} as second
RETURN first, second

Thanks cypher this is much more elegant than the javascript equivalent:

const options = [[first],[second]]
const Combos = (options) => {
  var results = [[]];
  for (var i = 0; i < options.length; i++) {
    var currentSubArray = options[i]; 
    var temp = [];
    for (var j = 0; j < results.length; j++) { 
       for (var k = 0; k < currentSubArray.length; k++) { 
         temp.push(results[j].concat(currentSubArray[k])); 
       }
    } 
    results = temp;
  }
  return results;
}

Just going to leave my trial and error notes here for the next student who comes here looking to do the same.

{"
Product Options Query: 
Returns Rows of 2 columns
1. a Product's Options by Category beside
2. that Category's Selections in an array
"} 

MATCH (product:Product {id: ${params.id})-[:KIND]->(o:Option)
WITH (o.category) as category, collect(o.selection) as selection
return category, selection

{"I don't really wanna work through 
more complex queries and mutations. 
I'm finding it preferable to organize 
the options in two types:"}

type Option {
  id: ID!
  name: String
  product: Product! @relation(name: "KIND", direction: "IN")
  selection: Selection @relation(name: "SELECT", direction: "OUT")
}

type Selection {
  id: ID!
  name: String
  image: Image @relation(name: "SELECTIONIMAGE", direction: "OUT")
  option: Option! @relation(name: "SELECT", direction: "IN")
}

Considering the shopify/wix/webflow product creation CMS flow it makes sense that you'd create the Option Category before defining the Option's actual choices.

Type Mutation {
  NewOption(id: String!, name: String!): Option
    @cypher(
      statement: """
      MATCH (p:Product {id: $id})
      CREATE (o:Option {
        id: apoc.create.uuid(),
        name: $name })
      CREATE (p)-[k:KIND]->(o)
      RETURN o
      """
    )
  NewSelection(id: String!, name: String!): Selection
    @cypher(
      statement: """
      MATCH (o:Option {id: $id})
      CREATE (s:Selection {
        id: apoc.create.uuid(),
        name: $name})
      CREATE (o)-[k:SELECT]->(s)
      RETURN s
      """
    )

This feels good but its not complete, based on the Product->Option->Selection schema:

With this as my graph

2X_1_1a922ff975adb076547ebe1f4de27135ce83ff6b.png

And this cypher query
Adapted from https://staging.thepavilion.io/t/cartesian-product-from-array/2312
Thank you andrew.bowman

MATCH (p:Product)-->(o:Option)-->(s:Selection)
WITH o, COLLECT([o.name, s.name]) AS s
With COLLECT(s) as input 
UNWIND range(0, size(input)-1) as bucket
UNWIND range(0, size(input[bucket])-1) as subIndex
WITH input, collect([bucket, subIndex]) as coords
WITH input, apoc.coll.combinations(coords, size(input)) as combos
UNWIND combos as combo
WITH input, combo, size(apoc.coll.toSet([coord in combo | coord[0]])) as bucketsUsed
WHERE bucketsUsed = size(input)
WITH [coord in combo | input[coord[0]][coord[1]]] as combo
RETURN apoc.map.fromPairs(combo)

I can return this:

2X_1_1853e6fe1a0f837d41b812b97480e77a31aa5903.png

Todo:

  1. I need to add a property to each selection to offset the price derived for each of the returned variant objects
  2. I need to add additional fields including a concatenated name, a generated sku, etc.

My guess is that appending the arrays used earlier in the query is the way

EDIT: CORRECTED
Finally I have my creation method but its currently decoupled from the NewSelection mutation that I previously described in my schema.

After having a while to think about it i think its best if the variants are never created directly but get generated or persist, update and delete automatically all based on the behavior of the Selections, each selection change CASE at a time. Initially I thought it was going to be more like one big scary red generate/update/delete button.

MATCH (cartesian:Product)-->(o:Option)-->(s:Selection)
WITH o, COLLECT(s) AS s
With COLLECT(s) as input 
UNWIND range(0, size(input)-1) as bucket
UNWIND range(0, size(input[bucket])-1) as subIndex
WITH input, collect([bucket, subIndex]) as coords
WITH input, apoc.coll.combinations(coords, size(input)) as combos
UNWIND combos as combo
WITH input, combo, size(apoc.coll.toSet([coord in combo | coord[0]])) as bucketsUsed
WHERE bucketsUsed = size(input)
WITH [coord in combo | input[coord[0]][coord[1]]] as variants
UNWIND variants as selections
WITH variants, selections
WITH variants, REDUCE(s = HEAD(variants), n IN TAIL(variants) | s.name + '-' + n.name) AS name, selections
MERGE (v:Variant {name:name})
MERGE (s:Selection {id:selections.id})
MERGE (s)-[:OF]->(v)
RETURN s, v

Next steps:

  1. Tune the NewSelection mutation to function differently using CASE for the first NewSelection of a given option, renaming all variants to append the concatenated name of the only-child NewSelection, and CASE for not-the-first selection of a given option, wherein a NewSelection must also generate its new variants and relationships.
  2. Update mutation for selection name and update variants concat(name)
  3. Bulk property update mutation for variants selected in a given list
  4. Delete selection mutation and its variants