콘텐츠로 이동

Zio.Node()

Neo4j 온톨로지를 훑고 집계합니다. 그릇(노드·속성)은 여기서 못 만듭니다 — 그것은 온톨로지 설계서가 정합니다. 네 진입점이 각각 무엇을 할 수 있는지는 Zio.Database (개요) 의 표에 나란히 있습니다.

내장된 Graph 객체를 사용하여 메서드 체이닝 방식으로 그래프 데이터를 조회할 수 있습니다.

Method Description Sample
.schema() 이 라벨의 칸 정의. {label, defined_in:[{twin_id, twin_title, display_property, properties[...]}]}. 같은 이름이 설계서 여럿에 있을 수 있어 어느 설계서에서 왔는지 함께 옵니다 — 칸이 서로 다르면 그것이 곧 볼 거리입니다. Zio.node("Inquiry").schema()
.available_edges() 이 라벨에서 나가고 들어오는 관계. {out:[{rel, to}], in:[{rel, from}]}. .out(rel, to=...) 에 넣을 이름이 여기 있습니다 — 짐작하지 마십시오. 관계 이름은 설계서를 만든 사람이 지은 것이라 HAS_PRIORITYhasSentiment 가 한 설계서에 함께 있습니다. Zio.node("Inquiry").available_edges()
Zio.node.labels() 지금 그래프에 서 있는 라벨과 개수. [{label, count}]. Zio.ontology.list() 는 「그리기로 한 것」이고 이쪽은 「지금 있는 것」입니다 — 둘이 어긋나는 것을 보는 것이 이 값의 쓸모입니다. Zio.node.labels()
Zio.node(label) 탐색을 시작할 시작 노드의 라벨(Label)을 지정합니다. Zio.node("Inquiry")
.where(**kwargs) 현재 포커스된 노드의 속성값으로 필터링합니다. 비교 접미사(Suffix)를 지원합니다 — 아래 비교 접미사 표 참고. 여러 개를 주면 모두 AND 로 묶입니다. .where(status="OPEN", yyyymmdd__gte="2026-03-01")
.where_or(**kwargs) 여러 조건을 OR 로 묶습니다(다른 where 들과는 AND). 키워드 검색처럼 여러 칸 중 하나라도 맞으면 될 때 씁니다 — 예전엔 질의를 여러 번 나눠 파이썬에서 합쳐야 했습니다. 접미사는 .where() 와 같습니다. 관계 너머 1촌 노드까지 OR 로 묶을 수 있습니다 — 먼저 홉을 alias 로 선언하고 별칭.속성 키로 섞습니다. 별칭은 선언한 홉이어야 하고(없으면 막힘 — 관계명을 짐작 안 함), 점 없는 키는 .where() 처럼 지금 서 있는 노드입니다(그래서 횡단 OR 에선 기준 노드도 별칭으로 밝히세요). 돌려받을 노드는 .select() 로 정합니다(안 주면 마지막 홉 반환). 검색 OR 은 홉을 optional_out 으로 — 필수 홉이면 관계 없는 행이 탈락합니다. 여러 번 부르면 각 호출이 별개의 OR 덩어리가 되어 서로 AND 로 이어집니다. .where_or(subject__contains=kw, content_action__contains=kw) .optional_out("HAS_WORK", to="Work", alias="work").where_or(**{"worklog.subject__contains": kw, "work.work_name__contains": kw}).select(id="worklog.worklog_id")
.out(rel_type, to, alias, from_) 관계를 따라 정방향(→)으로 한 홉 갑니다. to 는 도착 노드의 라벨, alias 는 거기 붙일 이름(안 주면 라벨에서 딴 것), from_ 는 어디서 갈라지는지입니다(안 주면 직전 노드). .out("CREATED_BY", to="Customer")
.in_(rel_type, to, alias, from_) 관계를 따라 역방향(←)으로 한 홉 갑니다. 상대가 나를 가리키는 경우입니다. 인자는 .out() 과 같습니다. .in_("DUE_ON", to="Order")
.with_relations(only=None) 노드에 1촌 관계를 통째로 붙여 한 번에 받습니다. 결과 한 줄은 노드 속성 그대로에 relations 칸이 하나 더 붙은 모양입니다 — {"Priority": [...], "Category": [...]}. 인자를 안 주면 .available_edges()["out"] 을 전부 붙이고, 대상 라벨 목록이나 {"rel":…, "to":…} 로 골라 줄 수도 있습니다. 아래 관계를 한 번에 절을 보십시오. .with_relations(["Priority", "Category"])
.select(**kwargs) 반환할 속성들을 별칭(Alias)과 함께 임의로 지정(Projection)하여 가져옵니다. 다중 조인 시 중간 노드나 관계선(Relationship) 속성도 조회할 수 있습니다. 왼쪽(별칭)은 당신이 붙이는 이름이고, 오른쪽은 이름.속성 입니다. 그 이름은 안 주면 라벨에서 딴 것이라 아래 “노드에 붙는 이름” 을 먼저 보십시오. .select(order_id="order.order_id", due_date="schedule.yyyymmdd")
.order_by(*fields) 결과 정렬 순서를 지정합니다. 이름 앞에 - 를 붙이면 내림차순입니다. 여러 개를 주면 앞에서부터 우선합니다. .select() 를 함께 쓸 때는 그때 붙인 별칭으로 정렬합니다 — 돌려주지 않는 값으로는 정렬할 수 없습니다. .select() 가 없으면 돌려받는 노드의 속성 이름을 그대로 씁니다. .code() · .entity() 에서는 칼럼 이름 하나씩만 옵니다. .order_by("-work_date", "work_name")
.group_by(**fields) 무엇을 기준으로 묶을지 지정합니다. .select() 와 같은 모양이고, 묶음 기준은 결과에 그대로 실립니다. .aggregate() 와 함께 써야 합니다.group_by() 만 주면 묶이지 않고 마지막 노드가 그대로 반환됩니다(콘솔에 경고가 뜹니다). 특정 필드만 골라 받으려면 .select() 를 쓰십시오. .group_by(status="work.status")
.aggregate(**specs) 묶음마다 무엇을 셀지 지정합니다. Count · Collect · First · Sum 만 됩니다. 각 집계에 where 를 주면 그 조건에 맞는 건만 셉니다. .aggregate(works=Count("work", distinct=True))
.count(of=None, distinct=False) 건수 하나를 바로 받습니다(숫자). 목록을 끌어와 len() 하지 않습니다. 묶음별 건수는 이것이 아니라 .group_by() + .aggregate() 입니다. Zio.node("Work").where(status="완료").count()
.limit(val) 조회될 결과의 최대 행 수(Limit)를 명시적으로 제한합니다. .limit(10000)
.create(props) 지정한 속성을 가진 신규 노드를 생성(CREATE)합니다. 표시명(_display)은 SDK가 자동으로 계산해 넣습니다. Zio.node('Error').create({'error_code': 'ERR_E09'})
.merge(on_create, on_match) 있으면 두고 없으면 만듭니다. 무엇으로 같은 것이라 볼지는 .where() 가 정합니다(같음 조건만). on_create 는 새로 만들 때만, on_match 는 이미 있을 때만 씁니다. 먼저 조회해서 없을 때만 .create() 하는 것과 다릅니다 — 그 사이에 다른 실행이 같은 것을 만들면 노드가 둘이 됩니다. Zio.node("Holiday").where(yyyymmdd="2026-08-15").merge(on_create={"ymd_name": "광복절"})
.update(props) 조건에 일치하는 노드들의 속성값을 일괄 수정(SET)합니다. 표시명(_display)은 SDK가 자동으로 다시 계산합니다. Zio.node('Error').where(error_code='ERR_E09').update({'status': 'RESOLVED'})
.delete(detach=True) 조건에 일치하는 노드와 연계된 관계선들을 일괄 삭제(DETACH DELETE)합니다. Zio.node("Error").where(error_code="ERR_E09").delete(detach=True)
.merge_relation(rel_type, target, target_filters) 두 노드 간에 특정 관계선이 존재하지 않을 시 신규로 연결(MERGE)합니다. Zio.node('Machine').where(machine_cd='EQIM-0006').merge_relation('HAS_ERROR', 'Error', {'error_code': 'ERR_E09'})
.delete_relation(rel_type, target, target_filters) 두 노드를 이어주는 특정 관계선을 찾아 삭제(DELETE)합니다. Zio.node('Machine').delete_relation('HAS_ERROR', 'Error', {'error_code': 'ERR_E09'})
.consolidate_into(**target_props) 조건에 일치하는 노드들을 같은 라벨의 다른 노드 하나로 흡수 통합합니다. 관계선은 전부 대상 노드로 옮겨지고 원본 노드는 사라집니다. 표기가 갈린 노드를 하나로 모을 때 씁니다 — .update() 로 이름만 바꾸면 같은 이름의 노드가 둘로 늘어나고 관계선도 따라오지 않습니다. Zio.node('Client').where(client_name='GS THE FRESH').consolidate_into(client_name='GS더프레쉬')
.optional_out(...) / .optional_in(...) 같은 홉이지만 그 관계가 없어도 그 행은 남습니다(OPTIONAL MATCH). 인자는 .out() 과 같습니다. 주의: optional 홉 뒤에 그 노드를 .where() 로 거는 것은 필터가 아닙니다 — OPTIONAL MATCH 특성상 선행 행은 안 걸러지고 그 노드 속성만 null 이 됩니다(조건이 안 걸린 것과 같은 결과). 그 관계로 행을 걸러내려면 .out()/.in_()(필수 매칭) 을 쓰거나 아래 .where_out() 을 쓰십시오. (optional 홉에 where 를 걸면 콘솔에 경고가 뜹니다.) .optional_out("REQUIRES_ACTION", to="Action", from_="item")
.where_out(rel, to, **filters) / .where_in(...) 필수 매칭 홉 + 그 대상 필터를 한 줄로..out(rel, to).where(대상 필터) 와 같지만 커서를 옮겼다 되돌릴 필요가 없고, 필수 매칭이라 조건에 안 맞는 선행 행이 제대로 걸러집니다(위 optional 함정을 피함). 필터 접미사는 .where() 와 같습니다. .where_out("WRITTEN_BY", to="Author", user_name__contains="문호상")
.fetch() 위에서 조합된 체인을 바탕으로 안전한 Cypher 쿼리를 생성 및 실행하여 결과를 GraphResult 객체로 반환합니다. res = Zio.node("Inquiry").fetch()
.to_cypher() 조립된 Cypher 와 파라미터를 문자열로 돌려줍니다. 실행하지 않습니다. 체인이 어떻게 번역됐는지 눈으로 확인해 디버깅할 때 씁니다 — SSH 환경에 Neo4j 직접 도구가 없어도 됩니다. 내가 짠 체인의 번역본만 보여주며(스키마 덤프 아님), 여기로 raw 를 넣을 수는 없습니다. print(Zio.node("Worklog").where(subject__contains="점검").to_cypher())
.to_pandas() 조회된 GraphResult를 분석하기 쉬운 Pandas DataFrame으로 즉시 변환합니다. df = res.to_pandas()
.to_markdown() 조회된 GraphResult를 마크다운 표 형태의 문자열로 반환합니다. LLM에게 데이터를 전달할 때 유용합니다. md = res.to_markdown()

