카테고리 보관물: 검색엔진 Search Engine

엘라스틱서치 필드값으로 집계하기 ElasticSearch Aggregation Query (group by)

ElasticSearch는 RDMS가 아닙니다. DB가 아닙니다.

넓은 의미로는 데이터베이스라고 할 수는 있습니다. 데이터를 넣고 분석하고 삭제하는 등의 관리를 할 수 있으니까요. 하지만 RDBMS가 가진 많은 기능을 제공하지는 않고 그 중에 어낼리틱스 기능또한 제공되지 않는 것 중 하나입니다.

하지만 DB처럼 사용하려는 사용자 요구가 많고 엘라스틱서치 측에서도 계속해서 그런 어낼리틱스 기능을 지원하려고 하고 있습니다.

집계 쿼리(Aggregation Query)는 DB의 “group by”같은 것을 말합니다.

집계 쿼리를 사용할 때 주의할 것

  • 엘라스틱서치의 집계 쿼리는 부하가 심합니다.
  • 집계하려는 필드가 색인에 들어있지 않은 경우는 더 심합니다.

쓰고 싶으믄 크지 않은 인덱스에 대해서 하고 가능하면 대상 필드에 색인을 걸어 두는 것이 좋습니다.

쿼리 예제

aggregation 쿼리의 예제입니다. “createDate”라는 날짜 포맷의 컬럼을 기준으로 몇개의 문서가 있는지 집계하는 쿼리입니다. range는 날짜 구간입니다. 아래와 같은 형식으로 쿼리를 작성해서 응용해서 쓰면 됩니다.

{
    "query": {
      "range": {
        "createdDate": {
          "gt": "20220423",
          "lte": "20220523"
        }
      }
    },
    "aggs": {
        "group_by_state": {
          "terms": {
            "field": "createdDate"
          }
        }
    }
}

위와 같이 하면 집계 결과와 검색결과가 같이 나오는데 검색결과에 모든 필드가 포함됩니다. 필요 없는 것을 빼고 비교적 간략하게 받으려면 _source를 지정해주면 조금 더 낫습니다.

{
    "query": {
      "range": {
        "createdDate": {
          "gt": "20220423",
          "lte": "20220523"
        }
      }
    },
    "aggs": {
        "group_by_state": {
          "terms": {
            "field": "createdDate"
          }
        }
    },
    "_source": [
      "createdDate",
      "doc_id"
    ]
}

아예 hits에 결과를 안넣고 받으려면 size를 0으로 해주면 됩니다.

{
  "size": 0,
  "query": {
    "range": {
      "createdDate": {
        "gt": "20220423",
        "lte": "20220523"
      }
    }
  },
  "aggs": {
    "group_by_state": {
      "terms": {
        "field": "createdDate"
      }
    }
  }
}

검색어 자동완성 서비스

구글, 빙, 네이버, 다음과 같은 검색포털에 접속해 보면 상단에 검색창이 있습니다.

이 검색창에 정보를 찾기 위해서 검색어를 입력하는데 이때 키보드를 타이핑할 때마다 검색창 바로 아래에 지금까지 타이핑한 글자들로 구성된 검색어 목록을 보여주고 선택을하면 검색어를 다 입력해주는  검색어 자동완성 기능이 있습니다.

검색어를 타이핑하다 말고 마우스나 키보드로 완성된 검색어를 입력할 수 있게 해주기 때문에 “검색어 자동완성”이라고 합니다.

데이터사이언스와 크게 관련이 없긴 하지만 검색어 자동완성에 대한 내용을 적어보겠습니다.

사실  추천되는 검색어 목록을 만드는 것이 데이터사이언스와 관련이 많이 있습니다만 내용이 너무 길어지니 이것은 나중에 설명하겠습니다.

앞서 설명했지만 “검색어 자동완성”은사용자가 검색어를 힘들게 다 입력하지 않아도 입력 도중의 단어로 시작하거나 또는 포함하는 단어들을 목록으로 보여주고 선택해서 검색어를 바로 완성하게 해주는 사용자 친화적 (user friendly) 기능(서비스)입니다.

아래의 그림처럼 구글에서 검색어를 입력하면서 볼 수 있는  이런 것입니다.

네이버, 다음, 빙, 야후, 덕덕고(duckduckgo)와 같은 검색포털과 각종 온라인 쇼핑몰, 오픈 마켓, 온라인 서점에서도 모두 볼 수 있습니다.

검색어 자동완성은 서비스와 사용자의 행동 유도 관점에서 각각 장단점이 있습니다.   (행동과학의 관점입니다)

장점

  • 사용자 편의성
    • 검색서비스를 이용하는 사람들이 타이핑이 불편하거나 느린 경우 편하게 원하는 검색어를 입력할 수 있게 해줍니다.
    • 사용자가 오타를 입력해서 검색결과를 찾지 못하는 것을 방지해줍니다
    • 검색어가 잘 생각나지 않을때 단어의 파편을 입력해서 목록을 보고 기억나지 않은 또는 부분적으로만 기억나는 단어를 입력해서 검색어를 입력할 수 있습니다.
    • 다른 사용자가 많이 입력하는 검색어가 제안되기 때문에 사람들이 많이 입력하는 최신 트렌드의 검색어를 자신도 입력해서 최신 자료를 입수 할 수 있습니다.
  • 포털의 이득
    • 사용자가 찾을 것이 없어도 뭐라도 제안을 해서 검색을 하게 만듭니다. 즉 사용자 입력할 검색어를 마땅히 기억하지 못해 검색 포털을 떠나는 것을 방지할 수 있습니다.
    • 상업적으로 유리한 검색어를 자동완성에 넣어서 상업적 이득을 볼 수 있습니다.

단점

  • 사용자의 새로운 경험 박탈
    • 사용자의 입장에서는 검색어를 잘못 입력해서 뜻하지 않게 좋은 자료나 괜찮은 자료를 찾을 수 있는 예외적인 경험을 할 수 있는 기회가 줄어듭니다.  사용자가 실수할 것을 방지해주니 장점이라고 볼 수도 있긴합니다.
    • 검색어는 인기 검색어나 사람들이 자주 입력하는 단어를 기반으로 추천되기 때문에 다른 사람들도 많이 검색하는 단어를 사용하기 때문에 정보 입수의 다양성에 반하게 됩니다. 일종의 개성없는 정보의 편식을 만듭니다. 반면 장점이기도 하겠습니다.

사용자의 입장에서는 어쨌든 단점보다는 장점이 더 많은 것 같습니다. 편하니까요. 하지만 가끔 검색어 자동완성이나 인기검색어로 정보의 섭취를 유도당하고 있는 것은 아닌지 생각해 봐야 합니다.

자동완성에 나오는 목록에 광고 목적으로 목록에서 위에 위치하도록 되어 있는 것도 있다는 것을 아셔야 합니다.

이제 구현 또는 구축에 대한 것을 적어보겠습니다. 기술적으로 보면 자동완성 시스템을 만들어서 운영하는 것은 생각보다 녹녹하지 않습니다.  자동완성은 포털 뿐만 아니라 소셜네트워크 서비스에서도 볼 수 있고 쇼핑몰, 인터넷서점, 그리고 사내 인트라넷 같은 곳에서도 볼 수 있습니다. 하지만 구현되어 있는 것도 다르고 각각의 상황에 따라 운영 방식이나 상태도 다를 것입니다.

구현이나 운연에 관련된 부분을 Frontend, Backend,  Data processing 3단계로 나눠서 설명해 보겠습니다.

Frontend 부분

Frontend 부분은 당연히 Javascript로 합니다. Ajax입니다. 사용자가 키보드를 타이핑할 때마다 backend로 사용자가 입력 중인 단어를 보내서 제안할 검색어 목록을 받아서 화면에 뿌려줍니다.
주의할 점은 사용자의 타이핑이 매우 빨라서 제안된 검색어가 화면에 보여지기 전에 사용자가 빠르게 타이핑을 하면 기존에 타이핑했던 요청(backend call)이나 지금 막 하고 있었던 자동화면창 표현(rendering)을 중단하고 다시 backend 호출을 하는 트릭이 필요합니다. 그렇게 하지 않으면 사용자에게 불편한 경험을 주게 됩니다. (눈으로 볼 때 거리적 거려서 불안감을 줍니다)

