본문 바로가기

Elastic

[Elastic] 지식 그래프란?

Relation DataBase(테이블 형식의 데이터 베이스)와 다르게 지식을 그래프 형식으로 표현하여 새로운 정보 추론과 여러 가지 속성을 확장할 수 있는 새로운 종류의 그래프 데이터 베이스이다.

  • 검색 품질을 향상 시킬 수 있다. -> 검색을 확장할 수 있다.
  • 아이디어, 컨셉들의 관계를 그래프로 나타낸다.
  • Entity들 사이에서 어떤 관계가 있는지 파악할 수 있다.
  • 화살표로 연관 관계를 표현해 준다. -> 여러 가지 Entity에 형성되는 메타 데이터를 나타낼 수 있다.

이런 지식 그래프 없이 Relational Database로 Entity들의 관계를 표현하려면 무수히 많은 테이블이 필요하다.

** 지식 그래프는 Entity들을 하나씩 정리해 주고, Entity들 사이에서의 관계를 표현해 줌으로써 매우 효율적인 데이터 저장소 역할을 할 수 있다.

예를 들어, 검색 엔진에 '어느 대륙에 숭례문이 위치해 있나요?'라는 문장을 입력하면 숭례문 -> 대한민국 -> 아시아 -> 대륙 확인 후, 아시아라는 검색 결과가 빠르게 가능하다.

즉, 검색 문장에 존재하지 않는 정보들을 관계를 따라가면서 검색 결과를 가져올 수 있다.


* 지식 그래프 데이터 베이스 종류

  • Neo4j
  • RedisGraph

Relational Database에 저장이 가능하지만 더 빠르게 연관 관계를 따라갈 수 있게 해주는 전문적인 그래프 데이터 베이스를 사용한다.

 

* 지식 그래프 사용 사례

  • 관계를 표현하여 가장 관련된 정보들을 먼저 보여주고자 할 때
  • 하나의 카테고리에 연관된 정보들을 보여주고자 할 때
  • 여러 가지의 요소들이 합쳐져 어떤 정보를 나타낼 때
  • 한 가지 정보를 토대로 연관된 모든 정보를 보여주고자 할 때

** 정보를 검색하고 결과를 반환할 때, 연관 관계를 고려하여 추론을 하고 추론 결과를 바탕으로 사용자에게 더 적합한 정보를 응답할 수 있다. -> 연관 관계 사이의 점수 중 낮은 점수는 Relevancy가 낮고, 높은 점수는 Relevancy가 높다는 것을 활용한다.

 

키워드 확장 예제

** 이 방법은 지식 그래프를 사용한 사례는 아니지만 지식 그래프의 개념을 확인해 볼 수 있는 예제이다.

1. 인덱스 생성

 

| 코드

import requests
import json

url = "http://localhost:9200/products"

payload = json.dumps({
    "settings": {
        "index": {
            "number_of_shards": 1,
            "number_of_replicas": 1
        },
        "analysis": {
            "analyzer": {
                "analyzer-name": {
                    "type": "custom",
                    "tokenizer": "keyword",
                    "filter": "lowercase"
                }
            }
        }
    },
    "mappings": {
        "properties": {
            "id": {
                "type": "long"
            },
            "content": {
                "type": "text"
            },
            "title": {
                "type": "text"
            },
            "url": {
                "type": "text"
            },
            "image_file": {
                "type": "text"
            },
            "post_date": {
                "type": "date"
            },
            "modified_date": {
                "type": "date"
            },
            "shipped_from": {
                "type": "text"
            },
            "keywords": {
                "type": "text"
            },
            "meta_data": {
                "type": "object"
            }
        }
    }
})
headers = {
    'Content-Type': 'application/json'
}

response = requests.request("PUT", url, headers=headers, data=payload)

print(response.text)

다음 코드를 사용하여 새로운 Index를 생성한다.

 

2. 데이터 넣기

 

| 코드