Cypher 를 글자 그대로 넘기던 자리입니다. 스킬이 도는 곳에는 엔진 소스도 DB 접속 정보도 없고 SDK 하나만이 문인데, 이것이 그 문을 지나지 않는 길이었습니다. 부르면 무엇으로 갈아타야 하는지 문장으로 알려 줍니다.

위의 함수들로 쓰십시오 — 그래프 구조가 바뀌어도 따라오지만, 손으로 쓴 Cypher 는 그날 조용히 어긋납니다.

이름 자리에는 바깥 값을 넣지 마십시오

섹션 제목: “이름 자리에는 바깥 값을 넣지 마십시오”

라벨·관계·속성 이름은 구조(스키마)를 가리키는 자리이고, .where() 의 오른쪽은 걸러 낼 값입니다. 이 둘은 다릅니다 — 값은 안전하게 파라미터로 넘어가지만, 이름은 질의에 글자 그대로 들어갑니다. 그래서 사용자 입력이나 LLM 이 뱉은 문자열을 이름 자리에 그대로 끼우면 안 됩니다.

# 위험 — 바깥에서 온 값을 라벨·이름 자리에
label = user_input # 예: LLM 이 고른 라벨
Zio.node(label).where(**{user_key: 1}) # label·user_key 가 그대로 질의에 박힌다
# 안전 — 이름은 내가 아는 것으로 고정, 바깥 값은 '값 자리'로
Zio.node("Inquiry").where(subject__contains=user_input) # user_input 은 파라미터로 나간다

