작동하는 간단한 검색 엔진 만들기

3 weeks ago 4

  • 기존 데이터베이스를 활용해 외부 서비스 없이 동작하는 검색 엔진 구조를 구현, 토큰화·가중치·스코어링을 중심으로 구성
  • 핵심 아이디어는 모든 텍스트를 토큰화 후 저장하고, 검색 시 동일한 방식으로 토큰을 매칭해 관련도를 계산하는 방식
  • Word, Prefix, N-Gram 토크나이저를 조합해 정확 일치·부분 일치·오타 대응을 모두 처리하며, 각 토크나이저는 고유한 가중치를 가짐
  • 가중치 시스템과 SQL 기반 스코어링 알고리듬을 통해 문서 길이, 토큰 다양성, 평균 품질 등을 종합 평가
  • 확장성과 투명성이 높아, 새로운 토크나이저나 문서 타입 추가, 가중치 조정, 스코어링 수정이 자유로운 구조

직접 검색 엔진을 만드는 이유

  • Elasticsearch나 Algolia 같은 외부 서비스는 강력하지만 복잡한 API 학습과 인프라 관리 부담이 존재
  • 단순히 기존 데이터베이스와 통합되어 작동하고, 디버깅이 쉬운 검색 기능이 필요할 때 직접 구축이 유용
  • 목표는 외부 의존성 없이 관련성 높은 결과를 반환하는 단순한 검색 엔진

핵심 개념: 토큰화와 매칭

  • 기본 원리는 모든 텍스트를 토큰화(tokenize)하여 저장하고, 검색 시 동일한 방식으로 토큰을 생성해 매칭
  • 인덱싱 단계에서 콘텐츠를 토큰 단위로 분리하고 가중치와 함께 저장
  • 검색 단계에서는 쿼리를 동일하게 토큰화하여 일치하는 토큰을 찾고 점수를 계산
  • 스코어링 단계에서 저장된 가중치를 활용해 관련도 점수를 산출

데이터베이스 스키마 설계

  • 두 개의 테이블 사용: index_tokens와 index_entries
    • index_tokens: 고유 토큰과 토크나이저별 가중치 저장
    • index_entries: 토큰과 문서를 연결하며, 필드·문서·토크나이저 가중치를 반영한 최종 점수 저장
  • 최종 가중치 계산식:
    field_weight × tokenizer_weight × ceil(sqrt(token_length))
  • 인덱스는 문서 조회, 토큰 조회, 필드별 쿼리, 가중치 필터링을 위해 설정

토큰화 시스템

  • WordTokenizer: 단어 단위 분리, 짧은 단어 제거, 정확 일치용 (가중치 20)
  • PrefixTokenizer: 단어 접두사 생성, 자동완성 및 부분 일치용 (가중치 5)
  • NGramsTokenizer: 고정 길이 문자 조합 생성, 오타 및 부분 일치 대응 (가중치 1)
  • 모든 토크나이저는 소문자 변환, 특수문자 제거, 공백 정규화를 공통 수행

가중치 시스템

  • 필드 가중치: 제목·본문·키워드 등 중요도 반영
  • 토크나이저 가중치: Word > Prefix > N-Gram 순
  • 문서 가중치: 위 두 요소와 토큰 길이를 결합해 계산
  • ceil(sqrt()) 함수를 사용해 긴 토큰의 영향력을 완화하며, 필요 시 로그나 선형 함수로 조정 가능

인덱싱 서비스

  • IndexableDocumentInterface를 구현한 문서만 인덱싱 가능
  • 문서 생성·수정 시 이벤트 리스너나 명령어(app:index-document, app:reindex-documents)로 인덱싱 수행
  • 절차:
    • 기존 인덱스 제거 후 새 토큰 생성
    • 각 필드에 대해 모든 토크나이저 실행
    • 토큰 존재 여부 확인 후 생성(findOrCreateToken)
    • 계산된 가중치로 index_entries에 배치 삽입(batch insert)
  • 중복 방지, 성능 향상, 업데이트 대응 구조

검색 서비스

  • 쿼리를 동일한 토크나이저로 처리해 인덱싱과 동일한 토큰 세트 확보
  • 토큰 중복 제거 후 길이순 정렬(긴 토큰 우선), 최대 300개로 제한
  • SQL 쿼리를 통해 토큰과 문서를 조인하고, 관련도 점수 계산 및 정렬
  • 결과는 SearchResult(documentId, score) 형태로 반환

스코어링 알고리듬

  • 기본 점수: SUM(sd.weight)
  • 토큰 다양성 보정: LOG(1 + COUNT(DISTINCT token_id))
  • 평균 가중치 보정: LOG(1 + AVG(weight))
  • 문서 길이 패널티: 1 / (1 + LOG(1 + token_count))
  • 정규화(normalization) : 최대 점수로 나누어 0~1 범위로 조정
  • 최소 토큰 가중치 필터(st2.weight >= ?)를 통해 의미 없는 저가중치 일치 제거

결과 처리

  • 검색 결과는 문서 ID와 점수로 반환되며, 저장소(repository)를 통해 실제 문서로 변환
  • FIELD() 함수를 사용해 검색 결과 순서를 유지한 채 문서 조회

시스템 확장성

  • 새로운 토크나이저는 TokenizerInterface 구현으로 추가 가능
  • 새로운 문서 타입은 IndexableDocumentInterface 구현으로 등록
  • 가중치나 스코어링 로직은 SQL 수정만으로 조정 가능

결론

  • 이 구조는 단순하지만 실제로 작동하는 검색 엔진으로, 외부 인프라 없이도 충분한 성능 제공
  • 명확한 로직, 완전한 제어, 쉬운 디버깅이 장점
  • 복잡한 시스템보다 직접 이해하고 제어 가능한 코드가 더 가치 있다는 점을 강조

Read Entire Article