如何使用Neo4j GraphQL Library(四)

https://neo4j.com/graphacademy/training-graphql-apis/03-graphql-apis-custom-logic/

使用Cypher 查询语言,为GraphQL API添加自定义逻辑

1. 首先清除Neo4j图数据中的数据

如果使用之前的数据库就清空数据,如果之前的数据库已经失效就重新创建一个新的空库。
https://neo4j.com/sandbox/

空库就不需要执行下面的清除数据代码

MATCH (a) DETACH DELETE a

2. 打开一个新的codesandbox

打开codesandbox:
https://codesandbox.io/s/github/johnymontana/training-v3/tree/master/modules/graphql-apis/supplemental/code/03-graphql-apis-custom-logic/begin?file=/.env

编辑.env文件,添加连接Neo4j图数据库的信息,保存。

3. 执行一段GraphQL mutation代码,添加一些初始数据

代码内容:
创建Book书籍信息,关联创建Subject题材和Author作者信息;
创建Customer顾客信息,关联创建Review评论、Order订单和ShipTo快递信息。

mutation {
  createBooks(
    input: [
      {
        isbn: "1492047686"
        title: "Graph Algorithms"
        price: 37.48
        description: "Practical Examples in Apache Spark and Neo4j"
        subjects: { create: [{ name: "Graph theory" }, { name: "Neo4j" }] }
        authors: {
          create: [{ name: "Mark Needham" }, { name: "Amy E. Hodler" }]
        }
      }
      {
        isbn: "1119387507"
        title: "Inspired"
        price: 21.38
        description: "How to Create Tech Products Customers Love"
        subjects: {
          create: [{ name: "Product management" }, { name: "Design" }]
        }
        authors: { create: { name: "Marty Cagan" } }
      }
      {
        isbn: "190962151X"
        title: "Ross Poldark"
        price: 15.52
        description: "Ross Poldark is the first novel in Winston Graham's sweeping saga of Cornish life in the eighteenth century."
        subjects: {
          create: [{ name: "Historical fiction" }, { name: "Cornwall" }]
        }
        authors: { create: { name: "Winston Graham" } }
      }
    ]
  ) {
    books {
      title
    }
  }

  createCustomers(
    input: [
      {
        username: "EmilEifrem7474"
        reviews: {
          create: {
            rating: 5
            text: "Best overview of graph data science!"
            book: { connect: { where: { isbn: "1492047686" } } }
          }
        }
        orders: {
          create: {
            books: { connect: { where: { title: "Graph Algorithms" } } }
            shipTo: {
              create: {
                address: "111 E 5th Ave, San Mateo, CA 94401"
                location: {
                  latitude: 37.5635980790
                  longitude: -122.322243272725
                }
              }
            }
          }
        }
      }
      {
        username: "BookLover123"
        reviews: {
          create: [
            {
              rating: 4
              text: "Beautiful depiction of Cornwall."
              book: { connect: { where: { isbn: "190962151X" } } }
            }
          ]
        }
        orders: {
          create: {
            books: {
              connect: [
                { where: { title: "Ross Poldark" } }
                { where: { isbn: "1119387507" } }
                { where: { isbn: "1492047686" } }
              ]
            }
            shipTo: {
              create: {
                address: "Nordenskiöldsgatan 24, 211 19 Malmö, Sweden"
                location: { latitude: 55.6122270502, longitude: 12.99481772774 }
              }
            }
          }
        }
      }
    ]
  ) {
    customers {
      username
    }
  }
}

4. 使用Cypher获取某个订单书籍总金额

我们需要汇总订单中每本书的价格,得到小计的书籍金额。
Cypher语句如下:

MATCH (o:Order {orderID: "9f08e841-0325-413e-b3da-43f156bd8724"})-[:CONTAINS]->(b:Book)
RETURN sum(b.price) AS subTotal

5. GraphQL计算订单书籍总金额

借鉴上面的代码,形成通用的逻辑,获取订单书籍金额小计。
添加代码到schema.graphql文件,为Order扩展一个属性subTotal,该属性的取值逻辑就是参考了上一步的Cypher代码。