이름 자리에 이상한 글자가 오면 SDK 가 막고 왜 안 되는지 알려 줍니다 (영문·숫자·밑줄이 아닌 라벨/관계, 이상한 속성 이름). 라벨을 고르게 하고 싶다면 Zio.node.labels()실제 있는 이름 목록을 받아 그 안에서만 고르십시오.

「이 문의에 달린 것 전부」를 보고 싶을 때 쓰십시오. .out() 은 한 번에 한 관계씩만 따라가므로, 관계가 아홉 가지면 문의 한 건에 아홉 번을 물어야 합니다. .with_relations() 는 그것을 한 질의로 접습니다.

# 옛 방식 — 문의 9건 × 관계 9가지 + 1
rows = Zio.node("Inquiry").where(sender_name__contains="전소영").fetch()
for r in rows.data:
for rel, to in [("HAS_PRIORITY", "Priority"), ("HAS_CATEGORY", "Category"), ...]:
Zio.node("Inquiry").where(message_id=r["message_id"]).out(rel, to=to).fetch()
# → 질의 82번 · 2.04초
# 지금
rows = (Zio.node("Inquiry")
.where(sender_name__contains="전소영")
.with_relations() # 관계를 안 적었습니다 — 설계서에 있는 것 전부
.fetch())
# → 질의 1번 · 0.03초
for r in rows.data:
print(r["subject"], r["relations"]["Priority"]) # [{'_display': '보통', ...}]