예를들어 “자동완성”이라는 검색어를 사용자가 포털의 검색창에 입력한다면 사용자는 다음과 같은 순서로 빠르게 키보드를 쳐나가게 됩니다.

  1. 자도
  2. 자동

타이핑이 빠른 사람이라면 1 ~ 2초이내에 12번의 타이핑을 하게 됩니다.  12번의 backend 호출이 일어나게 되는데 사용자가 타이핑을 멈추거나 잠시 쉴때까지는 불필요한 호출이기 때문에 생략하는 기술이 필요합니다.

물론 요즘은 javascript 라이브러리들이 잘 구현된것이 많아서 잘 된 것을 찾아서 갖다 쓰면 됩니다.
jquery autocomplete: https://jqueryui.com/autocomplete/
하지만 좀더 빠르고 잘 작동하는 것을 만드려면 직접 만드는 것이 좋습니다.

Backend 부분

backend 부분은 다양한 방법으로 구현이 가능하고 사이트의 규모나 동시 접속하는 사용자의 수같은 상황에 따라 쓸 수 있는 방법이 달라집니다.  여러 방법을 선택할 수 있습니다.

Javascript 색인 방식

몇개의 library가 잘 공개된 것이 있습니다. 이 방식은 Javascript로 자동완성에 사용할 검색어를 모두 N-gram 색인 구조로 만들어서 웹브라우저에 로딩한 후 사용하는 방식입니다.
데이터의 사이즈가 커지면 성능저하가 심하고 검색어 데이터가 대량(수천만 또는 수억)이 되면 아예 불가능 합니다.
웹페이지를 로딩할 때 데이터를 한꺼번에 로딩해야 하기 때문에 웹페이지 렌더링을 방해하거나 또는 렌더링이 된 후에도 초기에 잠깐 작동하지 않는 불편함이 생깁니다.
데이터가 적을 때 씁니다. python 문서화 패키지인 sphinx 같은 것이 static html에서 문서검색을 할 수 있게 하기 위해서 이런 것을 사용하는 것을 볼 수 있습니다. 일부 위키시스템도 이 방식을 사용합니다.
이 경우는 사실 backend가 하는 일이 별로 없긴합니다.

데이터베이스의 like query를 이용하는 방법

데이터베이스에 대놓고 like 구문을 보내서 검색하는 무식한 방식입니다. 무식하지만 쉽고 구현하기 쉽습니다. 데이터가 적고 접속 사용자도 적을 때 막 사용할 수 있는 방법입니다.

SELECT name 
FROM products
WHERE name like '%자동%'

물론 DB의 full-text 검색 기능을 이용하거나 memory table 기능을 이용하거나 아니면 in-memory 데이터베이스등을 이용해서 성능을 개선할 수 있겠지만 대용량 트래픽을 받아야 하는 상황이면 DB가 이런 과부하를 견디기 어렵습니다.

유사한 방법으로 Sphinxsearch 같은 것을 쓰고 cache 를 덧대주는 방법도 있겠습니다.  그런데 Sphinxsearch는 검색엔진입니다.

Static file을 이용하는 방법

모든 예상되는 입력에 대해서 json file같은 것을 static 모두 생성(generation) 한 후에 static content server에 올려놓고 frontend에서 요청이 오면 이 파일을 그대로 당겨서 전달하는 방식입니다. 과거 어떤 포털이 이 방식을 사용했다고 합니다. 너무 무식하긴 하지만 성능은 괜찮은 방법입니다.

이것도 backend에서 하는 것은 요청한 입력에 대한 맞는 파일을 찾아서 읽은 후에 입력한 키워드에 맞는 목록 데이터를 던져주면 됩니다.

NoSQL을 이용하는 방법

memcache를 쓰거나 NoSQL 중에 반응속도가 빠른 것을 마련하고 static file 생성하는 방식처럼 모든 입력에 해당하는 키워드와 그 키워드들의 조합파편을 key로 하고 value에 검색어 제안목록을 넣어두고 요청이 올 때마다 빼서 던져주는 방식입니다.
저는 안해봐서 잘 모르겠습니다만 성능은 괜찮다고 하는 것 같습니다. 솔직히 이렇게는 안하고 싶습니다.  메모리 효율이 좀 안좋을 것 같습니다.

트리 구조(tree structure)를 이용한 대몬(Daemon)을 사용하는 방법

가장 많이 쓰이는 방법입니다. 성능도 좋고 추가 기능을 넣을 수가 있기 때문입니다. 과거에는 이런 대몬(daemon)을 직접 개발해야 하는 것이 상당히 어렵고 고통이었지만 지금은 좋은 오픈소스가 있습니다.

이 방식은 트리 자료구조(알고리즘) 중에 적합한 것을 선택해서 빠르게 HTTP 리턴을 할 수 있는 경량 웹서버를 만드는 것입니다.  Frontend에서 요청이 오면 트리탐색을 해서 트리의 하위 노드들을 모두 취합(또는 적당히 정해진 갯수만 취합해서)해서 자동완성 제안 목록을 만들고 트리 노드에 붙어 있는 value(score같은 것입니다)를 보고 역순 정렬을 하거나 해서 검색어 추천 목록 정해진 갯수만큼 뽑아서 리턴합니다. 보통 리턴해줘야 하는 자동완성 검색어 목록은  20 ~ 30개이면 됩니다.

트리 자료 구조는 보통 Trie나 FSA(Finite State Automata)를 사용하거나 유사한 것을 사용합니다.  요즘은 더 괜찮은 것들이 있을지도 모르겠습니다.

구현할 때 몇가지를 고려해야 합니다.

  • 자체 캐시(Cache)
    • 자체 캐시를 지원해야 합니다. 자동완성을 위해서 backend의 앞쪽에 캐시를 붙여서 성능강화를 할 수도 있습니다만 자료구조내에서 데이터를 탐색하는 것도 로드가 발생하기 때문에 LRU같은 간단한 캐시 기능을 넣어야 합니다.
  • 랭킹(ranking)
    • 제안할 목록의 랭킹도 고려해야 합니다.  보통 검색어 제안 목록을 가나다 순이 아니라 스코어를 주고 그 순서로 보여주는 것이 사용자 경험상 더 낫기때문입니다.
    • 그래서 빠른 소팅(sorting) 라이브러리를 사용하거나 만들어야 합니다.
  • 메모리 페이징 문제
    • 추천하는 검색어 목록을 모두 메모리에 가지고 있을 수 있다면 그렇게 해야 합니다. 목록을 저장하기 위해서 디스크를 사용하면 페이징(paging)을 할 때 성능저하가 크기 때문에 가능한 메모리를 최대한 활용해야하고 디스크를 쓰더라도 SSD를 써야합니다.
  • 클러스터링
    • 디스크를 사용하지 않고 메모리에서만 처리하겠다고 하면 데이터가 많아질때 여러 서버에 분산하는 것이 필요합니다.  그런데 트리구조는 분산하기 굉장히 어렵습니다.
  • 온라인 업데이트
    • 데이터를 주기적으로 갱신해 주어야 하는데 가동중인 대몬을 멈추고 업데이트하기 보다는 대몬을 가동시킨 상태에서 데이터를 업데이트를 해야합니다.
    • 물론 로드밸런서(load balnacer) 같은 것을 두고 로드밸런서 뒤에 붙어 있는 서버의 대몬을 순차적으로 재시작해가면서 데이터를 갱신하는 방식을 사용할 수도 있습니다.  피곤하지요.

