이번 글에서는 내가 KBooks 사이트에 검색 기능을 처음 추가했던 과정을 공유한다.

국립중앙도서관 API에서 수집한 모든 도서 데이터는 이미 dbt 파이프라인을 통해 Supabase(Postgres) 로 들어오고 있었다. 이제 검색을 붙여보겠다.

아래는 데이터베이스 수준에서 검색을 준비한 단계별 과정과, 각각의 결정이 왜 타당했는지에 대한 설명이다.

1. 깨끗한 출발점: silver_books

원본 데이터는 raw_nl_books에 JSON 형태로 저장된다. 여기서 dbt를 통해 silver_books 테이블을 만들었고, 각 행은 ISBN-13으로 식별되는 고유한 책 하나를 나타낸다. 유효한 ISBN만 남겨 중복을 제거했다.

이 테이블에는 이미 사용자가 검색할 만한 주요 필드가 있었다: title, author, publisher_name, subject, book_introduction

그래서 가장 자연스러운 생각은 이거였다.

“이 테이블 위에 그냥 검색을 얹으면 되겠네?”

2. Postgres에서 “검색”의 의미 이해하기

Postgres는 텍스트 검색을 위해 두 가지 주요 방식을 제공한다.

🔍 Full-Text Search (FTS)

FTS는 텍스트를 단어 단위로 쪼개어(예: “Harry”, “Potter”) 단순 문자열 비교가 아닌 의미 기반 검색을 수행한다. 또한 관련도(ts_rank) 에 따라 결과를 정렬할 수 있어 가장 적합한 결과가 상단에 온다.

✏️ Trigram 검색

Trigram은 텍스트를 세 글자 단위로 나누는 방식이다. 이 인덱스를 사용하면 오타나 부분 일치가 있는 경우에도 검색이 가능하다. 예를 들어, “해리포”라고 쳐도 “해리 포터”를 찾을 수 있다. 한국어처럼 띄어쓰기나 철자 변형이 잦은 언어에도 유용하다.

이 두 가지를 함께 쓰면, 외부 검색 엔진 없이도 정확 검색 + 유사 검색을 모두 커버할 수 있다.

3. 무엇을 인덱싱할지 (그리고 무엇을 뺄지) 결정하기

처음에는 모든 텍스트 필드(title, author, publisher, subject, introduction)를 인덱싱했다. 잘 작동했지만, 불필요한 결과가 많았다. 예를 들어, ‘해리 포터’가 단 한 번 소개글(book_introduction) 에 등장한 책도 검색 결과에 포함됐다.

그래서 다음처럼 단순화했다:

  • 포함: title, author
  • 제외: subject, book_introduction, publisher_name

이 변경만으로도 결과가 훨씬 깔끔해졌고, 사용자가 기대하는 “정확한 검색 결과”가 나왔다.

4. dbt로 구현하기

dbt에서 데이터 변환과 인덱스 생성을 하나의 파이프라인으로 관리했다.

  • FTS 컬럼 생성: tsv = to_tsvector('simple', title || ' ' || author) → 검색 가능한 단어들을 압축 요약한 컬럼을 만든다.

  • 인덱스 생성 (dbt post_hook 사용):

    1. unique(isbn13) → 중복 제거용 기본 키
    2. GIN(tsv) → 전체 텍스트 검색용
    3. GIN(title gin_trgm_ops) → 오타나 부분 일치 탐색용

dbt는 테이블을 재생성할 때마다 자동으로 인덱스를 다시 만든다. 즉, 데이터 갱신과 인덱스 관리가 완전히 자동화된다.

5. 결과 검증 (Sanity Check)

다음 쿼리로 테스트했다:

select
  isbn13, title, author_display,
  ts_rank(tsv, plainto_tsquery('simple', '해리 포터')) as rank
from kbooks_dbt.silver_books
where tsv @@ plainto_tsquery('simple', '해리 포터')
order by rank desc
limit 20;

결과: ‘해리 포터’ 관련 도서만 나오고, 설명에 잠깐 언급된 책은 제외됐다. 정확성과 관련도 모두 만족.

6. 아키텍처 선택 이유 요약

목표 선택 이유
빠른 검색 GIN 인덱스 수백만 행을 스캔하지 않고 O(1)에 가까운 검색
오타·한국어 대응 Trigram 띄어쓰기·부분 일치 지원
단순 구조 silver_books 내부 처리 별도 테이블이나 동기화 작업 불필요
깔끔한 결과 title + author만 포함 노이즈 최소화
유지보수 용이 dbt에서 변환·인덱스 동시 관리 단일 소스 관리

이 구조는 수백만 행 규모로 확장해도 충분히 버틸 수 있다. 추후 검색 랭킹 페이지나 Meilisearch 같은 외부 엔진을 붙이더라도, silver_books에서 그대로 내보내면 된다 — 웹 로직을 바꿀 필요가 없다.

7. 결과

이제 사용자는 KBooks 웹사이트에서 “해리 포터” 를 입력하면, Supabase에서 즉시 관련 도서를 빠르고 정확하게 찾아볼 수 있다. 서드파티 검색엔진이나 동기화 지연 없이, 잘 인덱싱된 SQL만으로 완전한 검색 경험을 제공한다.

8. 핵심 요약

좋은 검색은 꼭 검색엔진이 필요한 게 아니다. Postgres에 데이터를 이미 저장하고 있다면, to_tsvector와 GIN 인덱스를 배우는 것만으로도 80%는 달성 가능하다.

이 기능을 dbt 파이프라인 안에 직접 넣어두면,

  • 검색은 항상 최신 클린 데이터에서 작동하고
  • 모든 변환·성능 최적화가 버전 관리된 하나의 repo에 담긴다.

그게 바로 내가 KBooks에서 원했던 방식이다 — 깔끔하고, 최소한으로, 그리고 빠르게.