숫자는 helpdesk 인스턴스에서 실제로 잰 값입니다. 안이 CALL (n) { OPTIONAL MATCH … RETURN collect(…) } 이라 관계 수만큼 행이 곱해지지 않습니다 — 관계가 없는 것은 빈 목록으로 옵니다.

한 홉까지입니다. 관계의 관계는 안 따라갑니다. 깊이를 늘리면 노드 수만 개짜리 그래프에서 한 줄이 수천 줄로 불어납니다. 두 홉이 필요하면 .out() 을 쓰십시오.

골라 담기와 같이 못 씁니다..select() · .group_by() · .aggregate() · .out() 과 함께 쓰면 「무엇이 한 행인가」가 달라지므로 막아 두었습니다. 받은 뒤 파이썬에서 추리십시오.

이름이 겹치면 관계까지 붙습니다. 같은 라벨로 가는 길이 둘이면 칸 이름이 Priority 가 아니라 HAS_PRIORITY:Priority 가 됩니다 — 하나가 다른 하나를 덮지 않게 하려는 것입니다.

.where() 의 키 뒤에 __접미사 를 붙여 비교 방식을 바꿉니다. 접미사가 없으면 = 입니다. 온톨로지(Cypher)와 커스텀 테이블(SQL) 양쪽에서 같은 이름으로 동작합니다.

