Ontology & Knowledge Graph

완전히 새로운 시도: Neo4j에서 OWL 활용하기

hsystems 2026. 8. 6. 10:34
728x90
반응형

[커뮤니티 콘텐츠로서 본 게시물은 특정 작성자의 견해와 의견을 반영한 것으로, Neo4j의 공식적인 입장이 반드시 반영되는 것은 아닙니다.]

왜 이 일을 하고 싶나요?

웹 온톨로지 언어(OWL)는 꽤 오랫동안 사용되어 왔고, 다양한 의미론적 애플리케이션에 활용되고 있어요.

온톨로지는 무료로 사용할 수 있고, 개발자가 실제 시나리오에 대한 모델을 만드는 데 도움을 주죠. SWRL 규칙과 Hermit 또는 Pellet 같은 추론기를 사용해서 인스턴스화, 결합, 그리고 강화할 수 있어요.

이러한 데이터 표현을 생성하는 이유는 여러 가지가 있어요. Natural Language Processing, 도메인 간 데이터 재사용, 또는 상황화 등이 그 예시죠. 이렇게 얻은 데이터는 기술 자료에 저장되고, SPARQL 쿼리를 사용해서 검색할 수 있어요.

그런데, 이미 이런 게 다 있다면, 이걸 Graph Database와 결합하는 게 무슨 의미가 있을까요?

SPARQL은 서로 다른 온톨로지를 동시에 사용하고, 잘 알려진 SQL과 유사하다는 장점이 있지만, 약점도 분명히 존재해요.

트리플 스토어는 대부분의 SPARQL 애플리케이션의 시작점인데, 관계형 데이터베이스에 비해서 디스크 공간을 많이 차지해요. 게다가, 아주 큰 데이터 세트의 경우에는 속도도 느려지죠.

반면에, 는 "그냥" 트리플이 아니라 전체 그래프를 저장해요. 배우기도 쉽고, 사용하기 쉬운 쿼리 언어와 사용자가 그래프를 쉽게 검색하고 탐색할 수 있는 웹 기반 그래픽 인터페이스를 제공하죠.

쿼리 속도도 빠르고, 확장성도 아주 뛰어나서 더 큰 데이터 세트를 처리할 수 있어요.

일반적인 경우와 마찬가지로, 이상적인 솔루션은 없어요. 결국 모든 것은 다음 사항들을 고려해서 특정 사용 사례에 따라 달라진다고 봐야 해요:

    • 들어오는 데이터의 양, 빈도, 그리고 연결성
    • 속도와 크기의 중요성
    • Database에서 실행되는 Query 유형

놀이터

여기에는 PROV-O 온톨로지가 있는데, 활동, 에이전트, 그리고 엔터티 간의 인과관계와 영향을 모델링하는 데 사용돼요. 이 개념은 상당히 추상적이지만, 객체의 기원(그리고 객체의 일생 동안 객체에 영향을 미쳤을 수 있는 요소)과 관련된 여러 질문에 답하는 데 유용하답니다.

PROV-O 온톨로지는 정말 다양한 분야에 적용할 수 있어요. 예를 들어 소셜 네트워킹("Peter가 매시업을 작성하는 데 영향을 준 블로그 게시물의 작성자는 누구였을까요?")이나 실험("실험이 실패하기 전에 마지막으로 액세스한 사람은 누구였으며 언제 액세스했을까요?") 같은 것들이죠.

PROV-O는 Bonfire 프로젝트에서도 사용되고 있는데, 다중 사이트 클라우드 실험 및 테스트 시설이라고 해요.

여기서 사람은 (에이전트) 리소스(엔티티)를 이용해서 실험을 하죠. 인프라 수준에서는 컴퓨팅 `Node`, 스토리지, 가상 네트워크(엔티티)를 생성하고 사용하고 파괴하는 (활동) 과정을 거쳐요. 실험이 끝나면 추가 분석을 위해 가상 머신에서 결과(엔티티)를 다운로드하고요.

이러한 결과는 수많은 활동과 행위자에 의해 영향을 받기 때문에, 결과가 어떻게 생성되었는지, 누가 관여했는지, 왜 다른 결과와 다른지 판단하기 어려울 때가 많아요. 하지만 출처를 사용하면 이런 질문에 답할 수 있다는 사실! 정말 흥미롭죠?

준비

BonFIRE에서 데이터는 RabbitMQ의 JSON 메시지 세트로 표현되는데요, 예를 들면 이런 형태에요:

{"timestamp":1375801302,"eventType":"state.shutdown",
"objectType":"compute","objectId":"/locations/server1/computes/123","groupId":"group1","userId":"bert"}

위 예시에서는 bert가 server1에 있는 컴퓨팅 `Node` 123을 종료하는 상황을 나타내요. 이 메시지는 Java 클래스에 담겨서 트리플로 변환되죠 ("수동으로"요!).

위 예시의 단일 메시지를 사용해서 다음과 같은 여러 트리플을 만들 수 있어요.

