내장된 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_PRIORITY 와 hasSentiment 가 한 설계서에 함께 있습니다.
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 로 이어집니다.
관계를 따라 정방향(→)으로 한 홉 갑니다. 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() 를 함께 쓸 때는 그때 붙인 별칭으로 정렬합니다 — 돌려주지 않는 값으로는 정렬할 수 없습니다. .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가 자동으로 계산해 넣습니다.
있으면 두고 없으면 만듭니다. 무엇으로 같은 것이라 볼지는 .where() 가 정합니다(같음 조건만). on_create 는 새로 만들 때만, on_match 는 이미 있을 때만 씁니다. 먼저 조회해서 없을 때만 .create() 하는 것과 다릅니다 — 그 사이에 다른 실행이 같은 것을 만들면 노드가 둘이 됩니다.
조건에 일치하는 노드들을 같은 라벨의 다른 노드 하나로 흡수 통합합니다. 관계선은 전부 대상 노드로 옮겨지고 원본 노드는 사라집니다. 표기가 갈린 노드를 하나로 모을 때 씁니다 — .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 를 걸면 콘솔에 경고가 뜹니다.)
필수 매칭 홉 + 그 대상 필터를 한 줄로..out(rel, to).where(대상 필터) 와 같지만 커서를 옮겼다 되돌릴 필요가 없고, 필수 매칭이라 조건에 안 맞는 선행 행이 제대로 걸러집니다(위 optional 함정을 피함). 필터 접미사는 .where() 와 같습니다.
위에서 조합된 체인을 바탕으로 안전한 Cypher 쿼리를 생성 및 실행하여 결과를 GraphResult 객체로 반환합니다.
res = Zio.node("Inquiry").fetch()
.to_cypher()
조립된 Cypher 와 파라미터를 문자열로 돌려줍니다. 실행하지 않습니다. 체인이 어떻게 번역됐는지 눈으로 확인해 디버깅할 때 씁니다 — SSH 환경에 Neo4j 직접 도구가 없어도 됩니다. 내가 짠 체인의 번역본만 보여주며(스키마 덤프 아님), 여기로 raw 를 넣을 수는 없습니다.
라벨·관계·속성 이름은 구조(스키마)를 가리키는 자리이고, .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() 로 실제 있는 이름 목록을 받아 그 안에서만 고르십시오.
__ 뒤에 붙는 낱말만 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 처럼 오타가 나면 오류와 함께 쓸 수 있는 접미사 목록을 알려줍니다. 전에는 조건이 통째로 사라져 필터 없이 전건이 조회됐습니다 — 오류도 안 났습니다. 속성 이름 자체에 __ 가 들어간다면 접미사로 읽히니 이름을 바꾸십시오.
갈래가 나뉘면 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 = "위험"
값이 없는 속성은 빈 문자열로 치환됩니다. 설계서에 Display Property 가 없으면 _display 를 건드리지 않습니다.
계산에 실패해도 예외를 던지지 않습니다. 표시명 때문에 스킬 본연의 저장 작업이 중단되지 않도록 설계되어 있습니다.
규칙은 status=live 설계서에서 가져옵니다. api 컨테이너 안에서는 PostgreSQL을 직접 읽고, Zero-Trust 샌드박스에서 실행되는 스킬은 GET /api/sdk/ontology/display-rules 로 규칙만 받아옵니다. (설계서 전체가 아니라 { 노드명: Display Property } 만 내보냅니다.) 프로세스당 1회 캐시하므로 설계서를 수정한 뒤에는 스킬을 재시작해야 새 규칙이 적용됩니다.
설계서는 소급 적용되지 않습니다. Display Property 를 바꿔도 이미 저장된 노드의 _display 는 그대로 남고, 그 시점 이후에 저장·수정되는 노드부터 새 규칙이 반영됩니다. 설계서는 지나간 데이터를 다시 쓰는 도구가 아니라 앞으로 발생하는 건에 대한 적용 기준표입니다.