이름은 전부 당신의 설계에서 옵니다. SDK 것은 접미사뿐입니다

아래 표에 나오는 title · qty 같은 것은 예시일 뿐 SDK 가 정해 놓은 이름이 아닙니다. 노드명도 필드명도 산업별 온톨로지 설계에 있는 이름을 그대로 적습니다. SQL 을 쓸 때와 같은 자리입니다.

SELECT * FROM Worklog WHERE subject LIKE '%GS25%'
└ 노드 ┘ └ 필드 ┘ └ 비교 ┘

그래서 산업이 다르면 이름이 통째로 달라집니다.

Zio.node("Worklog").where(subject__contains="GS25")
Zio.node("Menu").where(menu_name__contains="치킨")
Zio.node("Gongjong").where(gongjong_name__contains="철근", floor__gte=3)

__ 뒤에 붙는 낱말만 SDK 가 아는 것입니다(아래 표). 안 붙이면 = 입니다. 조건을 여러 개 주면 모두 만족하는 것만 나옵니다(AND). 엔진에는 당신의 필드 이름이 하나도 들어 있지 않습니다. 설계에 없는 이름을 적으면 조회 결과가 비거나 오류가 납니다 — 오타는 그렇게 드러납니다.

접미사 의미 예시
__gt / __gte 초과 / 이상 .where(qty__gte=100)
__lt / __lte 미만 / 이하 .where(yyyymmdd__lte="2026-03-31")
__ne 같지 않음 .where(status__ne="CLOSED")
__in 목록 안에 있음. 값은 반드시 리스트(배열)입니다. 빈 리스트를 주면 아무것도 매칭되지 않습니다 .where(client_name__in=["GS25", "보나캠프"])
__startswith / __endswith ~로 시작 / ~로 끝남 .where(email__endswith="@zio.run")
__contains 포함 .where(title__contains="장애")
__contains_all / __contains_any 값은 리스트입니다. 모두 포함 / 하나라도 포함. 검색창에 친 말을 띄어쓰기로 끊어 찾을 때 씁니다 — 같은 칸에 __contains 를 두 번 걸 수는 없기 때문입니다. __contains_any 에 빈 리스트를 주면 아무것도 매칭되지 않습니다 .where(work_name__contains_all=["GS25", "포스"])
__isnull true 면 값이 없는 것, false 면 있는 것 .where(closed_at__isnull=True)

모르는 접미사는 오류로 돌려줍니다

__gtee 처럼 오타가 나면 오류와 함께 쓸 수 있는 접미사 목록을 알려줍니다. 전에는 조건이 통째로 사라져 필터 없이 전건이 조회됐습니다 — 오류도 안 났습니다. 속성 이름 자체에 __ 가 들어간다면 접미사로 읽히니 이름을 바꾸십시오.

셀 수 있는 것은 네 가지입니다. 함수 이름을 문자열로 받지 않습니다aggregate(cnt="count(*)") 처럼 표현식을 받으면 그것은 Cypher 를 조금 짧게 쓰는 것일 뿐, SDK 로 감싼 것이 아닙니다.

집계 무엇 예시
Count(of, distinct, where) 건수. of 를 안 주면 count(*) 입니다 Count("n", distinct=True)
Collect(of, distinct) 값을 목록으로 모읍니다 Collect("client.client_name", distinct=True)
First(of, distinct) 모은 것 중 첫 하나. 부서·직위처럼 하나뿐인데 경로 때문에 여러 번 걸리는 값을 눕힐 때 First("buseo.buseo_name", distinct=True)
Sum(of) 합. 수량을 더할 때 Sum("work.qty_done")
# 프로젝트별로 작업 수 · 완료 수 · 관련 고객사를 한 번에
(Zio.node("Work")
.out("BELONGS_TO", to="Project")
.optional_out("FOR_CLIENT", to="Client", from_="work")
.group_by(project="project.project_name")
.aggregate(works=Count("n", distinct=True),
done=Count("n", distinct=True, where={"status__in": ["완료"]}),
clients=Collect("client.client_name", distinct=True))
.order_by("-works", "project")
.limit(500)
.fetch())
# 건수 하나만 필요하면 지름길
Zio.node("Work").where(status="완료").count()

