전문 검색
고친 사람 github-actions[bot]
전문 검색은 글의 본문 어디에 있든 찾는 낱말이 든 문서를 골라 주는 일입니다. 제목이나 번호처럼 정해 둔 값이 아니라 본문 속 낱말로 찾습니다. 글을 미리 낱말 단위로 정리해 두기 때문에 문서가 많아도 빨리 답합니다. 대개 잘 맞는 문서부터 줄 세워 돌려줍니다.
쉽고 빠른 이해
전문 검색은 낱말 하나로 글 더미에서 그 낱말이 든 글을 찾아 주는 일입니다. 게시판 검색창에 「환불」을 치면 본문 어딘가에 「환불」이 든 글까지 나오는 것이 이 일입니다.
이게 없으면 데이터베이스가 모든 글을 처음부터 끝까지 읽어야 합니다. 글이 늘수록 오래 걸립니다. 「달렸다」로 쓴 글을 「달리」로 찾아도 안 나옵니다.
이렇게 돕니다.
- 글이 들어오면 낱말로 쪼개고 낱말을 원래 모양으로 되돌립니다
- 낱말마다 그 낱말이 든 글의 번호를 적어 둡니다
- 검색어도 같은 방식으로 쪼개 번호를 꺼냅니다
- 꺼낸 글마다 얼마나 잘 맞는지 점수를 매겨 높은 순으로 돌려줍니다
대가도 있습니다. 낱말 목록을 따로 두므로 저장 공간이 더 듭니다. 글을 넣을 때마다 쪼개고 적는 일이 붙습니다. 뜻이 같아도 낱말이 다른 글은 못 찾습니다.
본문 속 낱말로 찾아야 할 때 씁니다. 주문번호나 금액처럼 값이 딱 떨어지는 조건에는 필요 없습니다.
상세
10년 치 편지를 모아 둔 상자에서 이사 이야기가 나온 편지를 찾는다고 해 봅시다. 한 통씩 다 읽으면 하루가 갑니다. 편지를 받을 때마다 공책에 낱말과 편지 번호를 적어 두었다면 공책의 「이사」 쪽만 펴면 됩니다.
전문 검색은 문서의 본문 전체를 대상으로 낱말을 찾아, 그 낱말이 든 문서를 골라내는 일입니다. 여기서 「전문」은 글 전체(全文)라는 뜻입니다. 전문가의 「전문」과는 한자가 다릅니다. 영어로는 full-text search 입니다.
게시판 검색창에 「환불」을 치면 제목에 「환불」이 없는 글도 나옵니다. 본문 한가운데에 「환불 받았어요」라고 적힌 글이 그렇습니다. 이렇게 본문 속 낱말로 찾아 주는 것이 전문 검색입니다.
이 편에서 쓸 낱말 둘을 먼저 정해 둡니다. 찾는 대상 한 건이 문서입니다. 게시글 하나, 상품 설명 하나, 로그 한 줄이 각각 문서 하나입니다. 사용자가 던지는 검색어는 질의입니다.
값으로 찾기와 낱말로 찾기
전문 검색이 따로 이름을 얻은 까닭은 흔한 조회와 견주면 보입니다. 이 소절은 두 조회가 무엇을 기준으로 답하는지를 봅니다.
데이터베이스 조회는 대개 값으로 찾습니다. 주문번호가 1234인 주문, 금액이 만 원을 넘는 주문이 그렇습니다. 답은 맞거나 틀리거나 둘 중 하나입니다.
전문 검색은 글 속 낱말로 찾습니다. 그래서 답에 정도가 생깁니다. 「한강에서 달리는」으로 찾으면 두 낱말이 다 든 글이 한 낱말만 든 글보다 더 잘 맞습니다.
LIKE 로 찾으면 막히는 곳
관계형 데이터베이스에서 글 속 낱말을 찾는 가장 쉬운 길은 SQL(Structured Query Language, 구조화 질의 언어)의 LIKE 입니다. 이 소절은 글 세 개를 두고 그 길이 어디서 막히는지 봅니다.
post 테이블에 글 세 개가 들어 있습니다.
| id | body |
|---|---|
| 1 | 매일 아침 한강을 달립니다 |
| 2 | 어제 한강을 달렸다 |
| 3 | 한강 공원 주차 안내 |
달리기 이야기를 찾으려고 LIKE 에 「달리」를 넣어 봅니다. % 는 아무 글자가 몇 개든 와도 된다는 뜻입니다. 앞뒤에 붙이면 「달리」가 글의 어디에 있어도 걸립니다.
SELECT id FROM post
WHERE body LIKE '%달리%'; -- 결과 없음
1번과 2번이 달리기 이야기입니다. 그런데 하나도 안 걸렸습니다. 「달립니다」와 「달렸다」에는 「달리」라는 글자 묶음이 없습니다. 한글은 받침이나 어미가 붙으면 글자 자체가 바뀌기 때문입니다.
걸렸다고 해도 LIKE 는 들었는지 안 들었는지만 답합니다. 어느 글이 더 잘 맞는지는 알려 주지 않습니다.
속도도 문제입니다. 데이터베이스의 인덱스는 조회를 빠르게 하려고 값을 정렬해 둔 목록입니다. 값을 앞 글자부터 정렬하므로 첫 글자를 알아야 찾아 들어갈 수 있습니다. 국어사전을 첫 글자로 찾아 들어가는 것과 같은 방식입니다.
'%달리%' 처럼 앞에 % 가 붙으면 첫 글자를 모릅니다. 그래서 인덱스가 돕지 못합니다. 데이터베이스는 모든 행을 하나씩 열어 봅니다. 이것이 풀 테이블 스캔입니다.
글을 낱말로 쪼개 두는 준비
전문 검색은 이 막힘을 글이 들어올 때 미리 풀어 둡니다. 첫 준비는 글을 낱말로 쪼개고 낱말의 모양을 고르는 일입니다.
글을 낱말 단위로 쪼개는 일이 토큰화입니다. 쪼개진 조각 하나가 토큰입니다. 영어는 띄어쓰기에서 자르면 대체로 낱말이 나옵니다.
쪼갠 뒤에는 모양을 고릅니다. 대문자는 소문자로 바꿉니다. 「달립니다」와 「달렸다」는 원래 모양 「달리다」로 되돌립니다. 모양이 달라도 같은 낱말이면 같은 토큰이 되게 하려는 것입니다.
한국어는 띄어쓰기만으로는 안 됩니다. 뜻을 가진 가장 작은 말 단위를 형태소라고 합니다. 「한강을」은 「한강」과 조사 「을」 두 형태소로 되어 있습니다. 글을 형태소로 나누고 원래 모양을 찾는 일이 형태소 분석입니다.
쪼개고 고르는 일을 한데 묶은 부품이 분석기입니다. 세 글이 형태소 분석을 쓰는 분석기를 지나면 대략 이렇게 됩니다.
| id | 본문 | 분석기를 지난 뒤 |
|---|---|---|
| 1 | 매일 아침 한강을 달립니다 | 매일 · 아침 · 한강 · 달리다 |
| 2 | 어제 한강을 달렸다 | 어제 · 한강 · 달리다 |
| 3 | 한강 공원 주차 안내 | 한강 · 공원 · 주차 · 안내 |
조사와 어미가 떨어졌습니다. 동사는 원래 모양으로 돌아왔습니다. 이제 1번과 2번에는 똑같이 「달리다」가 들어 있습니다.
질의도 같은 분석기를 지납니다. 「한강에서 달리는」을 치면 「한강」과 「달리다」가 됩니다. 넣을 때와 찾을 때 낱말 모양이 같아야 둘이 만납니다.
역색인
둘째 준비는 쪼갠 낱말을 찾아보기 표에 적는 일입니다. 이 표가 있어서 모든 행을 열어 보지 않아도 됩니다.
색인은 앞에서 본 인덱스를 우리말로 옮긴 말입니다. 찾기 쉽게 미리 정리해 둔 목록이라는 뜻은 같습니다. 책 뒤쪽의 찾아보기가 색인입니다. 이 편에서는 데이터베이스의 보통 인덱스와 가르려고 전문 검색 쪽에 색인이라는 말을 씁니다.
역색인은 낱말마다 그 낱말이 든 문서 번호를 적어 둔 색인입니다. 이 편에서 뒤로 그냥 「색인」이라 하면 이 역색인을 가리킵니다.
「역」은 방향이 거꾸로라는 뜻입니다. 문서를 열면 그 안의 낱말이 보이는 것이 보통 방향입니다. 역색인은 낱말에서 문서로 갑니다.
세 글로 만든 역색인은 아래와 같습니다.
| 낱말 | 든 문서 |
|---|---|
| 한강 | 1, 2, 3 |
| 달리다 | 1, 2 |
| 매일 · 아침 | 1 |
| 어제 | 2 |
| 공원 · 주차 · 안내 | 3 |
「달리다」를 찾으면 이 표의 한 줄만 읽고 1번과 2번을 얻습니다. 문서가 셋이든 천만 개든 읽는 줄은 질의의 낱말 수만큼입니다. 모든 행을 열어 보던 LIKE 와 갈리는 대목이 이것입니다.
낱말 하나 옆에 붙은 문서 번호 목록이 포스팅 리스트입니다. 위 표에서 「한강」의 포스팅 리스트는 1, 2, 3 입니다.
잘 맞는 순서로 세우기
후보를 찾았으면 순서를 정합니다. 이 소절은 세 글과 질의 「한강에서 달리는」으로 순서가 정해지는 과정을 봅니다.
질의는 분석기를 지나 「한강」과 「달리다」가 됩니다. 역색인에서 두 줄을 읽으면 「한강」은 1·2·3번을, 「달리다」는 1·2번을 내줍니다.
두 낱말이 다 든 문서만 원하면 겹치는 1·2번만 남깁니다. 하나라도 든 문서를 원하면 셋 다 남습니다. 이렇게 「그리고」·「또는」으로 후보를 거르는 방식이 불리언 검색입니다.
후보가 여럿이면 줄을 세웁니다. 기준은 관련도입니다. 관련도는 문서가 질의와 얼마나 잘 맞는지를 나타낸 점수입니다. 두 낱말이 다 든 1·2번이 「한강」만 든 3번보다 높습니다.
점수를 움직이는 신호는 대개 셋입니다.
| 신호 | 점수가 오르는 쪽 | 까닭 |
|---|---|---|
| 낱말이 문서에 나온 횟수 | 여러 번 나온 문서 | 그 낱말을 두고 쓴 글일 가능성이 크다 |
| 낱말이 얼마나 드문가 | 드문 낱말이 맞은 문서 | 어느 글에나 있는 낱말은 문서를 가려 주지 못한다 |
| 문서 길이 | 짧은 문서 | 긴 글에는 아무 낱말이나 한 번쯤 들어 있기 쉽다 |
둘째 줄은 세 글에서 바로 보입니다. 「한강」은 세 글에 다 있어서 어느 글이 더 맞는지 가려 주지 못합니다. 「달리다」는 두 글에만 있어서 1·2번을 3번 위로 올립니다.
표의 첫째 신호인 나온 횟수가 TF(Term Frequency, 낱말 빈도)입니다. 둘째 신호인 드문 정도가 IDF(Inverse Document Frequency, 역문서 빈도)입니다. 그 낱말이 든 문서가 적을수록 IDF 가 커집니다. 문서 수와 거꾸로 움직여서 「역」이 붙었습니다.
두 값을 곱해 점수 하나로 묶은 셈식이 TF-IDF입니다. 한 문서에 여러 번 나오면서 다른 문서에는 드문 낱말이 든 문서일수록 점수가 높습니다.
BM25는 이 두 신호에 셋째 신호인 문서 길이까지 넣어 다듬은 셈식입니다. 긴 글이 낱말을 많이 품었다는 까닭만으로 위에 오는 것을 막습니다. 이름의 BM 은 Best Matching(가장 잘 맞춤)의 줄임말입니다. 25 는 여러 판으로 고쳐 온 셈식 가운데 이 판에 붙은 번호입니다.
넣는 길과 찾는 길
지금까지 본 준비와 찾기를 한 그림에 모으면 이렇습니다.
flowchart TD
subgraph IN["글을 넣을 때"]
D["문서"] --> A1["분석기"]
end
A1 --> I["역색인"]
subgraph OUT["찾을 때"]
Q["질의"] --> A2["분석기"]
A2 --> L["낱말마다 문서 번호를 꺼낸다"]
L --> S["관련도 점수를 매긴다"]
S --> R["점수 높은 순 결과"]
end
I --> L
그림에서 볼 것은 분석기가 두 길에 다 있다는 점입니다. 두 길은 역색인에서 만납니다. 넣는 쪽의 분석기와 찾는 쪽의 분석기가 다르게 자르면 이 만남이 어긋납니다.
띄어쓰기로 안 갈리는 글
분석기가 낱말을 제대로 못 자르면 역색인도 틀어집니다. 이 소절은 공백만으로 낱말이 안 나오는 언어에서 쓰는 두 가지 자르는 법을 견줍니다.
중국어와 일본어는 낱말 사이를 띄우지 않습니다. 한국어는 띄우지만 조사와 어미가 낱말에 붙어 다닙니다. 어느 쪽이든 공백에서 자르기만 해서는 낱말이 안 나옵니다.
한 가지 길은 앞에서 본 형태소 분석입니다. 사전을 보고 뜻 단위로 자릅니다.
다른 길은 글자를 정해진 개수씩 겹쳐 잘라 토큰으로 삼는 것입니다. 이렇게 자른 조각이 n-gram입니다. 두 글자씩 자르면 「한강을」은 「한강」과 「강을」이 됩니다.
| 자르는 법 | 얻는 것 | 치르는 것 |
|---|---|---|
| 형태소 분석 | 「한강을」에서 「한강」만 남는다 | 사전에 없는 새 낱말을 잘못 자른다 |
| n-gram | 사전이 필요 없다. 낱말의 일부로도 찾힌다 | 토큰이 많아져 색인이 커진다. 엉뚱한 글도 걸린다 |
n-gram 의 「엉뚱한 글」은 「강을」 같은 조각에서 옵니다. 「강을 건넜다」로 찾으면 「한강을」이 든 글도 걸립니다.
데이터베이스 안과 검색 엔진
전문 검색은 제품 이름이 아니라 기능 이름입니다. 같은 기능을 두 곳에서 돌릴 수 있습니다.
하나는 데이터베이스 안입니다. PostgreSQL, MySQL, SQLite 같은 관계형 데이터베이스는 전문 검색 기능을 제공합니다. 원본 테이블 옆에 전문 검색용 색인을 하나 더 만들어 쓰는 식입니다. 서버를 따로 두지 않아도 됩니다.
다른 하나는 전문 검색을 맡으려고 만든 검색 엔진입니다. Elasticsearch와 OpenSearch가 그런 제품입니다. 문서가 한 대에 안 들어가면 여러 대에 나눠 담습니다. 질의는 그 여러 대에 모두 보냅니다. 돌아온 결과를 모아 다시 줄 세웁니다.
어느 쪽이 맞는지는 상황이 정합니다.
| 상황 | 맞는 곳 |
|---|---|
| 문서가 많지 않고 검색이 부가 기능이다 | 데이터베이스 안 |
| 검색이 서비스의 중심이고 문서가 한 대를 넘는다 | 검색 엔진 |
| 쌓이는 로그에서 오류 메시지를 낱말로 뒤진다 | 검색 엔진 |
| 주문번호나 금액처럼 값이 딱 떨어지는 조건이다 | 전문 검색이 아닌 보통 조회 |
치르는 대가
전문 검색을 들이면 치르는 것이 넷 있습니다.
| 대가 | 까닭 |
|---|---|
| 저장 공간이 더 든다 | 원본 글 옆에 역색인을 따로 둔다 |
| 쓰기에 일이 붙는다 | 글을 넣을 때마다 쪼개고 색인에 적는다 |
| 넣은 글이 곧바로 안 보일 수 있다 | 색인에 적는 일을 모아서 한꺼번에 하면, 그 사이에 넣은 글은 아직 색인에 없다 |
| 결과가 분석기에 달린다 | 낱말을 잘못 자르면 있는 글도 못 찾는다 |
검색 엔진을 따로 두면 하나가 더 붙습니다. 원본 데이터베이스와 검색 엔진의 내용이 어긋나지 않게 계속 맞춰 줘야 합니다. 방법은 둘입니다.
첫째는 이중 쓰기입니다. 애플리케이션이 데이터베이스에 쓴 뒤 검색 엔진에도 한 번 더 씁니다.
둘째는 변경 데이터 캡처입니다. 데이터베이스는 바뀐 내용을 차례로 적어 두는 변경 기록을 갖고 있습니다. 이 기록을 읽어 검색 엔진으로 옮기는 방식입니다.
낱말이 달라 놓치는 글
전문 검색은 낱말이 맞는지를 봅니다. 뜻이 맞는지는 모릅니다. 「환불」로 찾으면 「돈을 돌려받았어요」라고만 쓴 글은 안 나옵니다.
이 틈을 메우는 한 가지 길은 동의어 사전입니다. 같은 뜻의 낱말을 미리 묶어 둡니다. 찾을 때는 질의를 그 묶음으로 넓힙니다.
다른 길은 벡터 검색입니다. 글의 뜻을 숫자 목록으로 바꿔 둡니다. 그리고 가까운 숫자 목록을 가진 글을 찾습니다.
두 방식을 함께 쓰기도 합니다. 전문 검색의 점수와 벡터 검색의 점수를 섞어 쓰는 방식이 하이브리드 검색입니다.
관련 항목
전문 검색이 속한 분야와 다루는 대상
검색 · 자연어 처리 · 데이터베이스 · 문서 · 질의
전문 검색 없이 글 속 낱말을 찾는 방법
LIKE · 풀 테이블 스캔 · 정규 표현식 · B-tree · 인덱스
글을 넣을 때 거치는 처리 단계
분석기 · 토큰화 · 토큰 · 형태소 · 형태소 분석 · 어간 추출 · 표제어 추출 · 불용어 · n-gram · 유니코드 정규화
전문 검색을 받치는 색인 구조
색인 · 역색인 · 포스팅 리스트 · GIN · 세그먼트
후보를 고르는 질의 방식
불리언 검색 · 구문 검색 · 접두사 검색 · 퍼지 검색 · 와일드카드 검색 · 자동 완성 · 패싯
결과 순서를 정하는 점수 모델
관련도 · 랭킹 · TF-IDF · BM25 · 벡터 공간 모델 · 재순위화
검색 결과가 잘 맞는지 재는 지표
낱말 대신 뜻으로 찾는 방식
벡터 검색 · 시맨틱 검색 · 임베딩 · 하이브리드 검색 · 동의어 사전
전문 검색을 구현한 제품
검색 엔진 · Elasticsearch · OpenSearch · Apache Solr · Apache Lucene · Meilisearch · PostgreSQL 전문 검색 · PostgreSQL · MySQL · SQLite · FTS5
원본 데이터베이스와 색인을 맞추는 방법
다른 이름: full-text search · full text search · 전문검색 · 풀텍스트 검색