* 위키미디아 파일에서 데이터 처리(https://dumps.wikimedia.org/kowiki/)

import mwparserfromhell
import re
import xml.etree.ElementTree as etree

def loadWikimedia(source_file):
    tree = etree.parse(source_file)
    root = tree.getroot()
    namespace = getNamspace(root.tag)
    kg = {}
    for page in root.findall('./' + namespace + 'page'):
        title = page.find(namespace + 'title').text
        page_content = page.findall(
            './' + namespace + 'revision/' + namespace + 'text')
        entry = {}
        if len(page_content) > 0:
            wikicode = mwparserfromhell.parse(page_content[0].text)
            templates = wikicode.filter_templates()
            for template in templates:
                #print(template.name)
                for param in template.params:
                    value = stripWikilinksForText(str(param.value)).strip()
                    if len(value) > 0:
                        entry[str(param.name).strip()] = value
        kg[title] = entry
    return kg

# 위키미디아 스타일 링크에서 텍스트만을 추출하는 helper function
def stripWikilinksForText(wikilink):
    return re.sub(r'\[\[(.+?)\|.+?\]\]', r'\1', wikilink).replace('[[', '').replace(']]', '')

# XML 의 namespace 를 찾아 돌려준다.
def getNamspace(tag):
    return '{' + tag.split('}')[0].strip('{') + '}'

kg 변수에는 1차적인 맵들만 존재한다.

import ast
import datetime
import hashlib
import json
import mysql.connector
import requests
import kg.kg_loader as kg_loader

# SQL 에 연결하여 제품 페이지들을 추출하여 ProductPost array 로 돌려주는 함수
def getPostings():
    kg_source = 'kg/kowiki-20210701-pages-articles-multistream-extracted.xml'
    wiki_kg = kg_loader.loadWikimedia(kg_source)
    cnx = mysql.connector.connect(user='root',
                                password='비밀번호',
                                host='localhost',
                                port=9906,
                                database='데이터베이스')
    cursor = cnx.cursor()

    query = ('쿼리')
    cursor.execute(query)

    posting_list = []
    for () in cursor:
        print("Post {} found. URL: {}".format(id, url))
        meta_data = {}
        keywords = []
        # 1. 각 단어마다 위키미디아에 관련 정보를 찾는다.
        for n_gram in title.split():
           if n_gram in wiki_kg:
               print("found entry for " + n_gram)
               meta_data = {**meta_data, **wiki_kg[n_gram]}
               subspecies = maybeGetSubspecies(wiki_kg[n_gram])
               if subspecies != None:
                   keywords.append(subspecies)
        product = ProductPost(id, content, title, url,
                              post_date, modified_date, assumeShippingLocation(meta_value), image, meta_data, " ".join(keywords))
        posting_list.append(product)

    cursor.close()
    cnx.close()
    return posting_list

# 2. 위키미디아 데이타에 특정한 attribute을 추출
# Subspecies field 가 해당 wiki data 에 존재하면 돌려주고, 아니면 None 을 돌려준다.
def maybeGetSubspecies(wiki_page):
    sub_species_key = u'과'
    if sub_species_key in wiki_page:
        return wiki_page[sub_species_key]
    return None

# 엘라스틱서치에 출력하는 함수
def postToElasticSearch(products):
    putUrlPrefix = 'http://localhost:9200/products/_doc/'
    headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
    for product in products:
        id = str(product.id)
        print(id)
        r = requests.put(putUrlPrefix + id, data=json.dumps(product.__dict__,
                         indent=4, sort_keys=True, default=json_field_handler), headers=headers)
        if r.status_code >= 400:
            print("There is an error writing to elasticsearch")
            print(r.status_code)
            print(r.json())

# datetime -> iso 형식으로
def json_field_handler(x):
    if isinstance(x, datetime.datetime):
        return x.isoformat()
    raise TypeError("Unable to parse json field")

# 제품 페이지를 표헌하는 class
class ProductPost(object):
  def __init__(self, id, content, title, url, post_date, modified_date, shipped_from, image_file, meta_data, keywords):
    self.id = id
    self.content = content
    self.title = title
    self.url = url
    self.post_date = post_date
    self.modified_date = modified_date
    self.shipped_from = shipped_from
    self.image_file = image_file
    self.meta_data = meta_data
    self.keywords = keywords

p = getPostings()
postToElasticSearch(p)
  • mwparserfromhell -> 위키미디아의 데이터를 쉽게 parser 해주는 하나의 모듈
  • DataBase에 저장된 정보에 위키미디아에서 가져온 정보의 '과' 항목을 keyword 필드에 저장한다.

해당 파일을 실행해서 Elastic Search에 데이터를 저장한다.

http://localhost:9200/products/_search

Elastic Search에 저장된 데이터를 확인했을 때, 다음과 같은 결과를 확인할 수 있다.

keyword에 국화과가 저장된 것을 확인할 수 있다.

 

3. 키워드 검색

http://localhost:9200/products/_search?q=국화과 

* 국화과라는 키워드로 검색할 경우 id 14번이 검색 결과로 도출되었다. -> 키워드가 확장된 것을 확인할 수 있다.

 

** 키워드를 추가하였을 뿐, 효율적인 그래프 탐색을 할 수는 없다.

* 실전에서는 문서 자체에 키워드를 확장하지 않는다. -> Neo4j나 RedisGraph를 사용해서 키워드 검색 쿼리가 왔을 때, 키워드 관련 속성을 확장해서 검색 결과를 제공한다.

* 지식 그래프를 통해서 검색 결과를 향상시킬 수 있고, 개인화, 여러 가지 새로운 추론을 나타냄으로써 새로운 경쟁력을 키울 수 있다.

'Elastic' 카테고리의 다른 글

[Elastic] 검색 랭킹  (0) 2022.10.08
[Elastic] Elastic Search란?  (1) 2022.10.05
[Elastic] 검색 엔진이란?  (0) 2022.10.01