위의 것들은 구현할 때 고려할 내용이긴 하지만 오픈소스를 선택할 때 어떤 기능을 지원하는지 확인하는데 필요할 것 같습니다. 오픈소스 검색어 자동완성 대몬은 몇개 없습니다. 가장 잘 알려진 것은 아래 2개입니다.

  • Linked-in Cleo
  • Duck-duck-go libface

Cleo(클레오)는 라이브러리인데 Cleo를 바로 쓰기 보다는 Cleo-Primer라는 Cleo를 이용한 응용구현체의 소스를 받아서 빌드하고 설명서대로 데이터를 XML로 http 로 부어넣으면 온라인 업데이트가 가능한 방식입니다. Java로 되어 있습니다.  용도에 맞게 쓰려면 표현에 필요한 데이터 항목을 수정해야 하는데 그것 때문에 소스를 조금 수정해 주어야 합니다.

Libface 는 C++로 된 고속 대몬입니다. 써보진 않아서 아는 것이 별로 없습니다만 괜찮다고 합니다.  보기에도 괜찮아 보입니다.

이 외에도 몇개 더 있지만 소스가 오랫동안 개선 되지 않거나 사이트 자체가 사라져서 지금은 기억이 나지 않습니다.

검색엔진을 이용하는 방법

뭐 앞서의 방법이나 사실 다를바가 없지만 검색엔진 중에 자동완성 기능을 지원해주는 것들이 있습니다.
Elastic search도 기능을 지원합니다. 개발을 하지 않아도 되고 색인하는 방법만 익히면 쉽게 사용할 수 있습니다.

type-ahead 방식의 인메모리 자동완성 기능을 지원합니다. type-ahead방식은 뒤에 설명하겠습니다.

이 방법은 성능은 괜찮지만 다른 것에 비해서 무거운 검색엔진을 구동시켜야 한다는 부담이 있습니다. 하지만 대몬을 구동시키는 것이나 검색엔진을 구동시키는 것이나 데이터가 많아지면 부담스럽기는 매한가지입니다.

데이터 프로세싱

검색어 자동완성은 보통 두가지 데이터소스에서 데이터를 끌어와서 자동완성 대몬이나 서비스 구현체에 부어넣습니다.

  • 로그
    • 사용자가 입력한 검색어를 로그에 쌓아두고 빠르게 읽어서 자동완성 구동체에게 빠르게 공급합니다.
  • 데이터베이스
    • 상품의 이름을 쉽게 검색하게 하려는 쇼핑몰 같은데서는 굳이 로그를 뒤질 필요가 없습니다. 상품목록이나 주소, 사람이름 같은 것을 DB에서 내려받아 자동완성 구동체에 부어넣습니다.

로그를 읽어서 로그에서 많이 출현한 키워드에 가중치를 주거나 단순 횟수를 카운트해서 score값으로 쓸 수 있습니다. 로그를 추출할 때는 오타, 특수문자, 성인어 등을 어떻게 처리할지 함께 고민해야 합니다.

데이터베이스에서 꺼내 오는 경우는 상품명 같은것이 될텐데요. 검색어 자동완성도 일종의 추천시스템이라고 봐야하기 때문에 자동완성 목록을 추천할 때 단순히 가나다 순이 아닌 최신 상품이라든가 판매지수가 높은 상품이라든가를 상위에 노출되도록 하는 것을 고민해야합니다.

언어 처리

자동완성의 제안 목록이 되는 데이터를 프로세싱하고 대몬에 탑재할 때 고려할 것이 두가지 정도 더 있습니다.

형태소 분석기를 이용한 자동 띄어쓰기 및 자연어 처리

DB에 있는 상품명이든 사람들이 입력하는 키워드이던 띄어쓰기가 잘못되어 있는 경우가 있습니다. 이렇게 띄어쓰기가 잘 되어 있지 않은 것은 검색엔진에서 잘 다둬주지 않으면 제대로 된 검색결과를 리턴하지 않을 수 있습니다. 형태소 분석기에서 자동 띄어쓰기를 해서 잘 다듬어서 넣어주는 트릭이 필요할 수 있습니다.

자모분리

“빨간구두”를 키보드 한영전환을 하지 않고 두벌식으로 타이핑하면 “Qkfrksrnen”가 됩니다. 입력할 때 사용자가 영한전환을 안해서 생기는 문제인데 사용자의 실수라고 하더라도   “Qkfrks”를 치면 한글 “빨간…”으로 시작되는 검색어를 추천해서 사용자에게 편리함과 작은 감동을 주고 싶다면 데이터의 키를 만들 때 약간의 트릭을 쓰면 됩니다.

한글을 유니코드 처리를 이용해서 자모로 분리할 수 있고 자모에 대한 키보드상에 영문 매핑을 만든 후에 그것을 자료구조의 키로 만들어 주면 됩니다.

자판에서 ㄱ은 r에 매핑되고 ㄴ은 s에 매핑됩니다. 이렇게 변환된 것을 트리구조 또는 검색어 파편의 키로 만들어주면됩니다.

즉 “빨간구두”는 “Qkfrksrnen”를 키로 넣습니다.

대신 이 경우에는 Qkfrksrnen로 들어간 것이 원래 “빨간구두”였다는 것을 알아내야 하기 때문에 자료구조의 value에 함께 넣어두던가 역으로 복원하는 방법을 만들어 두어야 합니다. 번거롭긴 하지만 사용자 편의성을 높이는 방법 중 하나입니다.

위의 경우에 한글을 자모로 분리해서 영문키로 매핑하면 원래 영문으로 된 검색어는 어떻게 되는지 궁금하실지 모르겠습니다. 일부는 한글을 영타로 타이핑하면 영어에도 있는 단어와 겹치는 경우가 있지만 대부부의 경우는 겹치지 않습니다. 그래서 큰 문제는 안됩니다.

뒤로 매칭

“빨간”을 타이핑하면 “볼빨간”을 제안 즉 사용자가 입력한 키워드(입력하다만 키워드)로 끝나는 검색어를 추천해주고 싶을 수 있습니다.

사용자 자료를 찾기 위해서 키워드를 입력하는데 “빨간”은 생각이 나는데 이게 “볼빨간”인지 “눈빨간”, “목빨간” 인지 기억이 안날 수도 있습니다.  사용자를 잘 도와줘야 합니다.

이것은 간단한 방법을 쓰면 됩니다. 자료 구조에 데이터를 넣을 때 키의 문자열을 뒤집어서 넣으면 됩니다.

“볼빨간”을 자료구조에 넣을 때 “간빨볼”로도 하나 더 넣어두면 됩니다.
“볼빨간”이 영타로는 “qhfQkfrks”이기 때문에 다음처럼 해두면 됩니다.

키: value1, value2
qhfQkfrks: 볼빨간, 101
skrfkQfhq: 볼빨간, 100

위의 경우는 자료를 두벌 이상 넣어야 하기 때문에 대몬이 사용하는 메모리가 더 든다는 단점이 있습니다. 자료구조를 좀더 아름답게 고치면 메모리를 절약할 수도 있겠습니다.
런타임에 단어를 뒤집어서 조회를 해도 됩니다만 사용자의 키 타이핑이 빨라서 반응시간을 줄여야 하기 때문에 프로세싱할 때 미리 넣어두거나 대몬자체가 입력을 뒤집어서도 자료를 찾는 기능이 내장되어 있는 것이 좋습니다.

하지만 뒤집어서 넣는 것 보다는 type-ahead 방식이 여러모로 더 낫습니다.

그외  여러가지 잡다한 것들

자동완성은 영어로

  • Auto-completion
  • Auto type completion
  • Type ahead search 또는 type ahead completion
  • Completion suggest

다양 하게 부릅니다. 관련 영문 자료를 찾으시려면 저런 단어로 검색하시면 됩니다.

type-ahead 검색

type-ahead 검색은 auto-completion과 조금 구분이 되어야 합니다. type-ahead가 auto-completion의 일종이긴 하지만 단어의 어떤 부분을 매치해서 찾아주는지의 방법이 다소 다르기 때문입니다.

