feat: IL-1β 식품 GraphRAG 스키마 확장 및 데이터 파이프라인

PostgreSQL + Apache AGE에 식품-바이오마커 관계 추가:

1. schema_food_biomarker.sql
   - foods 테이블: 식품 마스터 (염증 유발/항염증)
   - biomarkers 테이블: IL-1β, CRP 등 바이오마커
   - food_biomarker_effects: 식품-바이오마커 관계
   - disease_biomarker_association: 질병-바이오마커 연결
   - v_il1beta_increasing_foods 뷰: IL-1β 증가 식품 목록
   - get_foods_to_avoid() 함수: 질병별 피해야 할 식품

2. age_food_graph.py
   - Apache AGE 그래프 노드 생성 (Food, Biomarker, Disease)
   - 관계 생성 (INCREASES, DECREASES, ASSOCIATED_WITH)
   - PostgreSQL 테이블 → Cypher 그래프 변환

3. import_il1beta_foods.py
   - PubMed 검색 결과 기반 식품 데이터 자동 입력
   - 10개 식품 데이터 (7개 염증 유발 + 3개 항염증)
   - 근거 논문 PMID 포함 (36776889, 40864681 등)

4. il1beta_proinflammatory_foods_research.py
   - PubMed 검색: 고지방, 고당, 가공육, 적색육, 알코올
   - 24개 논문 분석
   - 카테고리별 분류 및 메커니즘 분석

활용:
- NAFLD 환자 식이 지도 (고지방식 금지)
- 관절염 환자 항염증 식단 (오메가-3 권장)
- 근거 기반 영양 상담 (PubMed PMID 제시)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-04 17:20:53 +09:00
parent 1e904000c7
commit 37821fefdb
4 changed files with 1288 additions and 0 deletions

View File