# schema.graphql

extend type Order {
  subTotal: Float @cypher(statement:"MATCH (this)-[:CONTAINS]->(b:Book) RETURN sum(b.price)")
}

代码理解:
extend type Order,表示扩展Order的属性
subTotal,扩展属性名称
Float,扩展属性类型
@cypher(),表示cypher指令
statement,是参数,表示Cypher语句作为参数,其中的this代表了Order

如此,我们就可以在GraphQL Query中使用扩展的属性subTotal,代码如下:

{
  orders {
    books {
      title
      price
    }
    subTotal
  }
}

上面代码可以查询出订单包含的书籍标题和价格,以及书籍总金额

6. GraphQL计算订单运费

运费按照每公里0.01美元计算,代码如下:

# schema.graphql

extend type Order {
  shippingCost: Float @cypher(statement: """
  MATCH (this)-[:SHIPS_TO]->(a:Address)
  RETURN round(0.01 * distance(a.location, Point({latitude: 40.7128, longitude: -74.0060})) / 1000, 2)
  """)
}

代码解释:
"""代码""",三对引号中的代码,所见所得,不用加转义字符
RETURN,返回值:计算两点距离×价格,保留两位小数

当然,这时也可以查询出订单的运费

{
  orders {
    books {
      title
      price
    }
    subTotal
    shippingCost
  }
}
52401.JPG

7. GraphQL添加猜你喜欢

根据顾客历史订购信息和其他顾客历史订购信息推荐书籍,代码如下:

# schema.graphql

extend type Customer {
    recommended: [Book] @cypher(statement: """
    MATCH (this)-[:PLACED]->(:Order)-[:CONTAINS]->(:Book)<-[:CONTAINS]-(:Order)<-[:PLACED]-(c:Customer)
    MATCH (c)-[:PLACED]->(:Order)-[:CONTAINS]->(rec:Book)
    WHERE NOT EXISTS((this)-[:PLACED]->(:Order)-[:CONTAINS]->(rec))
    RETURN rec
    """)
}

查询代码:

{
  customers {
    username
    recommended {
      title
    }
  }
}

当我们希望可以限制推荐Book的数量的时候,可以用limit,代码如下:

# schema.graphql

extend type Customer {
    recommended(limit: Int = 3): [Book] @cypher(statement: """
    MATCH (this)-[:PLACED]->(:Order)-[:CONTAINS]->(:Book)<-[:CONTAINS]-(:Order)<-[:PLACED]-(c:Customer)
    MATCH (c)-[:PLACED]->(:Order)-[:CONTAINS]->(rec:Book)
    WHERE NOT EXISTS((this)-[:PLACED]->(:Order)-[:CONTAINS]->(rec))
    RETURN rec LIMIT $limit
    """)
}

代码解释:
limit = 3,默认限制推荐3本书。

查询推荐数量是(limit:1)的推荐书籍。(如不添加参数limt,则使用其默认值3)

{
  customers {
    username
    recommended(limit:1) {
      title
    }
  }
}

8. 添加天气信息

添加type Weather,扩展Addrss节点,为其添加属性currentWeather

type Weather {
  temperature: Int
  windSpeed: Int
  windDirection: Int
  precipitation: String
  summary: String
}

extend type Address {
  currentWeather: Weather @cypher(statement:"""
  WITH 'https://www.7timer.info/bin/civil.php' AS baseURL, this
  CALL apoc.load.json(
      baseURL + '?lon=' + this.location.longitude + '&lat=' + this.location.latitude + '&ac=0&unit=metric&output=json')
      YIELD value WITH value.dataseries[0] as weather
      RETURN {
          temperature: weather.temp2m,
          windSpeed: weather.wind10m.speed,
          windDirection: weather.wind10m.direction,
          precipitation: weather.prec_type,
          summary: weather.weather} AS conditions
      """)
}

如此,我们就可以查看快递地址对应的当前天气信息

{
  orders {
    shipTo {
      address
      currentWeather {
        temperature
        precipitation
        windSpeed
        windDirection
        summary
      }
    }
  }
}