“검색어 자동완성” 이라는 키워드가 있고 사용자가 매치 되는 검색어의 일부를 타이핑했을 때 제안해 준다고 예를 들어 보겠습니다. 이때 n-gram 방식의 auto-completion과 type-ahead 방식의 차이로 인해서 제안 목록이 달라질 것입니다.

사용자 입력: “검색”

이때 제안할 검색어가 “검색”이라는 단어로 시작하기 때문에 두 방식다 제안이 됩니다.

사용자 입력: “자동”

이 경우에도 두 방식 모두 “검색어 자동완성”을 찾아서 제안해줍니다.

사용자 입력: “색어”

n-gram 방식은 찾아서 제안해 주지만 type-ahead 방식은 찾지 못합니다.
왜냐하면 type-ahead방식은 “검색어 자동완성” 이라는 검색어 제안을 공백을 기준으로 분리해서 앞에서부터 매치되는 것이 있는 것만 찾기 때문입니다. 최근의 검색어 자동완성이라고 하면 모두 type-ahead 방식을 말합니다. n-gram 방식이 효율도 좋지 않고 경우에 따라 제안되는 검색어가 다소 생뚱맞기 때문입니다.

type-ahead 방식을 트리 구조를 내장하고 있는 대몬에서 처리하게 하려면 key rotation이라는 간단한 트릭을 쓸 수도 있습니다.

아래와 같이 특별한 기호를 두고 공백을 기준으로 단어의 위치를 돌려서 모두 넣어준 다음에 찾을 때 찾고 보여줄 때는 원래 것을 보여주면 됩니다.

“검색어 자동 완성”
“자동|완성|검색어”
“완성|검색어|자동”

구현하려면 조금 번거롭기 때문에 오픈 소스들에는 대부분 type-ahead를 지원하기 때문에 특별히 다르게 구현해야 할 것이 아니라면 가져다가 그대로 쓰시는 것이 편할 것 같습니다.

데이터 분석

추가로 얘기하고 싶은 것은 사용자가 입력한 검색어를 데이터 분석의 목적으로 사용하는 것에 대한 것입니다.

사용자가 입력하는 키워드의 수을 세고 여러가지 정보를 붙여서 사용자들의 관심 동향(trend)를 보는 것을 하겠다고하면 검색키워드를 가지고 데이터 프로세싱을 한 후에 이것저거 살펴보고 이용하면 됩니다.

사실 이런 데이터를 들여다 보는 것은 매우 재미있습니다.  시간대와 사회적 이슈에 따라 입력하는 단어들의 경향이 달라지는 것을 볼 수 있으니까요.

하지만 자동완성이 붙어 있게 되면 몇가지 특별한 내용을 가리게 됩니다. 사용자가 어떤 키워드를 잘못 입력하지는 오타를 많이 입력하는지를 자동완성기능이 사용자의 입력을 유도해서 보이지 않게 만들 수 있습니다. 이런 경우에는 자동완성 대몬 자체가 로그를 남겨서 어떤 것들이 조회되는지 봐야 할 수 있고 자동완성의 frontend에서도 사용자가 검색어를 스스로 다 입력했는지 만약 자동완성제안 목록을 선택했다면 몇번째것을 선택했는지 등을 함께 로그에 남겨서 살펴봐야 합니다.

오픈소스 검색엔진 베스파 Open source search engine Vespa

2017년 9월 26일에 Yahoo가 Vespa를 오픈해서 오픈소스로 공개했습니다.

먼저 밑에 프로젝트의 URL을 올려드립니다.
http://vespa.ai/

개인적으로 상당히 큰 사건이라고 생각합니다.

우선 Yahoo가 가진 핵심 기술 중에 절대로 공개하지 않을 것만 같은 것 중에 항상 언급이 되는 것 중에 다섯손가락 안에 꼽히는 것이 Vespa입니다.

하지만 Vespa가 뭔지 대부분의 분들은 모를 것입니다.  간단히 설명하면 Vespa는 원래는 검색엔진이었습니다. 하지만 최초에 만들어지고 Yahoo 내에서 계속 사용하면서 발전시켜서  기능이 많이 확장되었고 성숙하게 발전해서 통합 콘텐츠 처리 플랫폼이 되었습니다.

저도 Vespa를 이용해서 일을 했던 적이 있습니다만 매우 오래전이기 때문에 제가 봤던 것보다는 지금의 버전이 훨씬 더 많이 확장되었고 발전했을 것이라고 생각합니다.

조금 구체적으로 설명하면

앞서 말씀드린 것 처럼 Vespa는 검색엔진을 중심으로 둔 통합 콘텐츠 처리 플랫폼의 콤포넌트 셋입니다.  간단히 생각하면 검색엔진이라고 보면 되지만 일반적인 Solr이나 Elastic과 같은 검색엔진 보다는 주변에 가진 부속 컴포넌트들이 훨씬 더 많습니다.

Vespa의 역사를 보면 Vespa는 노르웨이의 엔지니어들이 주축으로 만든 한때 유럽의 Top1 검색엔진이었던 Fast검색엔진의 갈래입니다. Fast검색엔진은 후에 시장 주도권을 뺏긴후 차츰 회사 자체가 분할 매각되어서 지금은 거의 자취가 사라졌습니다.

분할된 Fast사는 Yahoo가 일부를 인수했고 시간의 간격을 두고 Microsoft가 일부를 인수했고 나머지 분할된 파트는 지금은 어찌되었는지 지금은 잘 모르겠습니다.

이 중에 Yahoo가 Fast에서 인수한 분할 부분은 Web search를 담당하는 쪽이었는데 인수한 Web search engine 중에  crawler같은 것을 버리고 vertical 검색 및 content platform으로 발전시킨 것이 Vespa입니다.  Vespa가 crawler같은 것을 버리고 web search 엔진을 추구하지 않은 이유는 Yahoo는 그당시에 이미 Inktomi web search를 가지고 있었기 때문에 Web search가 또 필요하지 않은 상태여서 Vertical search engine으로 방향을 전환하게 됩니다.

Vespa는 오토바이 브랜드명이기도 하지만 Vertical Search Plafrom의 약어입니다. 이름에 추구하는 바를 넣었듯이 한창때에는 세계 최고의 Vertical  Search Platform이라고 부를만 했습니다만 지금은 모르겠습니다. 검색 엔진쪽 만의 기능은 Elastic Search와 비교를 해봐야 하겠습니다.

Hadoop의 창시자인 더그커팅도 Yahoo에 근무하던 시절이 있었기 때문에 루씬도 Vespa에도 상당한 영향을 주었다고 알려져 있습니다. 때문에 Elastic이니 Solr이니 하는 Lucene기반의 것들과 비슷한 면도 많습니다.

검색엔진의 측면에서는 현재 주류라고 할 수 있는 Elastic Search와 비교를 해봐야 하겠습니다만 전체 기능의 광범위함과 성능을 볼 때는 Elastic Search가 Vespa를 따라가지 못할 것 같습니다.  하지만 편리함의 측면이라면 Elastic search가 더 나을 것입니다.

Vespa를 보면 코어 모듈의 튼튼함과 오랜 역사, 깔끔한 구조, 다양한 주변기능은 유럽 엔지니어들의 탁월한 능력을 엿볼 수 있게 해줍니다. 소스코드도 매우 깔끔하고 구조화가 잘 되어 있는 편이고 작동도 일관성있게 작동하는 것이 특징입니다.  소스코드를 들여다 보는 것 만으로도 많은 공부가 됩니다.

부속 콤포넌트 및 주변 콤포넌트로는 ML ranking(MLR이라고 부릅니다)을 비롯한 자체 개발한 문서저장용 NoSQL, 분산 데이터 파이프라인, 페더레이션(Federation)을 위한 컨테이너 등도 모두 가지고 있습니다.

그리고 여기에 Yahoo의 또 하나의 오픈 소스 선물인  Hadoop과 결합해서 사용하면 최고의 Content Agility Platform을 구성할 수 있게 됩니다.

