금석문 앞에 선 답사객은 안다 — 글자를 읽는 일과 뜻을 읽는 일은 다르다는 것을. 세상 데이터의 좋은 몫은 인간이 인간을 위해 자연어로 쓴 문서인데, 자연어에는 표의 구조도 계층의 구조도 없어 프로그램으로 부리기가 여간 까다롭지 않다. 이 장은 NLP가 문서에서 캐낸 구조가 그래프에 얼마나 자연스럽게 안착하는지, 그리하여 글자(strings)가 아니라 사물(things)을 찾는 검색이 어떻게 가능해지는지를 답사한다. 과연 여기서도 진리는 하나다 — 그래프는 아는 만큼 찾아 주는 것이다.
검색의 근래 역사는 실로 눈부시다. 그러나 그 초창기 — 불과 20년 전이요, 놀랍게도 많은 서비스에서는 오늘까지도 — 의 검색 엔진이란 자연어 문서 집합 위의 단순한 인덱스, 때로는 사람 손으로 큐레이션한 인덱스에 지나지 않았다. 키워드를 쳐 넣고 인덱스 항목과 맞아떨어지기를 바라는 것이 사용법의 전부였던 것이다. 인간 언어의 풍성한 어휘 변형 앞에서 이것이 얼마나 무력한가 — country를 찾으면 복수형 countries를 담은 문서들은 고스란히 지나쳐 버린다. 여기에 대체 철자(더러는 오타), 약어, 두문자까지 얹으면 일은 조금도 쉬워지지 않으니, 검색 결과가 종종 신통치 않았던(않은) 것은 당연한 귀결이다. 문제의 나머지 반쪽은 랭킹이다 — 이 기초적인 정경에서 엔진이 할 수 있는 최선이래야 검색어 출현 횟수 같은 단순 적합도 지표로 줄 세우는 정도였다.
검색은 누구나 매일 쓴다 — 하드 드라이브의 문서를, 온라인 상점의 상품을, 스트리밍의 노래를, 그리고 웹의 거의 모든 것을. 이 웹 검색을 두 전선에서 혁신한 것이 Google이다. 먼저 PageRank — 페이지 사이의 기존 연결(하이퍼링크)을 지렛대 삼아 검색 결과를 랭킹하는 알고리즘으로, 페이지의 적합도를 검색어 포함 여부만이 아니라 얼마나 많은 적합한 페이지가 그 페이지로 링크하는가에 근거 지었다. 몇 해 뒤에는 검색을 강화하는 지식 그래프를 도입했다. 오늘의 Google 검색은 정교한 알고리즘과 데이터의 총체 위에 서 있지만, PageRank와 Google Knowledge Graph는 여전히 웹을 이해하는 근본으로 남아 있다.
검색 엔진도 세월과 함께 진화했다 — 와일드카드 검색, 퍼지 검색, 논리식 키워드 결합, TF-IDF(단어 빈도–역문서 빈도) 같은 랭킹 기법들이 키워드 검색을 보강했다. 그러나 인덱스 기반 검색을 지식 그래프로 보완할 때 현대 엔진은 한 차원 위로 올라선다. 지식 그래프는 동의어, 인접 키워드, 도메인 관련 개념을 해소하는 데 필요한 "지능"을 담는다. 나아가 검색을 질문 응답의 영토로 데려간다 — 주어진 검색에 의미적으로 적합한 문서를 돌려줄 뿐 아니라, 자연어 질문에 정교한 응답을 내놓는 데도 쓰이는 것이다. 이 장은 앞의 문제를 다루고, 뒤의 문제는 13장이 탐사한다.
문서의 자연어 텍스트는 엔터티와 그들 사이의 관계를 서술함으로써 정보를 전하는 인간의 방식이다. 그런데 같은 엔터티와 관계라도 서술하는 길은 여럿이다 — 인간 언어는 풍요롭되 모호하고 맥락적이기 때문이다. 그러니 문서의 자동 처리(여기서는 검색)를 텍스트 표현 위에 세우는 것은 부정확하고 비효율적일 수밖에 없고, 처리를 더 높은 추상 수준으로 끌어올려야 한다. 목표는 Google 블로그에서 Amit Singhal이 선언한 그대로 — "글자가 아니라 사물을(things, not strings)" 찾는 것이다.
첫걸음은 자연어 처리의 가장 기초적인 형태 — 텍스트에 "숨은" 엔터티의 추출이다. 이 과정을 흔히 텍스트 주석(text annotation)이라 부르고, 그 NLP 기법의 이름이 개체명 인식(NER, named-entity recognition)이다. NER이 무엇을 해 주는지는 한 문장이면 족하다 — "The New York Times is a daily newspaper and its headquarters is on the west side of Midtown Manhattan in New York City." NER은 조직 "The New York Times"와 위치 "New York"이 같은 단어로 서술되었음에도 둘 다 언급되었음을 모호함 없이 밝혀낸다. 이 결과를 지식 그래프 기반 검색 엔진에 명시해 두면 위치 기반 검색과 회사 기반 검색이 모두 가능해진다 — 텍스트 검색이라면 "New York"이라는 글자의 일치만 찾을 뿐, 그것이 어느 쪽을 가리키는지는 끝내 가리지 못했을 일이다.
실습의 재료는 표 12-1의 기사 목록이다 — Twitter 의장 Patrick Pichette가 Neo4j 이사회에 합류했다는 기사와, JupiterOne이 Neo4j 기반 보안 도구 Starbase를 공개했다는 기사(Slack CSO Sean Catlett의 논평 포함). 필요한 부품은 둘이다(그림 12-1) — 자연어에서 엔터티를 추출할 소프트웨어와, 그 결과를 연결된 엔터티로 저장할 그래프 플랫폼. 첫 예제에서는 Hugging Face가 추출을, Neo4j가 저장과 분석을 맡는다.
from transformers import pipeline ner_pipe = pipeline("ner", aggregation_strategy="simple") title = """Twitter chair Patrick Pichette joins graph data platform Neo4j board of directors.""" for entity in ner_pipe(title): print(entity) # 기본 모델은 bert-base-NER — 일반 영어에는 훌륭. # 고도로 기술적인 도메인이라면 그 도메인용으로 # 훈련된 NER 모델을 써야 한다
{'entity_group': 'ORG', 'score': 0.9961534,
'word': 'Twitter', 'start': 0, 'end': 7}
{'entity_group': 'PER', 'score': 0.9972605,
'word': 'Patrick Pichette', 'start': 14, 'end': 30}
{'entity_group': 'ORG', 'score': 0.8314789,
'word': 'Neo4j', 'start': 62, 'end': 67}
# NER은 엔터티 목록에 더해 대개 둘을 준다 —
# type: 사람인가 위치인가 조직인가
# (bert-base-NER은 LOC·ORG·PER·MISC 4종)
# salience: 텍스트 내 상대적 중요도 —
# 중심적(높음)인가 곁가지(낮음)인가
from neo4j import GraphDatabase driver = GraphDatabase.driver("bolt://your.db.ip:7687", auth=("neo4j", "pwd")) entity_list = [] for entity in ner_pipe(title + fragment): entity_list.append(entity) cypher_query = ''' MERGE (a:Article { url:$url}) ON CREATE SET a.title= $title, a.text= $frg MERGE (p:Person { name: $author}) MERGE (a)-[:has_author]->(p) WITH a UNWIND $entityList AS entity MERGE (e:Entity { name: entity.word , type: entity.type }) MERGE (a)-[:references { salience: entity.score }]->(e) ''' with driver.session(database="neo4j") as session: session.write_transaction( lambda tx: tx.run(cypher_query, url=url, author=author, title=title, frg=fragment, entityList=[{ "word": x["word"], "type":x["entity_group"] } for x in entity_list])) driver.close() # 기사–엔터티의 연결이 명시적이고 가중된(salience) 그래프가 된다 (그림 12-2). # 완전판은 기사 목록을 순회하며 이 로직을 기사마다 적용한다
// "이 문서는 무엇에 관한 것인가?" MATCH (a1:Article)-[:references]->(e:Entity) WHERE a1.url = $url RETURN e.name AS entityName, e.type AS entityType // 같은 패턴이 역질문에도 답한다 — // "어떤 기사들이 이 엔터티를 언급하는가?" MATCH (a:Article)-[:references]->(e:Entity) WHERE e.name = $entityName AND e.type = $entityType RETURN a.url AS articleLink, a.title AS articleTitle
MATCH (c:Entity { name: $entityName, type: $entityType }) WITH c, date() - duration("P1Y") AS startdate UNWIND range(0,11) AS increment MATCH (c)<-[:references]-(a:Article) WHERE startdate + duration("P"+ (increment - 1) +"M") < a.published < startdate + duration("P"+ increment +"M") RETURN startdate + duration("P"+ increment +"M") AS date, count(a) // 단어 수 세기와는 격이 다른 엔터티 빈도 — // 시대별 인기, 흥망성쇠의 추이가 읽힌다
기사의 내용을 명시적으로 만든 엔터티의 무리를 갖는 순간 검색의 선택지가 대폭 넓어진다. 문서가 무엇에 관한 것인지 열거하고, 특정 엔터티를 언급한 기사를 거꾸로 찾고, 단어 수준이 아니라 엔터티 수준의 인기도와 트렌드를 잰다 — 전 기간 최고 인기 엔터티든, 특정 기간의 부침이든, 문서 출현 빈도를 세는 것만으로 시계열이 채워지는 것이다.
문서 주석이 띄운 그래프는 검색을 높은 추상 수준(단어가 아니라 엔터티)으로 올려놓았다. 이제 이 그래프의 구조를 분석하면(그림 12-3) 문서 사이의 유사성을 매우 높은 정밀도로 캐낼 수 있음을 보게 된다. 콘텐츠 기반 추천 시스템은 추천할 아이템의 피처에 중심을 둔다 — "You may also like X." 소비자 행동의 분석에 집중하는 협업 필터링 — "Customers like you also bought/read/watched X." — 과 대구를 이루는 접근이다. 즐겨 찾는 가구 이커머스에서 흰 원목 책상을 클릭했더니 화면 한켠에 흰 원목 책상들이 "비슷한 상품"으로 줄지어 선 풍경 — 낯설지 않을 것이다. 이 접근의 외삽은 쉽다. 아이템은 기사요, 피처는 거기서 추출한 엔터티인 것이다.
이제 필요한 것은 피처 일치들을 조합해 유사도 지표를 얻는 체계적인 방법 하나뿐이다 — 그 지표로 줄 세우면 추천할 상위 n개가 나온다. 가구 이커머스라면 색의 일치가 재질의 일치보다 중한지 가리기 어려워 가중치를 도메인 전문가가 정해야 했겠지만, 여기서는 문서 내 엔터티의 salience가 쓸 만한 힌트를 준다. 출발점은 간명하다 — 기사들은 공유하는 엔터티로 서로 이어져 있다.
MATCH (a1:Article)-[:references]->(e:Entity) <-[:references]-(a2:Article) WHERE a1.url = $url1 AND a2.url = $url2 RETURN e.name AS entityName, e.type AS entityType // 표 12-1의 두 기사라면 결과는 하나 — // 둘을 잇는 유일한 엔터티는 Neo4j Inc.다
MATCH (a1:Article)-[r1:references]->(e:Entity) <-[r2:references]-(a2:Article) WHERE a1.url = $url1 AND a2.url = $url2 RETURN sum(r1.salience * r2.salience) AS similarity_metric // 엔터티 겹침이 클수록 유사도가 크다 — // 단, 모든 엔터티가 같게 세어지지는 않는다. // 관계에 실린 salience가 저울추다
지극히 단순한 그래프 패턴이지만 지극히 강력한 지표다 — 독자가 비슷한 주제를 계속 읽고 싶어 하리라는 가정 아래 "다음에 읽을 것"을 권할 수 있게 된다. 그리고 두 아이템이 얼마나 비슷한지를 재는 일 못지않게 중요한 것이 정확히 어떻게 관련되는가의 서술이다 — 그것이 있어야 적합할 뿐 아니라 설명 가능한(explainable) 추천이 되는데, 지식 그래프에서는 이 또한 손쉽게 닿는 곳에 있다.
MATCH (a1:Article)-[r1:references]->(e:Entity) <-[r2:references]-(recommendation) WHERE a1.url = $url1 RETURN recommendation, sum(r1.salience * r2.salience) AS similarity_metric, collect(e) AS explanation ORDER BY similarity_metric DESC LIMIT 5 // collect(e)가 유사성을 설명하는 // 공유 엔터티의 열거를 함께 내놓는다
// 유사도를 관계로 구체화 (그림 12-4) MATCH (a1:Article)-[r1:references]->(e:Entity) <-[r2:references]-(a2:Article) WITH a1, a2, sum(r1.salience * r2.salience) AS similarity_metric MERGE (a1)-[:similar { metric: similarity_metric }]-(a2) // 지표가 영속되면 추천은 한결 가벼워진다 MATCH (a1:Article)-[r:similar]-(recommendation) WHERE a1.url = $url1 RETURN recommendation, r.metric AS similarity_metric ORDER BY similarity_metric DESC LIMIT 5
이런 질의를 큰 그래프에서 돌리려면 단일 트랜잭션 대신 배치가 답이다 — Cypher 절
CALL { … } IN TRANSACTIONS를 쓰거나 APOC 라이브러리의 주기 실행 메서드를 쓰면
된다.
문서 유사도는 다음에 보여 주고 읽히고 팔 것을 권하는 데 요긴하나, 닻(anchor)이 있어야 한다 — 앞의 추천들은 선택된 기사(또는 최근 읽은 기사들)를 닻으로 삼았다. 그런데 지식 그래프에 닻이 하나도 없는 콜드 스타트라면? 홈 화면을 채울 첫 추천은 어디서 오는가. 그럴 때는 지금 유행하는 토픽을 내거는 것이 합리적이다 — Example 12-6의 트렌드 질의와 닮은꼴로, 최근 6개월간 가장 많이 언급된 개념 열을 꼽으면 된다.
MATCH (c:Concept)<-[:refers_to]-(a:Article) WHERE date() - duration("P6M") < a.datetime < date() RETURN c.label, count(a) AS freq LIMIT 10 // 콜드 스타트에는 질문 자체가 없으니 // 의미적으로 풍부한 답 대신, 연결된 엔터티와 // 시맨틱 검색의 세계로 나아갈 발판을 준다
Example 12-12만큼 계산이 싸지는 않아도 콜드 스타트를 누그러뜨릴 길은 더 있다 — 기존 그래프에 유사도 알고리즘을 돌려 두는 것이다(원문은 "as per Chapter 5"로 앞 장의 기법을 가리킨다). 그 결과가 있으면 사용자가 특정 토픽·기사에 닻을 내리는 즉시 비슷한 것들을 제안할 수 있다. 나아가 설계를 잘하면 유사도 알고리즘을 세컨더리 서버에서 주기적으로 돌리고 결과만 프로덕션 그래프에 직접 기록할 수도 있다.
NER 기반 문서 주석은 옳은 방향으로 내디딘 큰 걸음이었으되, 두 가지 뚜렷한 한계를 지닌다. 하나, 엔터티의 비중의성이 해소되지 않는다 — 한 문서가 United Kingdom을 말하고 다른 문서가 약어 UK를 쓰면, NER은 둘 다 위치로 알아보긴 해도 둘이 같은 것을 가리킨다는 사실은 끝내 판정하지 못한다. 둘, 엔터티를 잇고 도메인 지식을 담는 관계가 없다 — 지리의 예를 이어 가면, 셋째 문서가 Wales를 말할 때 위치 엔터티는 식별되어도 Wales와 United Kingdom 사이의 PART_OF 관계는 검색·추천 시스템이 알 길이 없는 것이다.
이 한계를 넘는 길은 엔터티를 조직화 원리(organizing principle)의 알려진 항목에 매칭해 시맨틱 지식 그래프를 이루는 것이다. 조직화 원리란 온톨로지일 수도, 개념 체계일 수도, 어휘일 수도 있으며, 특정 도메인의 엔터티들과 그들이 서로 맺는 관계를 기술한다(그림 12-5 — 명시적 도메인 지식이 데이터 탐사의 새 길들을 낸다). 엔터티를 공유 개념 체계의 유일하게 정의된 항목에 매칭하는 과정을 흔히 개체명 연결(named-entity linking) 또는 엔터티 비중의성 해소(entity disambiguation)라 부른다. 이 일은 종종 추출 과정에 묶여, 목표 조직화 원리를 아는 특화 추출기가 미리 매칭된 엔터티를 내놓는 식으로 수행된다. 앞 절의 범용 NER 엔진을 쓸 때는 휴리스틱에 기반한 커스텀 알고리즘으로 연결해야 하는데, 그 사례는 이 장 뒷부분에서 다루고 먼저 더 일반적인 접근을 본다.
표준 조직화 원리의 채택은 업계마다 다르다. 가장 왕성한 곳 하나가 제약·헬스케어·생명과학이다 — 생명과학 학술지·서적 색인용 포괄 통제 어휘 MeSH(Medical Subject Headings)와 인간 질병의 표준화 온톨로지 Disease Ontology가 잘 알려진 성숙한 본보기다.
이번 예제의 자연어 패키지는 GCP(Google Cloud Platform) Natural Language API다. Hugging Face와 매우 비슷한 엔터티 추출을 제공하되 흥미로운 덤이 하나 있다 — 추출한 엔터티에 Wikipedia URL을 비롯한 메타데이터를 붙여 주는 것이다. 이 URL이 엔터티를 조직화 원리의 항목에 모호함 없이 매칭할 열쇠가 된다.
from google.cloud import language_v1 client = language_v1.LanguageServiceClient .from_service_account_json('services.json') text = u"Twitter chair Patrick Pichette joins graph data platform Neo4j board of directors" document = language_v1.Document( content=text, type_=language_v1.Document.Type.PLAIN_TEXT ) response = client.analyze_entities( request={"document": document}) for entity in response.entities: print(entity)
name: "Patrick Pichette" type_: PERSON metadata { key: "wikipedia_url" value: "https://en.wikipedia.org/wiki/Patrick_Pichette" } salience: 0.6320402026176453 name: "Twitter" type_: ORGANIZATION metadata { key: "wikipedia_url" value: "https://de.wikipedia.org/wiki/Twitter" } salience: 0.22149702906608582 name: "Neo4j" type_: ORGANIZATION metadata { key: "wikipedia_url" value: "https://en.wikipedia.org/wiki/Neo4j" } salience: 0.020158693194389343 # Google Knowledge Graph ID 등 다른 요소는 # 명료성을 위해 생략되었다
연결 문제의 답이 완성되려면 조직화 원리 쪽에도 같은 메타데이터(Wikipedia 페이지 참조)가
있어야 한다. 반갑게도 Wikidata나 DBpedia 같은 공개 온톨로지·지식 베이스는 교차 참조가 되어 있어 항목에
Wikipedia 페이지 참조를 담고 있다. Example 12-15는 엔터티 Neo4j(wd:Q1628290)를
기술하는 Wikidata 그래프의 단편이다 — Wikipedia 페이지 https://en.wikipedia.org/wiki/Neo4j가 바로 그
엔터티에 관한 것(schema:about)이라는 진술이 들어 있다.
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . @prefix schema: <http://schema.org/> . @prefix wdp: <http://www.wikidata.org/prop/direct/> . @prefix wd: <http://www.wikidata.org/entity/> . wd:Q1628290 rdfs:label "Neo4j"@de ; schema:description "graph database implemented in Java"@en ; wdp:P31 wd:Q595971 . <https://en.wikipedia.org/wiki/Neo4j> schema:about wd:Q1628290 .
Wikidata는 모든 요소를 수치로 식별한다. Neo4j가 wd:Q1628290이듯, Neo4j가 속한
부류인 graph database도 저 나름의 번호 wd:Q595971을 갖는
것이다(wdp:P31 = instance of). 족보에 항렬이 있듯 지식 베이스에는 번호가 있는 셈이다.
이 과정 전체를 Cypher만으로 해낼 수도 있다 — Neo4j의 APOC 라이브러리가 GCP Natural Language API를 부르는 래퍼 메서드를 제공하니, 앞의 예제들처럼 외부 코드를 쓸 일이 없는 것이다. 걸음은 셋이다. 첫째, 기사를 노드로 들인다 — uri·title·body·datetime 네 컬럼의 articles.csv를 파싱해 레코드마다 노드를 만든다. 둘째, 조직화 원리를 들인다 — W3C 표준(OWL·SKOS·RDFS)으로 기술되어 있다고 보고 neosemantics(n10s) 라이브러리로 가져온다. 셋째, 기사 본문에 엔터티 추출을 돌리고 그 결과를 조직화 원리에 잇는다.
LOAD CSV WITH HEADERS FROM 'file:///articles.csv' AS row CREATE (a:Article { uri: row.uri}) SET a.title = row.title, a.body = row.body, a.datetime = datetime(row.date)
CALL n10s.graphconfig.init({ handleVocabUris: "IGNORE", classLabel: "Concept", subClassOfRel: "broader"}) CALL n10s.skos.import.fetch ("path-to-file-containing-organizing-principle", "RDF/XML")
CALL apoc.periodic.iterate( "MATCH (a:Article) WHERE a.processed IS NULL // (1) RETURN a", "CALL apoc.nlp.gcp.entities.stream([item in $_batch | item.a], { // (2) nodeProperty: 'body', key: $key }) YIELD node, value SET node.processed = true // (3) WITH node, value UNWIND value.entities AS entity // (4) WITH entity, node WHERE NOT (entity.metadata.wikipedia_url is null) MATCH (c:Concept {altLabel: entity.metadata.wikipedia_url}) // (5) MERGE (node)-[rt:refers_to]->(c) SET rt.salience = entity.salience", // (6) {batchMode: "BATCH_SINGLE", batchSize: 10, params: {key: $key}}) YIELD batches, total, timeTaken, committedOperations RETURN batches, total, timeTaken, committedOperations; // 생산자–소비자의 반복 구조다. 앞 블록이 batchSize 10의 배치를 흘려보내면 뒤 블록이 처리한다. // (1) 아직 처리되지 않은 기사만 순회하고 (3) processed 플래그로 기사당 한 번만 처리되게 한다. // (2) 배치의 기사마다 GCP NL API를 호출 — 분석할 텍스트가 담긴 속성명(body)과 API 키를 넘긴다. // (4) API가 돌려준 엔터티들을 펼치고, wikipedia_url이 있는 것만 남겨 // (5) 그 URL을 altLabel로 가진 Concept 노드에 매치한 뒤 // (6) Article을 refers_to 관계로 잇고 salience를 가중치로 싣는다
이 과정의 결실은 앞 절과 사뭇 닮은 그래프이되, 이제 조직화 원리가 얹은 명시적 의미의 겹이 더해져 있다. 엔터티는 개념 체계로 유일하게 식별 가능해지고 — 그 체계는 공적이든 사적이든, 사용 사례나 업종 특화든, 부서용이든 전사용이든 무방하다 — 엔터티들은 의미 있고 설명 가능한 방식으로 더 촘촘히 얽혀 탐사와 분석의 새 길들을 연다.
이제 어떤 의미적 활용이 가능한가. "NoSQL database management systems"에 관한 기사를 찾아보라 — 어느 기사도 그 용어들을 명시적으로 입에 올리지 않았음에도 결과가 나온다. 어째서인가. 조직화 원리를 그래프에 들임으로써 Neo4j는 그래프 데이터베이스요, 그래프 데이터베이스는 (컬럼 스토어, 문서 데이터베이스, 키-값 스토어와 더불어) NoSQL 데이터베이스의 일종이라는 사실이 명시되었기 때문이다(그림 12-7). 이것이 흔히 말하는 시맨틱 검색이다 — 검색을 모호한 자연어 텍스트로부터 독립시켜 잘 정의된 도메인 엔터티 위에 세운 것이며, 곧 "글자가 아니라 사물을"의 원리를 실제로 구현한 것이다. 아는 만큼 보인다 했던가 — 그래프가 알기에, 검색이 보는 것이다.
MATCH (c:Concept) <-[:broader*0..]-(sc)<-[:refers_to]-(article:Article) WHERE c.prefLabel = "NoSQL database management system" RETURN article.title AS searchResult // (c)<-[:broader*0..]-(sc) 패턴이 조직화 원리를 // 재귀 항해하여 임의 깊이의 하위 개념을 모두 찾고, // 거기 직·간접으로 이어진 Article을 전부 돌려준다
이 접근은 여러 조직화 원리로 확장된다 — 카탈로그의 상품이 여러 차원으로 분류되듯 예제의 기사들도 그러하니, 시맨틱 검색과 유사도 양쪽에 상보적인 길이 여럿 나는 것이다. 실로 강력한 패턴이며, 지식 그래프가 웹 검색을 벼렸듯 비즈니스 정보 시스템의 검색을 벼린다.
이 아이디어들이 실전에서 움직이는 곳들이다. 헬스케어에서는 퇴원 요약, 임상 노트, 과학 문헌, 임상시험 프로토콜 등 — 하나같이 비정형 텍스트인 문서들 — 에 주석을 달아 환자 데이터의 고급 검색과 분석에 쓰며, 이는 환자 360도 뷰로 가는 디딤돌이기도 하다. 뉴스·미디어에서는 인물·사물·장소·사건 같은 핵심 엔터티의 검색 가능한 메타데이터를 기사에 결부한다 — "watch/read next…"의 추천·개인화가 가장 뻔한 쓰임이되 전부는 아니다. 오디오·비디오의 전사본에 주석을 달면 타임라인의 특정 순간과 언어로 줌인하는 식의, 더 정교하고 비선형적인 미디어 소비가 열린다.
지금까지는 엔터티 연결을 NER 소프트웨어가 맡아 주는 경우였다. 그렇지 못할 때는 얼마간의 수동 매칭이 필요하다 — 비중의성 해소의 기법에는 텍스트 분석과 그래프 분석이 있는데, 한 접근은 조직화 원리의 개념들에 키워드 집합이나 정규 표현식을 주석해 두고 NLP가 추출한 엔터티의 식별자에 적용하는 것이다.
// 키워드 기반 CREATE (c:Category { name: "Person", alts: ["Human","Pax", "Pers"]}) // 정규식 기반 (포함/배제 목록) CREATE (c:Category { name: "COVID-19", inc: ["covid.*","corona.*"], excl: ["sars.*"]})
MATCH (e:Entity { name: $entityname }) MATCH (c:Category) WHERE e.name IN c.alts WITH e, collect(c) AS candidate_cats WITH e, selection_logic(candidate_cats) AS selected_cat MERGE (e)-[:references]->(selected_cat) // selection_logic은 다중 매치에서 가장 그럴듯한 // 후보를 고르는 문제의 추상 — 무작위 선택부터, // 도메인 휴리스틱이나 거리·중심성 같은 구조 피처로 // 계산한 문맥 적합 가중치까지 무엇이든 될 수 있다
이 접근의 한계는 자명하다 — 키워드·정규식의 정밀도에 강하게 기댄다는 것이다. 연결을 맡을 커스텀 코드도 얼마간 써야 한다. 그러나 특화 추출기가 없는 도메인에서 조직화 원리의 은혜를 입는 실용적인 지름길임에는 틀림이 없다.
시맨틱 검색 지식 그래프는 공상 과학이 아니라 과학의 사실이다. 1960년대 아폴로 달 탐사까지 거슬러 오르며 반세기 넘는 집단 지식을 아우르는 NASA의 "lessons learned"는 성공한 임무와 시험 같은 긍정의 경험, 실패와 사고 같은 부정의 경험에서 길어 올린 지식의 보고(寶庫)다 — 가히 인류 우주 답사의 사초(史草)라 할 만하다.
2000년대 초의 LLIS는 항해도 검색도 어려워 심각하게 저활용되었다. 가장 큰 도전은 데이터 접근 자체였다 — 데이터가 그룹·부서·프로그램·제품마다 사일로로 흩어져 있었고, 무엇도 데이터를 연결하거나 상호 참조조차 하지 않았다. 직원 8만 명의 기관에서 데이터의 양·다양성·속도가 시스템을 짓누르고 있었다. 그러나 희망이 있었다 — lessons learned에는 풍부한 메타데이터가 결부되어 있어, NASA는 (엔터티를 추출하여) 자기 지정 카테고리에 근거해 토픽들을 상관시킬 수 있었다. 각 교훈을 그 토픽과 함께 보고, 토픽 사이의 상관까지 보게 되니, 사용자는 추세를 읽을 수 있게 되었다 — NASA 엔지니어들이 재앙적 결과를 예방하는 데 잠재적으로 보탬이 되는 능력이다. 오늘로 빨리 감으면, NASA의 LLIS는 이제 풍부한 지식 그래프다. 검색은 더 빨라졌고 결과는 더 적되 훨씬 더 적합하다. 지식 그래프의 근본 역할은 둘이다 — 먼저 (5장에서처럼) 사일로에 흩어진 소스들에서 데이터를 모으고, 그다음 이 장의 기법으로 엔터티를 통해 데이터를 서로 잇는다. 이 교훈 데이터베이스는 이미 상당한 가치를 낳았다. 한 NASA 엔지니어의 말이다 — "이것이 화성 임무 계획을 향한 연구개발에서 우리에게 최소 1년과 200만 달러 이상을 절약해 주었다." 당신의 시맨틱 검색 지식 그래프가 그만큼 훌륭하다면, 그것이 당신을 어디까지 데려갈지 누가 알겠는가.
인덱스 키를 짚는 대신 개념으로 검색하는 능력은 혁명적이다. 시맨틱 검색 지식 그래프가 그것을 가능케 한다 — 조직은 제 지식을 더 많이 간직하게 되고, 그 지식에 닿는 길은 지식 그래프를 통해 곧아진다. 답사의 끝에서 되새기는 이 장의 요체는 결국 처음의 그 한 구절이다. 글자를 좇지 말고 사물을 좇을 것 — 그래프는 아는 만큼 찾아 주는 것이다.
검색의 내력이 한계를 일렀다. 인덱스와 키워드의 시절은 어휘 변형(country/countries)·오타·약어에 무력했고 랭킹은 출현 횟수가 고작이었다. Google은 PageRank(하이퍼링크 랭킹)와 Knowledge Graph로 웹 검색을 두 번 벼렸고, 둘은 여전히 근본으로 남아 있다.
글자에서 사물로. NER(텍스트 주석)이 "The New York Times"(조직)와 "New York"(위치)을 같은 글자에서 가려낸다. Hugging Face(bert-base-NER, LOC·ORG·PER·MISC)로 추출하고 type·salience와 함께 references 관계로 그래프에 실으면, 정문(문서가 무엇에 관한가)과 역문(누가 이 엔터티를 말하는가), 그리고 엔터티 수준의 트렌드 시계열이 열린다.
유사도는 salience의 가중합. 아이템=기사, 피처=엔터티 — Σ r1.salience × r2.salience 한 줄이 콘텐츠 기반 추천의 심장이고, collect(e)가 설명 가능성을 보탠다. 즉석 계산과 similar 관계 구체화(배치는 IN TRANSACTIONS/APOC)를 골라 쓰고, 콜드 스타트에는 트렌딩 토픽이나 미리 돌려 둔 유사도 알고리즘으로 응수한다.
조직화 원리가 의미를 입힌다. NER의 두 한계(UK≠United Kingdom의 비중의성, Wales–UK 관계의 부재)는 개체명 연결로 넘는다 — GCP NL API의 Wikipedia URL과 Wikidata의 schema:about이 두 걸음의 다리를 놓고(wd:Q1628290), MeSH·Disease Ontology 같은 성숙한 체계가 업계의 본보기다. 특화 추출기가 없으면 키워드·정규식 주석과 selection_logic의 수동 매칭이 지름길이 된다.
아무도 안 쓴 말을 찾아내는 검색. Cypher 한길의 3단 파이프라인(적재 → n10s 수입 → apoc.nlp.gcp 추출·연결) 끝에, "NoSQL"을 입에 올린 기사가 없어도 broader*0.. 사다리가 Neo4j → 그래프 DB → NoSQL을 거슬러 결과를 찾는다. NASA는 이 길로 사일로의 교훈을 이어 화성 계획에서 1년과 $2M을 아꼈다.
시맨틱 검색에서 한 층 더 오르면 자연어의 이해다. 다음 답사지에서는 검색과 NLP가 어떻게 동행하는지를 실제 사례로 본다 — 지식 그래프에 말을 거는 법, Talking to Your Knowledge Graph(13장)다.