where 를 준 집계는 그 조건에 맞는 건만 셉니다. 조건 문법은 .where() 와 같습니다 — 비교 접미사도 그대로 씁니다. 집계한 뒤에는 묶음 기준과 집계 결과만 남습니다. 원래 노드의 속성은 이미 접혀서 없으므로 그것으로 .order_by() 하면 막힙니다.

프롬프트 안에서 목록 부르기 {{ }}

섹션 제목: “프롬프트 안에서 목록 부르기 {{ }}”

AI 에게 가는 글이면 어느 칸이든{{#노드.속성}} · {{$테이블.필드}} 를 적을 수 있습니다. 모델로 나가기 직전에 그 자리에서 실제 목록으로 바뀝니다.

적을 수 있는 곳 어디
도메인 · 역할 프롬프트 기초코드 → system_prompt 테이블의 각 행
데이터소스 설명 파이프라인 빌더 → 수집 설정의 description
필드 지시 · 출력 형식 파이프라인 스키마 → AI Prompt · Output Format
에이전트 노드 프롬프트 에이전트 빌더 → AI 모델 호출 노드

앞으로 생길 프롬프트 칸에서도 그대로 됩니다. 치환은 모델 호출 관문 한 곳에서만 일어나므로, 새 화면이 늘어도 따로 붙일 것이 없습니다.

표기 무엇이 들어오나
{{$job_type.type_name}} 기초코드에 정해둔 값 목록. role=prompt 필드가 있으면 설명도 함께
{{#Client.client_name}} 온톨로지에 이미 쌓인 노드 목록
{{#Client.client_name?limit=50}} 표시 상한. 안 적으면 200개까지. 잘리면 로그에 남습니다
{{$job_type.type_name?use=Y}} 필터. 조건에 맞는 행만
{{$job_type.type_name?use=Y&grade=A,B}} 조건 여러 개. & 로 잇고, 값에 쉼표를 쓰면 그중 하나(OR)입니다. 중괄호를 한 번 더 감싸지 마십시오{{{$…}}} 로 쓰면 바깥 중괄호가 글자로 남습니다
Output Format 에 이렇게 적으면
{
"client": "거래처 명칭. 아래에 같은 뜻이 있으면 그 이름을 그대로 쓰십시오: {{#Client.client_name}}",
"work_type": "다음 중 하나만: {{$job_type.type_name}}. 다른 값을 만들지 마십시오"
}
모델에게는 이렇게 갑니다
{
"client": "거래처 명칭. 아래에 같은 뜻이 있으면 그 이름을 그대로 쓰십시오: GS25, 한화비전, 쿠팡, ...",
"work_type": "다음 중 하나만: 설치, 교체, 철수, 점검, ... 다른 값을 만들지 마십시오"
}

지시를 직접 쓰십시오

#(쌓인 것)과 $(정해둔 것)은 지시가 반대입니다 — 하나는 “없으면 새로 만들어라”, 다른 하나는 “목록 밖으로 나가지 마라”. 목록 옆에 어떻게 쓰라는 말을 직접 적으십시오. 시스템이 정해 주면 두 지시가 섞입니다.

참조를 못 찾거나 결과가 0건이면 {{...}}그대로 남습니다. 빈 값으로 지우면 “다음 중 하나만: “ 같은 토막 문장이 남아 모델이 헤맵니다.

노드에 붙는 이름 (alias)

.select() · .order_by() · 집계 · from_ 이 서로를 가리킬 때 쓰는 이름입니다. 안 주면 라벨에서 땁니다Zio.node("Worklog")worklog, .out("HAS_WORK", to="Work")work 입니다.

  • 기본 이름: 라벨의 첫 글자를 소문자로. Workwork, WorkTypeworkType.
  • 같은 라벨을 두 번 지나면: work, work2 로 뒤에 숫자가 붙습니다. 헷갈릴 자리면 alias= 로 직접 붙이십시오.
  • 관계선 속성이 필요하면 <대상이름>_rel 로 읽습니다 — work_rel.qty.

이름을 붙이는 자리는 SQL·Cypher 와 같습니다 — 대상을 꺼내는 곳입니다. (FROM worklog wl, (w:Work))

Zio.node("Worklog") # 이름: worklog
.out("HAS_WORK", to="Work") # 이름: work
.optional_out("FOR_CLIENT", to="Client") # work 에서 이어짐
.optional_out("WRITTEN_BY", to="Author", from_="worklog") # 일지에서 갈라짐
.select(work="work.work_name", client="client.client_name",
who="author.user_name")

갈래가 나뉘면 from_ 로 어디서 갈라지는지 밝히십시오. 안 주면 직전에 도착한 노드에서 이어집니다 — 위에서 from_ 를 빼면 작성자를 고객사에서 찾으러 가서 아무것도 안 나옵니다. 없는 이름을 주면 쓸 수 있는 이름을 알려주며 막습니다. 이름이 위치가 아니라 라벨에서 나오므로, 중간에 한 홉을 끼워 넣어도 나머지 이름은 그대로입니다. 예전에는 t0·t1 이라 그 뒤가 전부 밀렸습니다.

노드 흡수 통합(.consolidate_into()) 규칙

  • 대상은 같은 라벨입니다. Zio.node("Client") 로 시작했으면 대상도 Client 입니다. 라벨을 넘나드는 통합은 지원하지 않습니다.
  • 속성은 대상 노드가 이깁니다. 같은 속성이 양쪽에 있으면 대상 값이 남고, 대상에 없는 속성은 흡수되는 쪽에서 넘어옵니다.
  • 관계선은 전부 옮겨집니다. 같은 상대와 같은 종류로 중복되면 하나로 합쳐집니다.
  • 대상 노드가 없으면 이름만 바꿉니다. 흡수할 상대가 없으니 .update() 와 같아집니다. 결과는 어느 쪽이든 “그 이름의 노드 하나”입니다.
  • 조건에 맞는 노드가 없으면 아무 일도 안 합니다. 몇 번을 다시 돌려도 결과가 같습니다(멱등).
  • 되돌릴 수 없습니다. 흡수된 노드는 사라집니다. 무엇이 합쳐질지 .fetch() 로 먼저 확인하는 절차를 스킬에 두십시오.

반환값은 살아남은 대상 노드입니다. 합쳐진 것이 하나도 없으면 빈 결과가 돌아옵니다 — .data 가 비었는지로 실제 변경 여부를 판단할 수 있습니다.

표시명(_display) 자동 계산 규칙

모든 노드는 그래프 화면의 원 안에 표시될 이름을 _display 속성으로 가집니다. 스킬 작성자는 이 속성을 직접 넣을 필요가 없습니다. .create().update() 가 호출되면 SDK가 온톨로지 설계서에 정의된 Display Property 를 읽어 자동으로 계산해 저장합니다. 온톨로지 수집 경로(writer.py)와 동일한 계산기(graphdb/display.py)를 공유하므로 결과가 항상 같습니다.

  • 단일 속성명: 설계서에 verdict 라고 지정하면 그 속성값이 그대로 표시명이 됩니다. → _display = "위험"
  • 중괄호 템플릿: [{sequence}차]{by} 처럼 지정하면 속성값으로 치환합니다. → _display = "[1차]mail.example.com"
  • 값이 없는 속성은 빈 문자열로 치환됩니다. 설계서에 Display Property 가 없으면 _display 를 건드리지 않습니다.
  • 계산에 실패해도 예외를 던지지 않습니다. 표시명 때문에 스킬 본연의 저장 작업이 중단되지 않도록 설계되어 있습니다.

규칙은 status=live 설계서에서 가져옵니다. api 컨테이너 안에서는 PostgreSQL을 직접 읽고, Zero-Trust 샌드박스에서 실행되는 스킬은 GET /api/sdk/ontology/display-rules 로 규칙만 받아옵니다. (설계서 전체가 아니라 { 노드명: Display Property } 만 내보냅니다.) 프로세스당 1회 캐시하므로 설계서를 수정한 뒤에는 스킬을 재시작해야 새 규칙이 적용됩니다.

설계서는 소급 적용되지 않습니다. Display Property 를 바꿔도 이미 저장된 노드의 _display 는 그대로 남고, 그 시점 이후에 저장·수정되는 노드부터 새 규칙이 반영됩니다. 설계서는 지나간 데이터를 다시 쓰는 도구가 아니라 앞으로 발생하는 건에 대한 적용 기준표입니다.

from zio_ontology import Zio
# ---------------------------------------------------------
# [샘플 1] 기초: 단순 조건 필터링
# ---------------------------------------------------------
# 'Inquiry(문의)' 노드 중 상태가 'OPEN'인 것들 모두 가져오기
open_inquiries = Zio.node("Inquiry").where(status="OPEN").fetch().data
# ---------------------------------------------------------
# [샘플 2] 중급: 관계(Relationship)를 타고 넘어가는 탐색
# ---------------------------------------------------------
# 이름이 '신달수'인 고객(Customer)이 작성한(CREATED) 문의(Inquiry) 내역
user_inquiries = (
Zio.node("Customer")
.where(name="신달수")
.out("CREATED")
.node("Inquiry")
.fetch()
.data
)
# ---------------------------------------------------------
# [샘플 3] 고급: 데이터프레임(Pandas) 연동을 통한 통계 분석
# ---------------------------------------------------------
def calculate_defect_rate():
# 1. 쿼리 실행 후 즉시 Pandas DataFrame으로 변환 (.to_pandas() 사용)
df = (
Zio.node("Factory")
.where(factory_code="F-001")
.out("PRODUCED")
.node("Product")
.where(status="DEFECTIVE")
.fetch()
.to_pandas()
)
if df.empty:
return "불량 내역이 없습니다."
defect_summary = df.groupby('department')['defect_type'].count()
return defect_summary.to_dict()
# ---------------------------------------------------------
# [샘플 4] 초고급: 다중 조인, 범위 필터 및 프로젝션(Select)
# ---------------------------------------------------------
def get_machine_capacities():
df = (
Zio.node("CapacityDate")
.where(yyyymmdd__gte="2026-03-01", yyyymmdd__lte="2026-03-31")
.out("available_for", to="MachineCapacity")
.select(
machine_cd="machineCapacity.machine_cd",
work_date="capacityDate.yyyymmdd",
shift_hour="machineCapacity.shift_hour",
shift_start="machineCapacity.shift_start",
shift_end="machineCapacity.shift_end"
)
.limit(10000)
.fetch()
.to_pandas()
)
return df
# ---------------------------------------------------------
# [샘플 5] 신규: Fluent CUD API 및 다중 홉 Optional 탐색
# ---------------------------------------------------------
def update_machine_error_manual():
# 특정 설비에 신규 HAS_ERROR 관계 생성 (MERGE)
(Zio.node("Machine")
.where(machine_cd="EQIM-0001")
.merge_relation("HAS_ERROR", "Error", {"error_code": "ERR_E04"}))
# 해결 시 HAS_ERROR 관계 제거
(Zio.node("Machine")
.delete_relation("HAS_ERROR", "Error", {"error_code": "ERR_E04"}))