MLR은 기계학습 기반의 검색결과 랭킹 기능입니다. GBDT기반으로 되어 있었습니다만 오픈 소스 버전에 탑재가 되었는지는 모르겠습니다.  (아마 되어 있을 것입니다. 분리하기 어렵기 때문에) 그리고 다국어 형태소 분석기와 Yahoo의 강력한 언어처리 모듈이 결합되어 비교적 매끈한 언어처리 기능까지도 지원했었습니다. 형태소 분석기와 언어처리 모듈은 오픈소스 버전에는 아마도 포함되지 않을 것입니다.

페더레이션 컨테이너 여러 검색엔진의 결과를 하나로 병합해주는 플랫폼을 말합니다.

분산 데이터 파이프라인은 소스로부터 검색엔진까지의 데이터 파이프라인으로 검색엔진으로 가기전까지 콘텐츠를 가공, 병합, 변형하는 플랫폼입니다.

이외에도 Phrase match와 Proximity 계산을 위한 고속 FSA 라이브러리와 그 외에 부속품들이 잔뜩 들어 있는 종합선물세트입니다.

대략 여기까지만 보셔도 알겠지만 대형 인터넷 포털사이트에서 사용하던 수십억건의 콘텐트를 관리, 가공, 활용, 서비스하는데 사용했던 솔루션을 이제는 누구나 사용할 수 있다는 것입니다.

기쁜일이지만 개인적으로는 서글픈 감상에도 젖어봅니다. Yahoo가 저렇게 하나씩 보따리를 풀고 사라져 가네요.

Cosine Similarity – 코사인 유사도

삼각함수와 선형대수학에 대한 기본적인 배경지식이 있다면 코사인 유사도는 이해하기 매우 쉽습니다.  그게 아니라면 처음에 개념을 잡는 것이 어려울 수 있습니다.  그렇다고해서 겁 낼 필요는 전혀 없습니다. 어마무지하게 어려운것이 아니라서 포기하지 않고 조금 신경쓰고 조금 시간을 쓰면 누구나 이해할 수 있는 것입니다.

이 글에 대해서

이 글은 조금 쉽게 풀어서 설명하려다 보니 내용이 조금(많이) 길게 되었습니다. 그러니 만약 이 글을 읽을 것이라면 시간적 여유가 있을 때 보시면 좋겠습니다.  보통 코사인 유사도는 교과서에서 매우 간단하게 설명하고 넘어가는데 그건 다 알고 있다는 가정에서 설명하기 때문입니다. 이것은 문제가 있는데 보는 사람은 모르기 때문에 교과서를 보는데 아는 것을 가정하고 간단하게 설명한다는 것은 매우 이상합니다.  어쨌든 그런 부분까지 쉽게 풀어서 쓰다 보니 글이 장황해 졌습니다. 제 탓입니다.

코사인유사도는 대체 뭘까?

200px-Dot_Product.svg

우선 개념(concept) 이해를 위해서 코사인유사도를 아주 짧게 정의해서 설명하면 이렇습니다.
두 벡터(Vector)의 사잇각을 구해서 유사도(Similarity)로 사용하는 것

혹시 벡터라는 용어에 울렁증이 있더라도 여기에서 포기하지마세요. 별거 아닙니다.

여기서 유사도를 구할 때 두 벡터 사이의 각을 코사인(Cosine)값으로 구해서 유사도값으로 사용하기 때문에 코사인 유사도(Cosine Similarity)라고 부릅니다. 더 줄여서 말하면  두 벡터의 사잇각을 코사인으로 구하면 되는 것입니다.

수포자라면 아직도 이 설명 자체로는 이해가 어렵겠습니다만 괜찮습니다. 좀더 읽어 보세요.

코사인이 나왔으니 말인데 코사인 외에도 삼각함수에서 늘 같이 딸려 나오는 사인(Sine)이나 탄젠트(Tangent)로 값을 계산했다면 아마 “사인 유사도”나 “탄젠트 유사도”로 불렀을 것입니다만, 굳이 코사인을 사용한 것은 이런 계산을 하는데 코사인값으로 계산하는 것이 더 쉽고 편하기 때문입니다. 코사인으로 계산하면 계산을 덜 해요. 그것 뿐입니다.

벡터의 유사도를 구하는 방법은 여러가지가 있습니다. 코사인 유사도는 그 중 하나입니다. 다른 것과는 다르게 코사인 유사도의 특징은 사잇각을 쓴다는 것입니다.

저 위에 설명을 위한 매우 간단한 삼각형 그림이 있습니다. 코사인 유사도를 설명하는 문서에서 예제로 많이 보여주는 매우 흔하고 유명한 그림입니다.
저 그림을 보고 설명하면 그림에서 A와 B는 유사도를 계산할 대상이 되는 두 벡터(vector, 또는 두개의 수열값 세트)이고 두 벡터의 A와 B의 사잇각 쎄타를 코사인으로 구하는 것이고 구해진 사잇각의 값을 코사인 유사도로 사용하는 것입니다.

여기에서 각이란 원점에서 각 벡터의 위치까지 선으로 이어서 선을 만든 다음 두 선의 각을 말하는 것입니다. 원점은 모든 좌표의 값이 0이 되는 곳입니다.  이건 수학시간에 배우셨지요?

벡터는 무엇인가?

이 지면에서 벡터까지 설명해야 하는 것은 너무 길어서 무리이겠지만 그래도 벡터에 대해 익숙하지 않은 분들을 위해서 먼저 벡터에 대해서 간단히 설명을 하고 넘어가겠습니다. 이미 잘 아신다면 당연히 이 부분은 건너 뛰어도 됩니다.

위의 그림에서 두 벡터는 2차원 벡터를 표현해 놓은 것입니다. 그림은 가로와 세로로 축이 2개있는 평면이니까요. 축이 2개라서 숫자도 2개씩입니다.
벡터는 2차원 이상의 고차원 벡터도 있습니다. 현실의 데이터는 대부분 수십차원에서 수백만차원까지 가는 고차원 벡터를 다루는 일이 더 많습니다.  숫자 2개가지고는 뭐 할 수 있는 것이 별로 없습니다. 정보량도 적구요. 고차원 벡터는 그림에서 보이는 x축, y축과 같은 축들이 더 많다고 생각하면 됩니다. z, l, m, n, … 등으로 말이지요.

벡터의 차원에 대해서 잠깐 설명하고 넘어가면 예를 만들어 보면 10차원 벡터는 아래와 같습니다.

10차원의 두 벡터의 예:

  • A = 1,2,3,4,5,6,7,8,9,10
  • B = 2,3,4,5,6,7,8,9,10,11

A와 B는 각각 수열(수의 나열)이고 각각 10개씩 숫자를 가지고 있으므로 둘 다 10차원의 벡터입니다. 앞서 말씀드렸듯이 10차원 벡터는 눈으로 볼 수 있게 시각적으로 표현할 수 없습니다. 삼각형이나 선으로 좌표축에 그릴 수 없습니다.

수학 수업에서 배우기도 하는데 배우셨더라도 아마 잊어버렸겠지만 3차원까지는 시각적으로 표현이 가능하지만 4차원부터는 인간의 눈으로 한 눈에 볼 수 있게 시각적으로 표현하는 것은 불가능합니다. 인간이 통제할 수 있는 차원이 3차원까지만 가능하기 때문입니다. 그 이상은 머리속으로 추상적으로만 연상(imagination)을 해야 합니다. 이건 아인슈타인도 못 그립니다.

어쨌든 10차원 보다 숫자들이 하나씩 더 있으면 11차원이고 1000개가 되면 1000차원입니다. 그냥 각 축에서 위치를 나타내는 숫자가 1000개 있으면 1000차원 벡터, 10000개면 10000차원 벡터입니다.

