369 lines
12 KiB
Plaintext
369 lines
12 KiB
Plaintext
// ========================================================================================
|
|
// 中式烹饪知识图谱构建脚本
|
|
// ========================================================================================
|
|
|
|
// 删除已存在的约束
|
|
DROP CONSTRAINT recipe_id_unique IF EXISTS;
|
|
DROP CONSTRAINT ingredient_id_unique IF EXISTS;
|
|
DROP CONSTRAINT cookingstep_id_unique IF EXISTS;
|
|
DROP CONSTRAINT cookingmethod_id_unique IF EXISTS;
|
|
DROP CONSTRAINT cookingtool_id_unique IF EXISTS;
|
|
DROP CONSTRAINT difficultylevel_id_unique IF EXISTS;
|
|
|
|
// 创建核心约束
|
|
CREATE CONSTRAINT recipe_id_unique IF NOT EXISTS FOR (r:Recipe) REQUIRE r.nodeId IS UNIQUE;
|
|
CREATE CONSTRAINT ingredient_id_unique IF NOT EXISTS FOR (i:Ingredient) REQUIRE i.nodeId IS UNIQUE;
|
|
CREATE CONSTRAINT cookingstep_id_unique IF NOT EXISTS FOR (s:CookingStep) REQUIRE s.nodeId IS UNIQUE;
|
|
|
|
// 创建基础索引
|
|
CREATE INDEX recipe_name_index IF NOT EXISTS FOR (r:Recipe) ON (r.name);
|
|
CREATE INDEX recipe_difficulty_index IF NOT EXISTS FOR (r:Recipe) ON (r.difficulty);
|
|
CREATE INDEX recipe_cuisine_index IF NOT EXISTS FOR (r:Recipe) ON (r.cuisineType);
|
|
CREATE INDEX ingredient_name_index IF NOT EXISTS FOR (i:Ingredient) ON (i.name);
|
|
CREATE INDEX ingredient_category_index IF NOT EXISTS FOR (i:Ingredient) ON (i.category);
|
|
CREATE INDEX cookingstep_number_index IF NOT EXISTS FOR (s:CookingStep) ON (s.stepNumber);
|
|
|
|
// 创建全文搜索索引(包含更多字段)
|
|
CREATE FULLTEXT INDEX recipe_fulltext_index IF NOT EXISTS FOR (r:Recipe) ON EACH [r.name, r.description, r.preferredTerm, r.fsn, r.tags];
|
|
CREATE FULLTEXT INDEX ingredient_fulltext_index IF NOT EXISTS FOR (i:Ingredient) ON EACH [i.name, i.description, i.preferredTerm, i.fsn];
|
|
CREATE FULLTEXT INDEX cookingstep_fulltext_index IF NOT EXISTS FOR (s:CookingStep) ON EACH [s.name, s.description, s.methods, s.tools];
|
|
|
|
// 创建层次结构节点
|
|
RETURN 'Creating hierarchy nodes from nodes.csv';
|
|
LOAD CSV WITH HEADERS FROM 'file:///nodes.csv' AS row
|
|
WITH row
|
|
WHERE row.nodeId < '200000000' // 层次结构节点的ID范围
|
|
AND row.nodeId IS NOT NULL
|
|
AND row.name IS NOT NULL
|
|
WITH row, row.labels as nodeType
|
|
CALL apoc.create.node([nodeType], {
|
|
nodeId: row.nodeId,
|
|
name: row.name,
|
|
preferredTerm: row.preferredTerm,
|
|
fsn: row.fsn,
|
|
conceptType: row.conceptType,
|
|
synonyms: row.synonyms,
|
|
category: row.category,
|
|
description: row.description,
|
|
originalLabels: row.labels,
|
|
isHierarchyNode: true
|
|
}) YIELD node
|
|
RETURN count(node);
|
|
|
|
// 创建菜谱节点
|
|
RETURN 'Creating Recipe nodes from nodes.csv';
|
|
LOAD CSV WITH HEADERS FROM 'file:///nodes.csv' AS row
|
|
WITH row
|
|
WHERE row.labels = 'Recipe'
|
|
AND row.nodeId >= '200000000' // 只处理具体实例,不处理层次结构
|
|
AND row.nodeId IS NOT NULL
|
|
AND row.name IS NOT NULL
|
|
|
|
MERGE (r:Recipe {nodeId: row.nodeId})
|
|
SET r.name = row.name,
|
|
r.preferredTerm = row.preferredTerm,
|
|
r.fsn = row.fsn,
|
|
r.description = row.description,
|
|
r.difficulty = CASE
|
|
WHEN row.difficulty IS NOT NULL THEN toFloat(row.difficulty)
|
|
ELSE null
|
|
END,
|
|
r.category = row.category,
|
|
r.conceptType = row.conceptType,
|
|
r.cuisineType = row.cuisineType,
|
|
r.prepTime = row.prepTime,
|
|
r.cookTime = row.cookTime,
|
|
r.servings = row.servings,
|
|
r.tags = row.tags,
|
|
r.filePath = row.filePath,
|
|
r.synonyms = row.synonyms,
|
|
r.textForEmbedding = row.name + ' ' + COALESCE(row.preferredTerm, '') + ' ' +
|
|
COALESCE(row.description, '') + ' ' + COALESCE(row.tags, ''),
|
|
r.originalLabels = row.labels;
|
|
|
|
// 创建食材节点
|
|
RETURN 'Creating Ingredient nodes';
|
|
LOAD CSV WITH HEADERS FROM 'file:///nodes.csv' AS row
|
|
WITH row
|
|
WHERE row.labels = 'Ingredient'
|
|
AND row.nodeId >= '200000000' // 只处理具体实例
|
|
AND row.nodeId IS NOT NULL
|
|
AND row.name IS NOT NULL
|
|
|
|
MERGE (i:Ingredient {nodeId: row.nodeId})
|
|
SET i.name = row.name,
|
|
i.preferredTerm = row.preferredTerm,
|
|
i.fsn = row.fsn,
|
|
i.description = row.description,
|
|
i.category = row.category,
|
|
i.conceptType = row.conceptType,
|
|
i.amount = row.amount,
|
|
i.unit = row.unit,
|
|
i.isMain = CASE
|
|
WHEN row.isMain IS NOT NULL THEN toBoolean(row.isMain)
|
|
ELSE null
|
|
END,
|
|
i.synonyms = row.synonyms,
|
|
i.originalLabels = row.labels;
|
|
|
|
// 创建烹饪步骤节点
|
|
RETURN 'Creating CookingStep nodes';
|
|
LOAD CSV WITH HEADERS FROM 'file:///nodes.csv' AS row
|
|
WITH row
|
|
WHERE row.labels = 'CookingStep'
|
|
AND row.nodeId >= '200000000' // 只处理具体实例
|
|
AND row.nodeId IS NOT NULL
|
|
AND row.name IS NOT NULL
|
|
|
|
MERGE (s:CookingStep {nodeId: row.nodeId})
|
|
SET s.name = row.name,
|
|
s.preferredTerm = row.preferredTerm,
|
|
s.fsn = row.fsn,
|
|
s.description = row.description,
|
|
s.category = row.category,
|
|
s.conceptType = row.conceptType,
|
|
s.stepNumber = CASE
|
|
WHEN row.stepNumber IS NOT NULL THEN toFloat(row.stepNumber)
|
|
ELSE null
|
|
END,
|
|
s.methods = row.methods,
|
|
s.tools = row.tools,
|
|
s.timeEstimate = row.timeEstimate,
|
|
s.synonyms = row.synonyms,
|
|
s.originalLabels = row.labels;
|
|
|
|
// 创建其他类型节点
|
|
RETURN 'Creating other node types';
|
|
LOAD CSV WITH HEADERS FROM 'file:///nodes.csv' AS row
|
|
WITH row
|
|
WHERE NOT (row.labels IN ['Recipe', 'Ingredient', 'CookingStep'])
|
|
AND row.nodeId IS NOT NULL
|
|
AND row.name IS NOT NULL
|
|
WITH row, row.labels as nodeType
|
|
CALL apoc.create.node([nodeType], {
|
|
nodeId: row.nodeId,
|
|
name: row.name,
|
|
description: row.description,
|
|
category: row.category,
|
|
conceptType: row.conceptType,
|
|
originalLabels: row.labels
|
|
}) YIELD node
|
|
RETURN count(node);
|
|
|
|
// 创建需要关系 (801000001)
|
|
RETURN 'Creating REQUIRES relationships';
|
|
LOAD CSV WITH HEADERS FROM 'file:///relationships.csv' AS row
|
|
WITH row
|
|
WHERE row.relationshipType = '801000001'
|
|
AND row.startNodeId IS NOT NULL
|
|
AND row.endNodeId IS NOT NULL
|
|
MATCH (source:Recipe {nodeId: row.startNodeId})
|
|
MATCH (target:Ingredient {nodeId: row.endNodeId})
|
|
MERGE (source)-[r:REQUIRES]->(target)
|
|
SET r.relationshipId = row.relationshipId,
|
|
r.amount = row.amount,
|
|
r.unit = row.unit,
|
|
r.originalType = row.relationshipType;
|
|
|
|
// 创建包含步骤关系 (801000003)
|
|
RETURN 'Creating CONTAINS relationships';
|
|
LOAD CSV WITH HEADERS FROM 'file:///relationships.csv' AS row
|
|
WITH row
|
|
WHERE row.relationshipType = '801000003'
|
|
AND row.startNodeId IS NOT NULL
|
|
AND row.endNodeId IS NOT NULL
|
|
MATCH (source:Recipe {nodeId: row.startNodeId})
|
|
MATCH (target:CookingStep {nodeId: row.endNodeId})
|
|
MERGE (source)-[r:CONTAINS_STEP]->(target)
|
|
SET r.relationshipId = row.relationshipId,
|
|
r.stepOrder = CASE
|
|
WHEN row.step_order IS NOT NULL AND row.step_order <> ''
|
|
THEN toFloat(row.step_order)
|
|
ELSE null
|
|
END,
|
|
r.originalType = row.relationshipType;
|
|
|
|
// 创建其他关系类型
|
|
RETURN 'Creating other relationships';
|
|
LOAD CSV WITH HEADERS FROM 'file:///relationships.csv' AS row
|
|
WITH row
|
|
WHERE NOT (row.relationshipType IN ['801000001', '801000003'])
|
|
AND row.relationshipType IS NOT NULL
|
|
AND row.startNodeId IS NOT NULL
|
|
AND row.endNodeId IS NOT NULL
|
|
MATCH (source {nodeId: row.startNodeId})
|
|
MATCH (target {nodeId: row.endNodeId})
|
|
WITH source, target, row,
|
|
CASE row.relationshipType
|
|
WHEN '801000004' THEN 'BELONGS_TO'
|
|
WHEN '801000005' THEN 'DIFFICULTY_LEVEL'
|
|
ELSE row.relationshipType
|
|
END as relType
|
|
CALL apoc.create.relationship(source, relType, {
|
|
relationshipId: row.relationshipId,
|
|
amount: row.amount,
|
|
unit: row.unit,
|
|
stepOrder: CASE
|
|
WHEN row.step_order IS NOT NULL AND row.step_order <> ''
|
|
THEN toFloat(row.step_order)
|
|
ELSE null
|
|
END,
|
|
originalType: row.relationshipType
|
|
}, target) YIELD rel
|
|
RETURN count(rel);
|
|
|
|
// 基于数据创建分类节点(支持多重分类)
|
|
RETURN 'Creating category nodes from data';
|
|
|
|
// 处理多重分类:按逗号分割并创建分类节点
|
|
MATCH (n)
|
|
WHERE n.category IS NOT NULL AND n.category <> ''
|
|
WITH n, split(n.category, ',') as categoryList
|
|
UNWIND categoryList as categoryName
|
|
WITH n, trim(categoryName) as cleanCategoryName
|
|
WHERE cleanCategoryName <> ''
|
|
MERGE (cat:Category {name: cleanCategoryName})
|
|
MERGE (n)-[:BELONGS_TO_CATEGORY]->(cat);
|
|
|
|
// 为每个唯一的conceptType创建概念类型节点
|
|
MATCH (n)
|
|
WHERE n.conceptType IS NOT NULL AND n.conceptType <> ''
|
|
WITH DISTINCT n.conceptType as conceptTypeName
|
|
MERGE (ct:ConceptType {name: conceptTypeName});
|
|
|
|
// 连接实体到概念类型
|
|
MATCH (n), (ct:ConceptType)
|
|
WHERE n.conceptType = ct.name
|
|
MERGE (n)-[:HAS_CONCEPT_TYPE]->(ct);
|
|
|
|
// 创建基于数据的相似性关系(仅基于相同分类)
|
|
RETURN 'Creating data-driven similarity relationships';
|
|
MATCH (n1), (n2)
|
|
WHERE n1.category = n2.category
|
|
AND n1.nodeId <> n2.nodeId
|
|
AND n1.category IS NOT NULL
|
|
AND labels(n1) = labels(n2)
|
|
WITH n1, n2, n1.category as sharedCategory
|
|
LIMIT 1000
|
|
MERGE (n1)-[:SIMILAR {
|
|
basis: 'same_category',
|
|
category: sharedCategory,
|
|
confidence: 0.6
|
|
}]->(n2);
|
|
|
|
// 生成统计信息
|
|
RETURN 'Generating statistics';
|
|
MATCH (n)
|
|
WITH labels(n) as nodeTypes, count(n) as nodeCount
|
|
UNWIND nodeTypes as nodeType
|
|
WITH nodeType, sum(nodeCount) as totalCount
|
|
WHERE nodeType <> 'Category' AND nodeType <> 'ConceptType'
|
|
CREATE (stat:GraphStatistics {
|
|
nodeType: nodeType,
|
|
count: totalCount,
|
|
generatedAt: datetime()
|
|
});
|
|
|
|
MATCH ()-[r]->()
|
|
WITH type(r) as relType, count(r) as relCount
|
|
CREATE (stat:RelationshipStatistics {
|
|
relationshipType: relType,
|
|
count: relCount,
|
|
generatedAt: datetime()
|
|
});
|
|
|
|
// 处理同义词关系(如果数据中有同义词)
|
|
RETURN 'Creating synonym relationships';
|
|
MATCH (n)
|
|
WHERE n.synonyms IS NOT NULL AND n.synonyms <> '' AND n.synonyms <> '[]'
|
|
WITH n, n.synonyms as synonymsJson
|
|
WHERE synonymsJson IS NOT NULL
|
|
// 这里可以根据实际的同义词格式进行解析
|
|
// 目前先创建一个标记,表示该节点有同义词
|
|
SET n.hasSynonyms = true;
|
|
|
|
// 创建基于时间的步骤序列关系
|
|
RETURN 'Creating step sequence relationships';
|
|
MATCH (r:Recipe)-[:CONTAINS_STEP]->(s1:CookingStep)
|
|
MATCH (r)-[:CONTAINS_STEP]->(s2:CookingStep)
|
|
WHERE s1.stepNumber IS NOT NULL AND s2.stepNumber IS NOT NULL
|
|
AND s2.stepNumber = s1.stepNumber + 1
|
|
MERGE (s1)-[:NEXT_STEP {
|
|
sequence: true,
|
|
timeDifference: s2.stepNumber - s1.stepNumber
|
|
}]->(s2);
|
|
|
|
// 创建基于工具的关系
|
|
RETURN 'Creating tool-based relationships';
|
|
MATCH (s1:CookingStep), (s2:CookingStep)
|
|
WHERE s1.tools IS NOT NULL AND s2.tools IS NOT NULL
|
|
AND s1.tools = s2.tools
|
|
AND s1.nodeId <> s2.nodeId
|
|
WITH s1, s2, s1.tools as sharedTool
|
|
LIMIT 1000
|
|
MERGE (s1)-[:USES_SAME_TOOL {
|
|
tool: sharedTool,
|
|
similarity: 0.5
|
|
}]->(s2);
|
|
|
|
// 创建基于烹饪方法的关系
|
|
RETURN 'Creating method-based relationships';
|
|
MATCH (s1:CookingStep), (s2:CookingStep)
|
|
WHERE s1.methods IS NOT NULL AND s2.methods IS NOT NULL
|
|
AND s1.methods = s2.methods
|
|
AND s1.nodeId <> s2.nodeId
|
|
WITH s1, s2, s1.methods as sharedMethod
|
|
LIMIT 1000
|
|
MERGE (s1)-[:USES_SAME_METHOD {
|
|
method: sharedMethod,
|
|
similarity: 0.7
|
|
}]->(s2);
|
|
|
|
// 为难度等级创建特殊处理
|
|
RETURN 'Processing difficulty levels';
|
|
MATCH (d:DifficultyLevel)
|
|
SET d.level = CASE d.name
|
|
WHEN '一星' THEN 1
|
|
WHEN '二星' THEN 2
|
|
WHEN '三星' THEN 3
|
|
WHEN '四星' THEN 4
|
|
WHEN '五星' THEN 5
|
|
ELSE 0
|
|
END;
|
|
|
|
// 创建菜谱到难度等级的直接关系(如果数据中有对应关系)
|
|
MATCH (r:Recipe)
|
|
WHERE r.difficulty IS NOT NULL
|
|
MATCH (d:DifficultyLevel)
|
|
WHERE d.level = toInteger(r.difficulty)
|
|
MERGE (r)-[:HAS_DIFFICULTY_LEVEL]->(d);
|
|
|
|
// 优化:为高频查询创建计算属性
|
|
RETURN 'Creating computed properties for optimization';
|
|
|
|
// 为菜谱计算总时间
|
|
MATCH (r:Recipe)
|
|
WHERE r.prepTime IS NOT NULL AND r.cookTime IS NOT NULL
|
|
SET r.totalTime = CASE
|
|
WHEN r.prepTime =~ '.*分钟.*' AND r.cookTime =~ '.*分钟.*' THEN
|
|
toInteger(split(r.prepTime, '分钟')[0]) + toInteger(split(r.cookTime, '分钟')[0])
|
|
ELSE null
|
|
END;
|
|
|
|
// 为菜谱计算食材数量
|
|
MATCH (r:Recipe)
|
|
OPTIONAL MATCH (r)-[:REQUIRES]->(i:Ingredient)
|
|
WITH r, count(i) as ingredientCount
|
|
SET r.ingredientCount = ingredientCount;
|
|
|
|
// 为菜谱计算步骤数量
|
|
MATCH (r:Recipe)
|
|
OPTIONAL MATCH (r)-[:CONTAINS_STEP]->(s:CookingStep)
|
|
WITH r, count(s) as stepCount
|
|
SET r.stepCount = stepCount;
|
|
|
|
RETURN 'Knowledge graph construction completed!';
|
|
|
|
MATCH (n)
|
|
RETURN labels(n) as NodeType, count(n) as Count
|
|
ORDER BY Count DESC; |