:Action_state.shutdown_1375801302 rdf:type :Action
:Compute_/locations/server1/computes/123 rdf:type :Compute
:Compute_/locations/server1/computes/123 prov:invalidatedBy :Action_state.shutdown_1375801302
:Experimenter_Bert rdf:type :Experimenter
:Experimenter_Bert prov:wasAssociatedWith :Action_state.shutdown_1375801302
...

여기서 사용되는 접두사는 트리플을 가져올 온톨로지에서 정의돼요.

만약 메시지가 온톨로지로 바로 들어가야 한다면, 위 단계는 필요 없어요. 대신 OWL API를 사용해서 개인, 속성 등을 생성할 수 있죠.

하지만 이렇게 트리플로 변환하면 트리플 형식을 사용하는 한 모든 종류의 소스에서 데이터를 읽을 수 있는 인터페이스 역할을 하게 돼요. 만약 OWL API를 사용했다면 데이터가 변경될 때마다 코드를 바꿔야 했을 거예요.

그다음, 이러한 트리플은 OWLRDFConsumer 클래스를 사용해서 온톨로지에 추가할 수 있어요. 이렇게 하면 데이터를 풍부하게 만들기 위해 추론기를 호출할 수 있는 온톨로지에 트리플이 추가되는 거죠.

지금까지는 특별한 건 없죠? 진짜 흥미로운 부분은 추론이 이루어진 후부터 시작이에요!

그래프 얻기

이제 메모리에는 온톨로지 자체와 트리플에서 나온 개인을 포함하는 온톨로지 객체가 있어요. 이걸 그냥 `Knowledge Graph`에 저장할 수도 있지만, 그랬다면 여기서 이 글을 읽고 있진 않겠죠? 😉

온톨로지는 그래프에요. 여기에는 최상위 `Node` (owl:Thing)와 이를 확장하는 클래스가 있죠. 클래스에 속하는 객체와 객체를 연결하는 객체 속성도 있고요. 개인은 `Node` 속성, 관계 속성 또는 관계 유형으로 표현될 수 있는 데이터 속성과 주석을 가질 수 있어요.

온톨로지 가져오기는 정말 간단해요.

1단계

필요한 유일한 객체는 이전에 생성된 온톨로지 객체에요.

파일에서 로드할 수도 있지만, 별 차이는 없어요.

private void importOntology(OWLOntology ontology) throws Exception {
     OWLReasoner reasoner = new Reasoner(ontology);
          if (!reasoner.isConsistent()) {
               logger.error("Ontology is inconsistent");
               //throw your exception of choice here
               throw new Exception("Ontology is inconsistent");
          }
          Transaction tx = db.beginTx();
          try {

2단계

owl:Thing `Node`를 나타내는 Neo4j에서 시작 `Node`를 만들어요. 이게 바로 우리가 만들 그래프의 루트 `Node`가 되는 거죠.

               Node thingNode = getOrCreateNodeWithUniqueFactory("owl:Thing");

3단계

온톨로지에 정의된 모든 클래스를 가져와서 그래프에 추가해요.

          for (OWLClass c :ontology.getClassesInSignature(true)) {
               String classString = c.toString();
               if (classString.contains("#")) {
                    classString = classString.substring(
                    classString.indexOf("#")+1,classString.lastIndexOf(">"));
               }
                    Node classNode = getOrCreateNodeWithUniqueFactory(classString);

4단계

슈퍼클래스가 있는지 확인하고, 있다면 연결해요. 없다면 owl:Thing에 다시 연결하면 돼요.

직접 슈퍼클래스에만 링크하도록 하는 거 잊지 마세요! rdf:type 속성을 표현하는 데 사용되는 관계 유형은 "isA"라는 사용자 정의 유형이에요.

                    NodeSet<OWLClass> superclasses = reasoner.getSuperClasses(c, true);
                    if (superclasses.isEmpty()) {
                         classNode.createRelationshipTo(thingNode,
                         DynamicRelationshipType.withName("isA"));
                         } else {
                         for (org.semanticweb.owlapi.reasoner.Node<OWLClass>
                         parentOWLNode: superclasses) {
                              OWLClassExpression parent =
                              parentOWLNode.getRepresentativeElement();
                              String parentString = parent.toString();
                              if (parentString.contains("#")) {
                                   parentString = parentString.substring(
                                   parentString.indexOf("#")+1,
                                   parentString.lastIndexOf(">"));
                              }
                              Node parentNode =
                              getOrCreateNodeWithUniqueFactory(parentString);
                              classNode.createRelationshipTo(parentNode,
                                  DynamicRelationshipType.withName("isA"));
                         }
                    }

5단계

이제 각 수업마다 모든 individual을 확보해볼까요? `Node`를 생성하고 이를 상위 `class`에 다시 연결하는 거예요.

                    for (org.semanticweb.owlapi.reasoner.Node<OWLNamedIndividual> in
                         : reasoner.getInstances(c, true)) {
                         OWLNamedIndividual i = in.getRepresentativeElement();
                         String indString = i.toString();
                         if (indString.contains("#")) {
                              indString = indString.substring(
                                   indString.indexOf("#")+1,indString.lastIndexOf(">"));
                         }
                         Node individualNode =
                         getOrCreateNodeWithUniqueFactory(indString);
                         individualNode.createRelationshipTo(classNode,
                         DynamicRelationshipType.withName("isA"));

6단계

각 individual에 대해 모든 object property와 모든 data property를 가져와요. `Node` property 또는 `relationship`으로 `Graph`에 추가하면 된답니다. 주장된 공리뿐만 아니라 모든 공리를 얻어야 해요.

                         for (OWLObjectPropertyExpression objectProperty:
                         ontology.getObjectPropertiesInSignature()) {
                              for
                              (org.semanticweb.owlapi.reasoner.Node<OWLNamedIndividual>
                              object: reasoner.getObjectPropertyValues(i,
                              objectProperty)) {
                                   String reltype = objectProperty.toString();
                                   reltype = reltype.substring(reltype.indexOf("#")+1,
                                   reltype.lastIndexOf(">"));
                                   String s =
                                   object.getRepresentativeElement().toString();
                                   s = s.substring(s.indexOf("#")+1,
                                   s.lastIndexOf(">"));
                                   Node objectNode =
                                   getOrCreateNodeWithUniqueFactory(s);
                                   individualNode.createRelationshipTo(objectNode,
                                   DynamicRelationshipType.withName(reltype));
                              }
                        }
                        for (OWLDataPropertyExpression dataProperty:
                        ontology.getDataPropertiesInSignature()) {
                              for (OWLLiteral object: reasoner.getDataPropertyValues(
                              i, dataProperty.asOWLDataProperty())) {
                                   String reltype =
                                   dataProperty.asOWLDataProperty().toString();
                                   reltype = reltype.substring(reltype.indexOf("#")+1, 
                                   reltype.lastIndexOf(">"));
                                   String s = object.toString();
                                   individualNode.setProperty(reltype, s);
                              }
                         }
                    }
               }
               tx.success();
          } finally {
               tx.finish();
          }
     }