9. 基于Apache Lucene实现全文检索

1)在Neo4j Browers中使用Cypher,针对Book的title和description属性添加全文索引

CALL db.index.fulltext.createNodeIndex("bookIndex", ["Book"],["title", "description"])

2)方式一:在Neo4j Browers中进行全文检索(基于刚刚创建的bookIndex)

CALL db.index.fulltext.queryNodes("bookIndex", "garph~")

代码解释:
~,表示模糊匹配
graph~,表示title和descript中含有graph,忽略轻微的拼写错误

3)方式二:GraphQL实现Book全文检索

添加GraphQL Type(在schema.graphql文件中),即添加了一个全文检索方法bookSearch

type Query {
    bookSearch(searchString: String!): [Book] @cypher(statement: """
    CALL db.index.fulltext.queryNodes('bookIndex', $searchString+'~')
    YIELD node RETURN node
    """)
}

GraphQL Query代码

{
  bookSearch(searchString: "garph") {
    title
    description
  }
}

代码解释:
调用的查询方法是booksearch
模糊查询的字段是graph,模糊匹配标识~被封装在booksearch方法中了
查询返回title和description信息

查询结果JSON

{
  "data": {
    "bookSearch": [
      {
        "title": "Graph Algorithms",
        "description": "Practical Examples in Apache Spark and Neo4j"
      }
    ]
  }
}

无论是在Neo4j查询,还是使用GraphQL查询,都使用了之前用Cypher创建的bookIndex。

相关:
CALL db.index.fulltext.createNodeIndex,对节点创建索引
CALL db.index.fulltext.createRelationshipIndex,对关系创建索引
CALL db.index.fulltext.queryNodes,节点全文检索
CALL db.index.fulltext.queryRelationships,关系全文检索
CALL db.index.fulltext.drop,删除索引
参考:
https://blog.csdn.net/weixin_42348333/article/details/89816699

10. 为书籍Book新增所属的Subject题材

1)在schema.graphql文件中添加Mutation操作方法mergeBookSubjects

# schema.graphql

type Mutation {
  mergeBookSubjects(subject: String!, bookTitles: [String!]!): Subject @cypher(statement: """
  MERGE (s:Subject {name: $subject})
  WITH s
  UNWIND $bookTitles AS bookTitle
  MATCH (t:Book {title: bookTitle})
  MERGE (t)-[:ABOUT]->(s)
  RETURN s
  """)
}

代码理解:
方法有两个输入参数
subject,新的主题
bookTitles,需要更新的Book title值,[]-表示数组,第一个!-表示title不能为空,第二个!-表示数组不能空
MERGE,更新Subject的name
WITH:可以连接多个查询的结果,即将上一个查询的结果用作下一个查询的开始
UNWIND,可以将列表拆分成行,将数组bookTitles拆分
MATCH,匹配查询,Book的title==传入的参数bookTitle
RETURN,返回Subject

2)新增subject

在执行新增subject之前先看看书籍的题材信息

{
  books {
    title
    subjects {
      name
    }
  }
}

执行新增subject

mutation {
  mergeBookSubjects(
    subject: "Non-fiction"
    bookTitles: ["Graph Algorithms", "Inspired"]
  ) {
    name
  }
}

在执行新增suject之后再看看书籍的题材信息,两者对比如下
执行前:

{
  "data": {
    "books": [
      {
        "title": "Graph Algorithms",
        "subjects": [
          {
            "name": "Neo4j"
          },
          {
            "name": "Graph theory"
          }
        ]
      },
      {
        "title": "Inspired",
        "subjects": [
          {
            "name": "Design"
          },
          {
            "name": "Product management"
          }
        ]
      },
      {
        "title": "Ross Poldark",
        "subjects": [
          {
            "name": "Cornwall"
          },
          {
            "name": "Historical fiction"
          }
        ]
      }
    ]
  }
}

执行后(有两本书各添加了题材:Non-fiction非虚构类小说)