물론 여러 벡터가 있다면 각 벡터에서 숫자의 순서들은 서로 같은 의미를 가지는 것에서 가져온 숫자여야 합니다. 이것 마저 설명하면 매우 복잡해지니 여기서는 그냥 잊어버리셔도 됩니다.

2차원이든 100차원이든 1000차원이든 시각적으로 표현이 불가해도 코사인 유사도는 정해진 공식으로 구할 수 있고 구하는 방법도 간단합니다.

코사인 유사도의 용도

계산법을 보기전에 아마도 많은 분들이 궁금할 것이 “두 벡터의 유사도를 구해서 어디에 쓰는가?” 라는 것일텐데요. 용도를 알게 되면 이해하는 것이 더 쉽습니다.

그런데 그전에 또 먼저, 벡터의 유사도는 무엇인가?

용도를 설명하기 전에 유사도를 먼저 설명해야 합니다. 그 참 설명을 하기위해서 먼저 설명해야할 뭔가가 정말 많습니다만. 벡터의 유사도를 구하는 것은 비슷하다는 것을 어떻게 정의하냐에 따라 계산하는 방법이나 결과들이 마구 달라집니다. 우선 벡터는 그냥 숫자들의 묶음이라고 생각해보면 이것만으로는 무작정 비슷한지 아닌지를 알 수 없는 문제도 있습니다.

유사도를 직선거리(유클리디안 디스턴스; euclidean distance)로 구하거나 사잇각이 얼마나 좁은지를 계산해서 구하거나 하는 것이 가장 잘 알려진 것입니다.  사잇각을 계산해서 유사도를 구하는 것 중에 가장 잘 알려진 것이 코사인 유사도입니다.

또 고등학교 수학책을 보면 두 벡터의 내각을 구하는 것으로 코사인 유사도가 이미 나와있습니다.  아마도 배웠지만 잊어 버린 사람들이 많을 것입니다.

코사인 유사도의 용도

코사인 유사도는 대충 다음과 같은 구체적인 용도가 있습니다. 흔히 볼 수 있는 것들입니다.

  1. 검색 엔진에서 검색어(Query)와 문서(Document)의 유사도를 구해서 가장 유사도가 높은 것을 먼저 보여주기 위한 기본 랭킹 을 위한 알고리즘으로 사용됩니다.
  2. 텍스트마이닝에서 쓰입니다. 검색엔진과 텍스트마이닝이 상당히 관련이 있기 때문에 사실 1번과 관련성이 깊습니다. 텍스트 마이닝은 흔히 벡터 스페이스 모델(Vector Space Model)을 사용하고 TF-IDF(Term Frequency – Inverse Document Frequency)를 사용하는데 단어집합들 간의 유사도를 구하기 위해서 코사인 유사도를 사용하는 것이 빈번하게 나옵니다. 그래서 word2vec 같은 딥러닝 모델에서도 나옵니다.
  3. 그 외에도 다른 분석이나 수리 모형에서도 유사도를 구할 때 사용합니다. 가끔 나옵니다. 매우 빈번하게 사용하지는 않습니다.
  4. 클러스터링(Clustering, 군집화) . 군집화 모델에서 데이터 포인트를 서로 묶을 때 쓰이긴 합니다.

위의 예에 대해서 부연설명을 더 해보겠습니다. 역시 매우 장황하니 관심 없으시면 건너 뛰셔도 됩니다.

*위의 1번의 검색 랭킹의 문제*

검색엔진이나 텍스트 마이닝에서 주로 쓰이는 이유는 문서를 숫자로 표현하는 방법중에 가장 쉽고 잘 알려진 방법이 포함된 단어들의 출현 횟수를 세고 그걸 숫자로 만드는 것이기 때문입니다.

검색엔진에서 흔히 비교할 문서들은 검색엔진의 검색창에 입력한 질의어(query라고 합니다)와 검색엔진이 가지고 있는 문서들을 비교해서 가장 비슷한 것을 찾기 때문입니다. 여기서 코사인 유사도를 구하는 대상이 사용자가 입력한 질의어와 검색엔진이 가지고 있는 모든 문서들과의 쌍입니다. 그렇게 해서 코사인 유사도를 구해서 가장 유사도가 큰 것을 가장 위에 보여줍니다. (현재의 검색엔진은 이렇게 단순하게 작동하지 않습니다. 오해를 방지하기 위해서 적어둡니다).

이때 검색엔진이나 텍스트마이닝에서는 유사도를 비교할 때 단순히 단어의 출현횟수만을 가지고 문서를 수치데이터(벡터)로 바꾸지 않고 TFIDF라는 수치값을 계산해서 씁니다. 그래서 코사인유사도와TFIDF는 늘 쌍으로 같이 언급이 됩니다. 이건 나중에 기회가 되면 설명하겠습니다. (TFIDF에 대한 포스트를 참고하세요)

*위의 3번의 클러스터링에서의 문제*

클러스터링에서의 거리 계산은 모두 연산 자원(Computational Cost) 문제와 관련이 있습니다. 코사인 유사도 역시 그렇습니다.

유사도를 구하는 목적의 근본적인 목적이 $latex A$가 $latex B$와 유사한지 $latex A$와 $latex C$가 더 유사한지와 같은 상대적인 비교를 하기 위한 것입니다.

A와 B가 둘만 있다면 둘을 비교해서 둘이 얼마나 유사한지는 사실 알 수 없습니다.  알 필요도 없습니다.

예를들어 세상에 사람이 둘 만 남았다면 이 두 사람은 서로 가장 비슷하게 생긴 사람일까요? 아니면 서로 전혀 다른 사람일까요? 두가지 모두 해당되기도 하겠지만 실제는 이게 쓸모가 없습니다. 그냥 모르는 것입니다.

즉 A와B, C, …등등이 있으면 가장 유사한 것들끼리 묶어보거나 A와 가장 비슷한것을 B, C 와 같은 것 중에서 찾아서 고르는 경우가 대부분이기 때문입니다. 그래서 여러 개의 벡터를 대상으로 각각 서로 서로 쌍을 맺어 유사도를 구해서 가장 유사도가 높은 순으로 정렬해서 가까운 것 1개를 선택한다거나 여러개를 선택해서 여러가지 목적으로 사용하게 됩니다.

클러스터링을 할 때도 마찬가지겠지요 벡터의 개수 즉, 비교할 데이터가 $latex n$개고 벡터로 표현할 수 있다면 $latex \frac{n \times (n-1) }{2}$번 만큼 연산을 해야 합니다. RDMBS에 100개의 레코드가 있고 컬럼이 여러개 있는데 모두 숫자라면 각 레코드들 간의 유사도를 모두 구하면 $latex \frac{100(100-1)}{2}$ 만큼 유사도값을 뽑아야 합니다. 에… 계산하면 4950번 입니다.

코사인 유사도를 위한 전제 조건

매우 기본이 되는 것이라 설명을 할 필요가 없겠지만 이왕 이 글이 너무 장황해진 김에 적어 보자면 다음과 같은 전제 조건이 있습니다.

  • 두 벡터의 원소들은 모두 양수(플러스!)여야 합니다. x, y 직교 좌표축에서 1사분면에 오는 것들입니다. (모눈종이에서 중심을 기준으로 오른쪽 위)
    그래서 원소들의 값이 음수가 되지 않는 문제에만 가져다 씁니다.
  • 벡터의 원소수는 같아야 합니다.
    너무 당연한 것입니다. 비교하는 벡터의 원소 갯수가 일치하지 않으면 각각 빠진 것을 0으로 채워서 동일하게 만들어야 합니다. 벡터의 원소 갯수가 좌표축에서의 축의 갯수이기 때문입니다.

코사인 유사도의 특징

평면 좌표에서 1사분면의 두 벡터의 코사인 값은 0 ~ 1 사이의 값입니다. 벡터의 각이 작을 수록 1에 가까워지고 클수록 0에 가까워집니다. 따라서 결과를 재가공(rescaling)하지 않고 바로 쓰기 편합니다. 두 벡터가 정확히 직교이면 값이 0이 됩니다.