자, 이제 끝났어요! 이제 정말 재미있는 부분을 살펴볼까요? 바로 온톨로지를 쿼리하는 거예요!

그래프워킹

이건 현재 데이터베이스에 있는 그래프의 모습이에요.

여기에는 온톨로지뿐만 아니라 "자연스러운" 형태로 표현된 모든 individual과 property도 포함되어 있어요. 이제 쿼리를 시작해 볼 수 있겠죠?

특정 VM(entity)의 수명 주기 동안 어떤 일이 발생했는지 알아보기 위한 간단한 쿼리부터 시작해볼까요?

START e=node:name(name="experiment123"), ag=node:name(name="Agent")
MATCH e-[r:hadActivity]->ac-->a-[:isA*]->ag
RETURN distinct e.name as experiment, type(r) as relationship, a.name as agent
ac.name as activity, ac.startedAtTime as starttime, ac.endedAtTime as endtime
ORDER BY starttime

...아니면 두 실험이 언뜻 보기에는 똑같아 보일 때, 어떻게 다른지 알아보기 위해 좀 더 복잡한 패턴 매칭을 시도해 볼 수도 있어요. 상상력만이 한계일 뿐이죠!

결론

Protégé는 간단한 시각화와 SPARQL 쿼리 실행 기능을 제공해요.

Neo4j는 Cypher를 사용해서 가져온 온톨로지를 훨씬 더 직관적으로 쿼리할 수 있게 해줘요. 결국 온톨로지는 그래프니까요! 게다가 webadmin 인터페이스를 사용하면 그래프를 훨씬 효과적으로 "탐색"할 수 있답니다.

온톨로지 가져오기가 시간에 민감한 작업이 아니기 때문에, 이 경우에는 시간이 크게 문제가 되지 않아요. 실험이 완료된 후에 딱 한 번만 수행되고, 전체 온톨로지를 가져오거든요.

몇 시간 분량의 실험 데이터가 포함된 온톨로지의 경우, 가져오는 데 몇 초밖에 걸리지 않아요. 그래프를 가져온 후에는 쿼리가 엄청 빠르기 때문에, 온톨로지를 분석하고 시각화하는 데 정말 훌륭한 도구가 될 수 있죠.

Graph Database에 대해 더 자세히 알고 싶으신가요? 아래를 클릭해서 O'Reilly의 무료 eBook을 다운로드하고, 미션 크리티컬 애플리케이션에 그래프 기술을 사용하는 방법을 알아보세요.

  • Natural Language Processing
  • ontology
  • owl
  • owl API
  • prov-o
  • sparql
  • sql
  • Triple Stores

에이치시스템즈LogTree는 Neo4j 기반 GraphRAG 플랫폼으로, 데이터를 자동으로 지식그래프화하고 자연어 질의로 즉시 답을 제공합니다.

👉 에이치시스템즈 홈페이지

728x90
반응형