{
  "data": {
    "books": [
      {
        "title": "Graph Algorithms",
        "subjects": [
          {
            "name": "Non-fiction"
          },
          {
            "name": "Neo4j"
          },
          {
            "name": "Graph theory"
          }
        ]
      },
      {
        "title": "Inspired",
        "subjects": [
          {
            "name": "Non-fiction"
          },
          {
            "name": "Design"
          },
          {
            "name": "Product management"
          }
        ]
      },
      {
        "title": "Ross Poldark",
        "subjects": [
          {
            "name": "Cornwall"
          },
          {
            "name": "Historical fiction"
          }
        ]
      }
    ]
  }
}

11. 添加订单预估交付时间

上面的代码,不管是基于现有数据生成新数据(小计订单书籍金额),还是模糊查询,还是更新数据,都是基于自己数据库中现有的数据做相应的操作。除此之外,我们可能还需要从其他外部的数据库/API/或系统中获取所需的数据,这时候我们就需要通过业务代码来自定义逻辑,如订单预估交付时间。

在schema.graphq文件中扩展Order 的属性

extend type Order {
    estimatedDelivery: DateTime @ignore
}

代码解释:
Order中添加属性estimatedDelivery,类型是Datetime
@ignore,表示属性对应的数据来源于自定义

在index.js中添加resolvers代码,实现订单预估交付时间获取逻辑

const resolvers = {
  Order: {
    estimatedDelivery: (obj, args, context, info) => {
      const options = [1, 5, 10, 15, 30, 45];
      const estDate = new Date();
      estDate.setDate(
        estDate.getDate() + options[Math.floor(Math.random() * options.length)]
      );
      return estDate;
    }
  }
};

在index.js中编辑如下代码
注意,上面的代码要在下面的代码之前,因为下面的代码使用了上面定义的resolvers

const neoSchema = new Neo4jGraphQL({
  typeDefs,
  resolvers,
  debug: true
});

GraphQL 查询预估交付时间

{
  orders {
    shipTo {
      address
    }
    estimatedDelivery
  }
}

查询结果 JSON

{
  "data": {
    "orders": [
      {
        "shipTo": {
          "address": "111 E 5th Ave, San Mateo, CA 94401"
        },
        "estimatedDelivery": "2021-06-08T10:01:40.470Z"
      },
      {
        "shipTo": {
          "address": "Nordenskiöldsgatan 24, 211 19 Malmö, Sweden"
        },
        "estimatedDelivery": "2021-07-08T10:01:40.470Z"
      }
    ]
  }
}

通过Cypher定义的全文索引,可以通过Cypher和GraphQL使用;
通过GraphQL定义的逻辑,如预估交付时间,仅可以通过GraphQL使用。

12. 练习

定义相似书籍的逻辑,可以自行定义,下面的代码供参考
在Book中添加similar属性(相似书籍),并定义数据获取的逻辑

# schema.graphql

extend type Book {
  similar: [Book] @cypher(statement: """
  MATCH (this)-[:ABOUT]->(s:Subject)
  WITH this, COLLECT(id(s)) AS s1
  MATCH (b:Book)-[:ABOUT]->(s:Subject) WHERE b <> this
  WITH this, b, s1, COLLECT(id(s)) AS s2
  WITH b, gds.alpha.similarity.jaccard(s2, s2) AS jaccard
  ORDER BY jaccard DESC
  RETURN b LIMIT 1
  """)
}
{
  books(where: { title: "Graph Algorithms" }) {
    title
    similar {
      title
    }
  }
}

查询出一条相似的书籍:Inspired


52501.JPG

代码解释:
gds.alpha.similarity.jaccard

The Jaccard Similarity algorithm,杰卡德相似性算法,主要用来计算样本集合之间的相似度。给定两个集合A,B,jaccard 系数定义为A与B交集的大小与并集大小的比值。杰卡德值越大,说明集合之间相似度越大。

Neo4j 4.1将GraphAlgorithms用Graph Data Science Library代替。

参考:
https://blog.csdn.net/name__student/article/details/97010623
https://blog.csdn.net/u014607067/article/details/108602290

13. 小测试

q1.JPG
q3.JPG

你可能感兴趣的:(如何使用Neo4j GraphQL Library(四))