삼각함수를 배울 때 이미 배우는 것이기 때문에 이걸 기억 하고 있다면 좋겠습니다만 생각하지 않는다면 다시 한 번 찾아보세요.

공식

공식입니다. 자세히 보지 마세요.

$latex \text{similarity} = cos(\theta) = {A \cdot B \over |A| |B|} = \frac{ \sum\limits_{i=1}^{n}{A_i \times B_i} }{ \sqrt{\sum\limits_{i=1}^{n}{(A_i)^2}} \times \sqrt{\sum\limits_{i=1}^{n}{(B_i)^2}} }$

아주 간단한 공식이고 고등학교 수학교과서에도 나옵니다. 공식은 매우 쉽기 때문에 한 번 이해를 하고 나면 볼 필요도 없습니다.

공식 풀이

공식을 가능한 자세히 설명을 적어 보겠습니다. 먼저 공식에서 분자 부분과 분모 부분을 나눠서 설명하면 다음과 같습니다.

분자 부분 – 벡터 내적 (vector inner product, dot product)

코사인 유사도 공식에서의 분자 부분은 벡터의 내적(dot product)을 구하는 것입니다. 영어로는 inner product(이너 프러덕) 또는 dot product(닷프러덕)이라고 흔히 말합니다.

$latex \ll A \cdot B \gg $이 벡터의 내적(dot product) 표기입니다.
벡터의 내적은 계산이 매우 쉽습니다.
두 벡터의 각 원소들을 순서대로 짝맞춰서 곱한 다음에 결과들을 다 더하면 됩니다.

아래와 같은 두 벡터가 있다고 하겠습니다. 차원이 5차원인 2개의 벡터입니다.  요소가 5개이기 때문에 5차원입니다.  (외계인이 산다는 그 5차원이 아닙니다) 값은 현실의 예제가 아닌 제가 임의로 마구 넣은 것입니다.

$latex A = (1,2,3,4,5) $
$latex B = (6,7,8,9,10) $

  • 각각 짝을 지어 잘 곱합니다. 순서를 맞춰서 잘 해줍니다.
    $latex 1 \times 6 = 6$
    $latex 2 \times 7 = 14$
    $latex 3 \times 8 = 24$
    $latex 4 \times 9 = 36$
    $latex 5 \times 10 = 50$
  • 곱한 것을 다 더합니다.
    $latex 6 + 14 + 24 + 36 + 50 = 130$

위의 과정이 벡터의 내적을 구한 것입니다.
끝입니다. 너무 힘든 계산이었습니다. 썰렁한 농담입니다.

그런데 여기서 벡터의 내적이 왜 나오는지 궁금할 수 있습니다. 각을 구하는데 왜 저런게 필요한 것이지?  이것은 뒤에 설명하겠습니다.

분모 부분 – 두 벡터의 크기를 곱한다

분모 부분은 두 벡터의 크기를 각각 구해서 곱하면 됩니다.

$latex |A|$는 A벡터의 크기를 말합니다.
$latex |B|$는 B벡터의 크기를 말합니다.

분모는 두벡터의 크기를 구해서 곱하면 되는데요 벡터의 크기(norm 이라고 부릅니다)가 기억이 안나실 수 있는데요. 원점에서부터의 거리를 말하는데. 이건 기하학적으로 보면 사실 ‘피타고라스 정리‘에서 직각삼각형의 빗변을 구하는 것을 말합니다.

그냥 “피타고라스 정리를 이용해서 빗변을 구하는 것을 하면된다”는 말입니다.

그런데 2차원까지는 직각삼각형인데 3차원부터는 입체가 되고 4차원부터는 아예 모양을 상상도 할 수 없게 됩니다만 그래도 피타고라스 정리로 구할 수 있다고 수학자들이 증명을 이미 해놓았습니다. 믿고 쓰면 됩니다. 못 믿으시겠으면 직접 증명을 해보시면 됩니다.

자! 앞에서 설명했듯이 벡터의 길이는 피타고라스 정리를 사용하면 구할 수 있고 그걸로 2개의 값을 구해서 서로 곱하면 분모 부분은 완성됩니다.

직각삼각형의 빗변의 길이를 구하는 피타고라스 정리를 기억 못하면 매우 곤란합니다.

$latex C=\sqrt{ A^2 + B^2 }$

  • A벡터의 크기를 구합니다. 피타고라스 정리.

$latex \sqrt{ 1^2 + 2^2 + 3^2 + 4^2 + 5^2 } = 7.4161984870957$

  • B벡터의 크기를 구합니다. 피타고라스 정리.

$latex \sqrt{6^2 + 7^2 + 8^2 + 9^2 + 10^2} = 18.1659021245849$

  • 이제 마무리로 구한 것을 곱합니다

$latex 7.4161984870957 \times 18.1659021245849 = 134.7219358530751$

숫자값들이 소숫점 뒤로 길게 나와서 복잡해 보이지만 별거 아닙니다. 뭐 계산은 계산기나 컴퓨터가 하는 거니까요.

마무리 계산 – 분자를 분모로 나누기

이제 다 구했으니 분자를 분모로 나눕니다.  나누기는 매우 힘든 산술계산이지만 우리에게는 계산기가 있습니다.

$latex \frac{130}{134.7219358530751}=0.9649505$

위에 계산된 결과 값이 코사인 유사도 값입니다. 약 0.96이네요.

풀어놓고 보니 별거 아닙니다.
R 코드로 풀어보면 이렇습니다.

# 벡터가 둘이 있습니다.
vector1 <- c(1, 2, 3, 4, 5)
vector2 <- c(6, 7, 8, 9, 10)

# 그냥 구하기 (코드를 더 짧게 줄일 수도 있습니다만)
numer <- sum(vector1 * vector2)
denom <- sqrt(sum(vector1^2)) * sqrt(sum(vector2^2))
numer / denom

# lsa 패키지에서 제공하는 펑션으로 구하기
install.packages("lsa")
library(lsa)
cosine(vector1, vector2)

R도 되지만 Pyhon이나 다른 언어들로도 당연히 계산이 됩니다.  엑셀도 됩니다. 그리고 위의 코드에도 나와 있지만 특별한 경우가 아니라면 굳이 계산식을 따로 코딩으로 구현할 필요는 없습니다. 대부분 소프트웨어 내에서 자체 제공하거나 컴퓨터 언어들의 추가 패키지, 라이브러리로 제공하고 있어서 매우 쉽게 계산이 가능합니다.

여기에서 벡터 내적은 왜 나오는 것일까?

200px-Dot_Product.svg

내적에서 무너질 분들이 좀 있을텐데요. 고교 수학에 분명 나옵니다. 기억 안나도 괜찮습니다. 어차피 이것도 설명할꺼니까요.

벡터의 내적은 검색을 해 보시면 자료가 많이 나올 것입니다. 수학적으로 매우 중요한 것 중 하나입니다.  특히 선형대수학은 벡터의 내적이 없으면 있으나 마나한 물건이 됩니다. 사실 코사인 유사도를 이해못하는 대부분의 이유는 내적을 이해하지 못하기 때문입니다.

위의 삼각형 그림을 다시 살펴보면 코사인 법칙은 피타고라스 정리에서 출발했기 때문에 코사인값을 구하려면 두 벡터가 만드는 내부의 도형이 직각삼각형이어야 합니다. 그런데 위의 그림을 보시면 A, B와 원점이 만드는 도형이 직각삼각형이 아닌 것을 알 수 있습니다. 그냥 삼각형입니다. 하다 보면 저런 삼각형이 운이 좋게 직각삼각형인 경우도 있겠지만 매우 드물고 요행으로 “직각삼각형이 될지어다!” 라고 바라는 것은 일반화된 해결책이 아니기 때문에 의미가 없습니다.

직각삼각형이 되는 것은 위의 그림에서는 |A|와 B와 원점으로 만든 삼각형입니다. 직각 표시 보이시죠? 그래서 직각삼각형으로 만들어서 피타고라스 정리에서 출발한 코사인 법칙으로 사잇각을 계산하려면 |A|의 길이를 알아내야만 합니다.

