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
}
}
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
代码解释:
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