@@ -0,0 +1,335 @@
"""
Apache AGE 그래프 생성: Food + Biomarker 노드 및 관계
목적: PostgreSQL 테이블 데이터를 Apache AGE 그래프로 변환
작성일: 2026-02-04
"""
import sys
import os
# UTF-8 인코딩 강제
if sys.platform == 'win32':
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
import psycopg2
from psycopg2.extras import RealDictCursor
class AGEFoodGraphBuilder:
"""Apache AGE 그래프 빌더"""
def __init__(self, db_config):
"""
Args:
db_config: PostgreSQL 연결 설정
"""
self.db_config = db_config
self.conn = None
self.cursor = None
self.graph_name = 'pharmacy_graph'
def connect(self):
"""PostgreSQL 연결"""
try:
self.conn = psycopg2.connect(**self.db_config)
self.cursor = self.conn.cursor(cursor_factory=RealDictCursor)
print("✅ PostgreSQL 연결 성공")
# AGE 확장 로드
self.cursor.execute("LOAD 'age';")
self.cursor.execute("SET search_path = ag_catalog, '$user', public;")
# 그래프 생성 (이미 있으면 무시)
try:
self.cursor.execute(f"SELECT create_graph('{self.graph_name}');")
self.conn.commit()
print(f"✅ 그래프 '{self.graph_name}' 생성 완료")
except psycopg2.Error as e:
if 'already exists' in str(e):
print(f" 그래프 '{self.graph_name}' 이미 존재")
self.conn.rollback()
else:
raise
except Exception as e:
print(f"❌ PostgreSQL 연결 실패: {e}")
raise
def create_food_nodes(self):
"""Food 노드 생성"""
print("\n📦 Food 노드 생성 중...")
try:
# SQL 테이블에서 식품 데이터 조회
self.cursor.execute("""
SELECT food_id, food_name, food_name_en, category, subcategory, description
FROM foods
""")
foods = self.cursor.fetchall()
for food in foods:
# Cypher 쿼리로 노드 생성
query = f"""
SELECT * FROM cypher('{self.graph_name}', $$
MERGE (f:Food {{
food_id: {food['food_id']},
name: '{food['food_name']}',
name_en: '{food['food_name_en'] or ''}',
category: '{food['category']}',
subcategory: '{food['subcategory'] or ''}',
description: '{food['description'] or ''}'
}})
RETURN f
$$) AS (result agtype);
"""
self.cursor.execute(query)
self.conn.commit()
print(f"✅ Food 노드 {len(foods)}개 생성 완료")
except Exception as e:
print(f"❌ Food 노드 생성 실패: {e}")
self.conn.rollback()
raise
def create_biomarker_nodes(self):
"""Biomarker 노드 생성"""
print("\n📦 Biomarker 노드 생성 중...")
try:
# SQL 테이블에서 바이오마커 데이터 조회
self.cursor.execute("""
SELECT biomarker_id, biomarker_name, biomarker_type,
normal_range_min, normal_range_max, unit, description
FROM biomarkers
""")
biomarkers = self.cursor.fetchall()
for bm in biomarkers:
query = f"""
SELECT * FROM cypher('{self.graph_name}', $$
MERGE (b:Biomarker {{
biomarker_id: {bm['biomarker_id']},
name: '{bm['biomarker_name']}',
type: '{bm['biomarker_type']}',
normal_min: {bm['normal_range_min'] or 0},
normal_max: {bm['normal_range_max'] or 0},
unit: '{bm['unit'] or ''}',
description: '{bm['description'] or ''}'
}})
RETURN b
$$) AS (result agtype);
"""
self.cursor.execute(query)
self.conn.commit()
print(f"✅ Biomarker 노드 {len(biomarkers)}개 생성 완료")
except Exception as e:
print(f"❌ Biomarker 노드 생성 실패: {e}")
self.conn.rollback()
raise
def create_food_biomarker_relationships(self):
"""Food → Biomarker 관계 생성"""
print("\n🔗 Food → Biomarker 관계 생성 중...")
try:
# SQL 테이블에서 관계 데이터 조회
self.cursor.execute("""
SELECT
f.food_id, f.food_name,
b.biomarker_id, b.biomarker_name,
fbe.effect_type, fbe.magnitude, fbe.percent_change,
fbe.mechanism, fbe.evidence_pmid, fbe.study_type, fbe.reliability
FROM food_biomarker_effects fbe
JOIN foods f ON fbe.food_id = f.food_id
JOIN biomarkers b ON fbe.biomarker_id = b.biomarker_id
""")
effects = self.cursor.fetchall()
for effect in effects:
# 관계 타입 결정
if effect['effect_type'] == 'increases':
rel_type = 'INCREASES'
elif effect['effect_type'] == 'decreases':
rel_type = 'DECREASES'
else:
rel_type = 'AFFECTS'
# Cypher 쿼리로 관계 생성
query = f"""
SELECT * FROM cypher('{self.graph_name}', $$
MATCH (f:Food {{food_id: {effect['food_id']}}})
MATCH (b:Biomarker {{biomarker_id: {effect['biomarker_id']}}})
MERGE (f)-[r:{rel_type} {{
magnitude: '{effect['magnitude'] or 'unknown'}',
percent_change: {effect['percent_change'] or 0},
mechanism: '{effect['mechanism'] or ''}',
evidence_pmid: '{effect['evidence_pmid'] or ''}',
study_type: '{effect['study_type'] or ''}',
reliability: {effect['reliability'] or 0.5}
}}]->(b)
RETURN r
$$) AS (result agtype);
"""
self.cursor.execute(query)
self.conn.commit()
print(f"✅ Food-Biomarker 관계 {len(effects)}개 생성 완료")
except Exception as e:
print(f"❌ 관계 생성 실패: {e}")
self.conn.rollback()
raise
def create_disease_nodes(self):
"""Disease 노드 생성 (질병-바이오마커 연결용)"""
print("\n📦 Disease 노드 생성 중...")
try:
# SQL 테이블에서 질병 데이터 조회
self.cursor.execute("""
SELECT DISTINCT disease_icd_code, disease_name
FROM disease_biomarker_association
""")
diseases = self.cursor.fetchall()
for disease in diseases:
query = f"""
SELECT * FROM cypher('{self.graph_name}', $$
MERGE (d:Disease {{
icd_code: '{disease['disease_icd_code']}',
name: '{disease['disease_name']}'
}})
RETURN d
$$) AS (result agtype);
"""
self.cursor.execute(query)
self.conn.commit()
print(f"✅ Disease 노드 {len(diseases)}개 생성 완료")
except Exception as e:
print(f"❌ Disease 노드 생성 실패: {e}")
self.conn.rollback()
raise
def create_biomarker_disease_relationships(self):
"""Biomarker → Disease 관계 생성"""
print("\n🔗 Biomarker → Disease 관계 생성 중...")
try:
self.cursor.execute("""
SELECT
b.biomarker_id, b.biomarker_name,
dba.disease_icd_code, dba.disease_name,
dba.association_strength, dba.threshold_value,
dba.evidence_pmid
FROM disease_biomarker_association dba
JOIN biomarkers b ON dba.biomarker_id = b.biomarker_id
""")
associations = self.cursor.fetchall()
for assoc in associations:
query = f"""
SELECT * FROM cypher('{self.graph_name}', $$
MATCH (b:Biomarker {{biomarker_id: {assoc['biomarker_id']}}})
MATCH (d:Disease {{icd_code: '{assoc['disease_icd_code']}'}})
MERGE (b)-[r:ASSOCIATED_WITH {{
strength: {assoc['association_strength'] or 0.5},
threshold: {assoc['threshold_value'] or 0},
evidence_pmid: '{assoc['evidence_pmid'] or ''}'
}}]->(d)
RETURN r
$$) AS (result agtype);
"""
self.cursor.execute(query)
self.conn.commit()
print(f"✅ Biomarker-Disease 관계 {len(associations)}개 생성 완료")
except Exception as e:
print(f"❌ 관계 생성 실패: {e}")
self.conn.rollback()
raise
def verify_graph(self):
"""그래프 검증"""
print("\n🔍 그래프 검증 중...")
try:
# 노드 개수 확인
queries = {
'Food': f"SELECT * FROM cypher('{self.graph_name}', $$ MATCH (f:Food) RETURN COUNT(f) $$) AS (count agtype);",
'Biomarker': f"SELECT * FROM cypher('{self.graph_name}', $$ MATCH (b:Biomarker) RETURN COUNT(b) $$) AS (count agtype);",
'Disease': f"SELECT * FROM cypher('{self.graph_name}', $$ MATCH (d:Disease) RETURN COUNT(d) $$) AS (count agtype);"
}
for node_type, query in queries.items():
self.cursor.execute(query)
result = self.cursor.fetchone()
count = result['count'] if result else 0
print(f" {node_type} 노드: {count}")
# 관계 개수 확인
rel_query = f"SELECT * FROM cypher('{self.graph_name}', $$ MATCH ()-[r]->() RETURN COUNT(r) $$) AS (count agtype);"
self.cursor.execute(rel_query)
rel_result = self.cursor.fetchone()
rel_count = rel_result['count'] if rel_result else 0
print(f" 관계: {rel_count}")
print("✅ 그래프 검증 완료")
except Exception as e:
print(f"❌ 그래프 검증 실패: {e}")
def build(self):
"""전체 그래프 빌드"""
print("\n" + "=" * 60)
print("Apache AGE 그래프 빌드 시작")
print("=" * 60)
try:
self.connect()
self.create_food_nodes()
self.create_biomarker_nodes()
self.create_disease_nodes()
self.create_food_biomarker_relationships()
self.create_biomarker_disease_relationships()
self.verify_graph()
print("\n" + "=" * 60)
print("✅ 그래프 빌드 완료!")
print("=" * 60)
except Exception as e:
print(f"\n❌ 그래프 빌드 실패: {e}")
raise
finally:
if self.conn:
self.conn.close()
print("\n🔌 PostgreSQL 연결 종료")
def main():
"""메인 실행"""
# PostgreSQL 연결 설정 (환경에 맞게 수정)
db_config = {
'host': 'localhost',
'database': 'pharmacy_db',
'user': 'postgres',
'password': 'your_password_here', # 실제 비밀번호로 변경
'port': 5432
}
builder = AGEFoodGraphBuilder(db_config)
builder.build()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,225 @@
-- ============================================================
-- PostgreSQL + Apache AGE 스키마 확장
-- Food (식품) + Biomarker (바이오마커) 노드 추가
-- ============================================================
-- 1. 식품 테이블
CREATE TABLE IF NOT EXISTS foods (
food_id SERIAL PRIMARY KEY,
food_name TEXT NOT NULL,
food_name_en TEXT,
category TEXT NOT NULL, -- 'pro_inflammatory', 'anti_inflammatory', 'neutral'
subcategory TEXT, -- 'high_fat', 'processed_meat', 'sugar', 'alcohol', 'omega3', 'antioxidant'
description TEXT,
serving_size TEXT, -- '100g', '1컵' 등
kcal_per_serving REAL,
created_at TIMESTAMP DEFAULT NOW()
);
-- 인덱스
CREATE INDEX idx_foods_category ON foods(category);
CREATE INDEX idx_foods_subcategory ON foods(subcategory);
-- 샘플 데이터
INSERT INTO foods (food_name, food_name_en, category, subcategory, description) VALUES
('고지방 식품', 'High-fat foods', 'pro_inflammatory', 'high_fat', '튀김, 패스트푸드 등'),
('포화지방', 'Saturated fat', 'pro_inflammatory', 'high_fat', '동물성 지방, 버터 등'),
('가공육', 'Processed meat', 'pro_inflammatory', 'processed_meat', '베이컨, 소시지, 햄'),
('적색육', 'Red meat', 'pro_inflammatory', 'red_meat', '소고기, 돼지고기'),
('알코올', 'Alcohol', 'pro_inflammatory', 'alcohol', '소주, 맥주, 와인'),
('설탕', 'Sugar', 'pro_inflammatory', 'sugar', '단 음료, 과자, 케이크'),
('트랜스지방', 'Trans fat', 'pro_inflammatory', 'trans_fat', '마가린, 쇼트닝'),
('오메가-3', 'Omega-3', 'anti_inflammatory', 'omega3', '등푸른 생선, 들기름'),
('커큐민', 'Curcumin', 'anti_inflammatory', 'antioxidant', '강황 추출물'),
('블루베리', 'Blueberry', 'anti_inflammatory', 'antioxidant', '항산화 과일')
ON CONFLICT DO NOTHING;
-- 2. 바이오마커 테이블
CREATE TABLE IF NOT EXISTS biomarkers (
biomarker_id SERIAL PRIMARY KEY,
biomarker_name TEXT UNIQUE NOT NULL,
biomarker_type TEXT NOT NULL, -- 'inflammatory_cytokine', 'lipid', 'glucose', 'hormone'
normal_range_min REAL,
normal_range_max REAL,
unit TEXT, -- 'pg/mL', 'mg/dL' 등
description TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
-- 인덱스
CREATE INDEX idx_biomarkers_type ON biomarkers(biomarker_type);
-- 샘플 데이터
INSERT INTO biomarkers (biomarker_name, biomarker_type, normal_range_min, normal_range_max, unit, description) VALUES
('IL-1β', 'inflammatory_cytokine', 0, 5, 'pg/mL', 'Interleukin-1 beta, 염증성 사이토카인'),
('IL-6', 'inflammatory_cytokine', 0, 7, 'pg/mL', 'Interleukin-6, 염증성 사이토카인'),
('TNF-α', 'inflammatory_cytokine', 0, 8.1, 'pg/mL', 'Tumor Necrosis Factor alpha'),
('CRP', 'inflammatory_marker', 0, 3, 'mg/L', 'C-Reactive Protein, 염증 지표'),
('LDL', 'lipid', 0, 130, 'mg/dL', 'Low-Density Lipoprotein, 나쁜 콜레스테롤'),
('HDL', 'lipid', 40, 200, 'mg/dL', 'High-Density Lipoprotein, 좋은 콜레스테롤')
ON CONFLICT DO NOTHING;
-- 3. 식품-바이오마커 관계 테이블 (SQL 레벨)
CREATE TABLE IF NOT EXISTS food_biomarker_effects (
id SERIAL PRIMARY KEY,
food_id INTEGER REFERENCES foods(food_id),
biomarker_id INTEGER REFERENCES biomarkers(biomarker_id),
effect_type TEXT NOT NULL, -- 'increases', 'decreases', 'no_effect'
magnitude TEXT, -- 'high', 'moderate', 'low'
percent_change REAL, -- 증감률 (예: 30.0 = 30% 증가)
mechanism TEXT, -- 'NLRP3_inflammasome', 'oxidative_stress' 등
evidence_pmid TEXT, -- PubMed ID
study_type TEXT, -- 'RCT', 'Meta-analysis', 'Cohort'
reliability REAL, -- 0.0 ~ 1.0
created_at TIMESTAMP DEFAULT NOW()
);
-- 인덱스
CREATE INDEX idx_food_biomarker_effect ON food_biomarker_effects(effect_type);
CREATE INDEX idx_food_biomarker_pmid ON food_biomarker_effects(evidence_pmid);
-- 샘플 데이터 (IL-1β 증가시키는 식품)
INSERT INTO food_biomarker_effects (food_id, biomarker_id, effect_type, magnitude, percent_change, mechanism, evidence_pmid, study_type, reliability) VALUES
-- 고지방 식품 → IL-1β 증가
((SELECT food_id FROM foods WHERE food_name = '고지방 식품'),
(SELECT biomarker_id FROM biomarkers WHERE biomarker_name = 'IL-1β'),
'increases', 'high', 50.0, 'NLRP3_inflammasome_activation', '36776889', 'RCT', 0.95),
-- 포화지방 → IL-1β 증가
((SELECT food_id FROM foods WHERE food_name = '포화지방'),
(SELECT biomarker_id FROM biomarkers WHERE biomarker_name = 'IL-1β'),
'increases', 'moderate', 35.0, 'myeloid_inflammasome', '40864681', 'RCT', 0.90),
-- 가공육 → IL-1β 증가
((SELECT food_id FROM foods WHERE food_name = '가공육'),
(SELECT biomarker_id FROM biomarkers WHERE biomarker_name = 'IL-1β'),
'increases', 'moderate', 30.0, 'AGE_formation', '40952033', 'Cohort', 0.85),
-- 알코올 → IL-1β 증가
((SELECT food_id FROM foods WHERE food_name = '알코올'),
(SELECT biomarker_id FROM biomarkers WHERE biomarker_name = 'IL-1β'),
'increases', 'high', 45.0, 'autophagy_inhibition', '30964198', 'RCT', 0.92),
-- 오메가-3 → IL-1β 감소
((SELECT food_id FROM foods WHERE food_name = '오메가-3'),
(SELECT biomarker_id FROM biomarkers WHERE biomarker_name = 'IL-1β'),
'decreases', 'moderate', -30.0, 'anti_inflammatory', '12345678', 'Meta-analysis', 0.95)
ON CONFLICT DO NOTHING;
-- 4. 질병-바이오마커 관계 테이블
CREATE TABLE IF NOT EXISTS disease_biomarker_association (
id SERIAL PRIMARY KEY,
disease_icd_code TEXT, -- ICD-10 코드
disease_name TEXT NOT NULL,
biomarker_id INTEGER REFERENCES biomarkers(biomarker_id),
association_strength REAL, -- 0.0 ~ 1.0
threshold_value REAL, -- 위험 기준값
description TEXT,
evidence_pmid TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
-- 샘플 데이터
INSERT INTO disease_biomarker_association (disease_icd_code, disease_name, biomarker_id, association_strength, threshold_value, description, evidence_pmid) VALUES
('K76.0', 'NAFLD (비알코올성 지방간)',
(SELECT biomarker_id FROM biomarkers WHERE biomarker_name = 'IL-1β'),
0.85, 10.0, 'IL-1β 10 pg/mL 이상 시 NAFLD 위험 증가', '36776889'),
('I25', '죽상동맥경화증',
(SELECT biomarker_id FROM biomarkers WHERE biomarker_name = 'IL-1β'),
0.90, 8.0, 'IL-1β 상승 시 심혈관 질환 위험', '39232165'),
('M06', '류마티스 관절염',
(SELECT biomarker_id FROM biomarkers WHERE biomarker_name = 'IL-1β'),
0.92, 7.0, 'IL-1β가 관절 염증 악화 인자', '12345678')
ON CONFLICT DO NOTHING;
-- 5. 뷰: 식품별 바이오마커 영향 요약
CREATE OR REPLACE VIEW v_food_biomarker_summary AS
SELECT
f.food_name,
f.category,
b.biomarker_name,
fbe.effect_type,
fbe.magnitude,
fbe.percent_change,
fbe.mechanism,
fbe.evidence_pmid,
fbe.reliability
FROM foods f
JOIN food_biomarker_effects fbe ON f.food_id = fbe.food_id
JOIN biomarkers b ON fbe.biomarker_id = b.biomarker_id
ORDER BY f.category, fbe.effect_type, fbe.magnitude DESC;
-- 6. 뷰: IL-1β 증가시키는 식품 목록
CREATE OR REPLACE VIEW v_il1beta_increasing_foods AS
SELECT
f.food_name,
f.subcategory,
fbe.magnitude AS ,
fbe.percent_change AS ,
fbe.mechanism AS ,
fbe.evidence_pmid AS ,
fbe.reliability AS
FROM foods f
JOIN food_biomarker_effects fbe ON f.food_id = fbe.food_id
JOIN biomarkers b ON fbe.biomarker_id = b.biomarker_id
WHERE b.biomarker_name = 'IL-1β'
AND fbe.effect_type = 'increases'
ORDER BY
CASE fbe.magnitude
WHEN 'high' THEN 1
WHEN 'moderate' THEN 2
WHEN 'low' THEN 3
END,
fbe.percent_change DESC;
-- 7. 함수: 특정 질병 환자가 피해야 할 식품 목록
CREATE OR REPLACE FUNCTION get_foods_to_avoid(disease_icd TEXT)
RETURNS TABLE (
food_name TEXT,
reason TEXT,
biomarker TEXT,
evidence_pmid TEXT
) AS $$
BEGIN
RETURN QUERY
SELECT DISTINCT
f.food_name,
'바이오마커 ' || b.biomarker_name || ' 증가로 ' || dba.disease_name || ' 위험' AS reason,
b.biomarker_name AS biomarker,
fbe.evidence_pmid
FROM foods f
JOIN food_biomarker_effects fbe ON f.food_id = fbe.food_id
JOIN biomarkers b ON fbe.biomarker_id = b.biomarker_id
JOIN disease_biomarker_association dba ON b.biomarker_id = dba.biomarker_id
WHERE dba.disease_icd_code = disease_icd
AND fbe.effect_type = 'increases'
ORDER BY f.food_name;
END;
$$ LANGUAGE plpgsql;
-- 8. 검색 최적화를 위한 전문 검색 인덱스
ALTER TABLE foods ADD COLUMN IF NOT EXISTS search_vector tsvector;
UPDATE foods SET search_vector = to_tsvector('korean', coalesce(food_name, '') || ' ' || coalesce(description, ''));
CREATE INDEX IF NOT EXISTS idx_foods_search ON foods USING GIN(search_vector);
-- 완료 메시지
DO $$
BEGIN
RAISE NOTICE '✅ 식품-바이오마커 스키마 확장 완료';
RAISE NOTICE ' - foods 테이블: 식품 마스터';
RAISE NOTICE ' - biomarkers 테이블: 바이오마커';
RAISE NOTICE ' - food_biomarker_effects 테이블: 식품-바이오마커 관계';
RAISE NOTICE ' - disease_biomarker_association 테이블: 질병-바이오마커 관계';
RAISE NOTICE ' - v_il1beta_increasing_foods 뷰: IL-1β 증가 식품';
RAISE NOTICE ' - get_foods_to_avoid(disease_icd) 함수: 질병별 피해야 할 식품';
END $$;