|A|의 길이를 알아내는 방법이 벡터의 내적을 이용하는 것입니다.

A와 B의 두 벡터가 있을 때 A와 B의 내적을 계산하면 B * |A| 또는 A * |B|를 구할 수 있습니다.

여기서 |A|는 A를 B의 벡터의 선상(또는 연장선상)으로 직교(직각)이 되게 그대로 내린(정사영 영어로는 projection 한다고 표현합니다) 곳과 원점까지의 거리입니다. |B|는 반대편으로 B를 정사영 한 것입니다.

B 곱하기 |A| = |B| 곱하기 A

정사영을 어느 쪽으로 하던지 상관없습니다. 두 값은 항상 동일합니다. 왜 동일한지까지 설명하려면 지면이 너무 많이 필요해서 생략하겠습니다. (곰곰히 생각해 보면 같을 수 밖에 없다는 것을 알 수 있습니다)

네. 두 벡터의 내적값은 항상 1개입니다. 어느 방향으로 구해도 똑 같습니다.

결국 두 벡터의 내적을 구해서 벡터 하나의 크기로 나누면 |A| 또는 |B|의 길이를 구할 수 있어서 저 상황에서 직각삼각형을 만들 수 있습니다. 더 정확하게 말하면 직각삼각형을 이루는 구성요소인 세 변을 모두 찾아 낼 수 있습니다.그러면 비로서 코사인값을 계산할 수 있게 됩니다.

간단한 요약

벡터의 내적은 왜 필요하냐?
두 벡터 사이에 있는 직각삼각형때문에요.
직각삼각형은 왜 나오냐?
코사인값을 구해야 해서요.
코사인값이 직각삼각형과 무슨 관계냐?
코사인법칙이 피타고라스 정리에서 나왔기 때문에요.

그래서 두 벡터가 유사한지 어떻게 알 수 있는가?

이름이 코사인 유사도이니 이것의 용도가 어떤것이 유사한지 아닌지를 확인하는  것이라는것은 유추할 수 있는데 두 개의 벡터만으로는 서로 유사한지 아닌지를 알기 어렵습니다. 아니 모릅니다.  유사한지 아닌지와 가까운지 먼지 판단하는 기준은 상대적인 것입니다. 물론 절대적인 기준값을 하나 정해놓고 유사하다 아니다를 결정하는데 사용해도 됩니다. 하지만 그 기준값을 또 직접 결정해서 만들어줘야하고 적당히 좋은 값을 정하기가 더 어렵습니다.

위에서 잠깐 설명했지만 코사인의 특징으로 두 벡터의 각이 호도법으로 0도가 되면 코사인 유사도값은 1이되고 호도법으로 각이 커질수록(90도에 가까워 질수록) 0에 가까워진다는 것입니다.  조금 풀어서 설명하면 코사인유사도가 0 또는 Inf가 되면 전혀 유사하지 않은 직교(orthogonal)가 되고 1이 되면 두 벡터의 원점으로부터의 방향이 완전히 겹치게 됩니다.  그런데 이것만으로는 유사한지 아닌지를 판단하기 어렵습니다. 호도법으로 45도 보다 각이 작으면 유사하다고 해야 할까요?

유사도는 상대적인 개념이기 때문에 벡터 2개로는 두 벡터가 유사한지 아닌지를 알기는 어렵습니다. 벡터가 최소 3개 이상은 있어야 합니다.

그래서 N개의 벡터가 있고 A라는 1개의 벡터가 있을때 A벡터와 가장 가까운 벡터를 N개 중에서 찾을 때 코사인 유사도를 사용해서 코사인 값이 가장 큰 것을 선택해서 사용합니다. 당연히 코사인값은 N번 계산해야 합니다.

A벡터가 상대적으로 가장 가까운 벡터는 어떤 것인가를 찾는 것입니다.

어째서 두 벡터의 직선 거리를 계산하지 않고 각도를 사용하는가?

두 벡터가 가까운지 아닌지를 찾는 방법중에 가장 쉬운 것이 직선거리를 계산하는 유클리디안 거리(Euclidean distance)입니다.  유클리디안 거리의 문제점은 각 축의 숫자값의 크기에 따라 영향을 크게 받는다는 것입니다.  축이 수량값을 나타내는 것이고 각축의 값들이 매우 큰 벡터들가 매우 적은 벡터들이 섞여 있다면 벡터의 성향보다는 양적수치가 비슷한 벡터끼리 가깝게 계산되는 경우가 많습니다.

물론 이것도 나름대로 의미가 있기 때문에 이 자체로 큰 문제가 있는 것은 아닙니다. 하지만 양적 크기가 차이 클 것이 뻔한 데이터들은 유클리디안 거리를 사용하면 대부분 문제가 있습니다.

적용하려는 데이터의 특징과 사용하는 사람의 의도에 맞는 것을 선택하면 되는 것입니다.

텍스트마이닝에서의 코사인 유사도

앞서에서도 잠깐 설명을 했지만 코사인 유사도는 텍스트 데이터(텍스트 마이닝)에 사용하는 경우가 많습니다.  물론 다른 곳에도 많이 쓰입니다만 자료를 찾아보면 눈에 쉽게 띄는 쪽은 텍스트 마이닝과 관련된 것 입니다. 텍스트에서 추출한 텀(term, 단어, 색인어)들의 빈도의 분포가 지수 스케일이기 때문에 벡터의 사잇각을 두고 비슷한 방향인지 아닌지를 보는 방법이고 이것이 비교적 합리적인 방법이기 때문입니다.

그리고 검색엔진에서 질의어로 본문을 찾아서 유사한 것을 찾는데 질의어와 본문이 가지고 있는 단어들의 빈도수 같은 것의 양적차이가 매우 크기 때문에 코사인 유사도가 유리합니다.

텍스트마이닝에서 코사인 유사도의 주의점

텍스트 데이터는 분량(데이터 사이즈)이 많기 때문에 코사인유사도 값을 구하려고 해도 현실에서는 컴퓨터 연산이 많이 소모되어서 하지 못하는 경우도 있습니다. 이런 작업을 하려면 사실은 대부분의 경우 분산 프로세싱을 수행해서 계산해야 합니다.  크기가 적당하다면 RDBMS를 사용하고 아니면 Hadoop이나 Spark같은 분산 컴퓨팅 환경(빅데이터 플랫폼)에서 작업해야 할 수도 있습니다.

Excel이나 R, Python으로는 계산하기에 너무 무거울 정도로 문서가 많고 그 상황에서도 코사인유사도 계산을 해야 한다면 코사인 유사도를 계산하는 과정을 완전히 이해 해두는 것이 나중에 코사인 유사도를 직접 구현해서 계산할 때를 위해서는 좋습니다.

요즘은 빅데이터 플랫폼들이 좋아서 기능을 기본 제공하는 경우도 많습니다.  그리고 어렵지 않게 구현할 수 있기 때문에 빅데이터 플랫폼으로 손쉽게 대량의 벡터들이나 텍스트데이터에서의 텀들의 유사도를 구할 수 있습니다.

텍스트 데이터를 가지고 코사인 유사도(Cosine Similarity)를 구해서 문서간의 유사도를 구하는 것은 다음 포스트에 해 보겠습니다.

코사인 유사도의 공식 유도?

코사인 유사도는 코사인 제2법칙에서 유도한 것이라고 되어 있고 실제로 유도가 됩니다. 여기서 유도하는 것은 설명하지 않겠습니다.  딱 한 번 해봤는데 기억도 안나고 하기 싫습니다. 재미 없습니다. 혹시 궁금하신 분은 자료를 찾아 보시면 그렇게 어렵지 않게 해볼 수 있습니다. 그런데 실제 응용에는 공식 유도가 크게 중요하지 않습니다.

TFIDF – Term Frequency Inverse Document Frequency