
이 문서는 PostgreSQL의 내장 전체 텍스트 검색 FTS, Full-Text Search 랭킹 기능에만 의존하지 않고, 왜 별도의 BM25 지원 테이블을 PostgreSQL 위에 직접 구현해야 하는지에 대한 아키텍처와 그 작동 방식을 설명합니다.
PostgreSQL은 이미 to_tsvector(...), ts_rank(...), ts_rank_cd(...)와 같은 훌륭한 FTS 기능을 제공합니다.
이는 텍스트 토큰화와 위치 기반 랭킹에는 매우 유용하지만, 완전한 형태의 BM25 모델은 아닙니다.
핵심적인 한계점:
따라서 이 시스템에서 PostgreSQL 내장 FTS는 토크나이저, 형태소 분석기, 어휘 정규화 및 희소 토큰추출기로서의 가치로만 활용되며, 최종적인 어휘 랭킹 모델로는 사용되지 않습니다.
단일 토큰에 대한 표준 BM25 점수 산출 공식은 개념적으로 다음과 같습니다.
이 수식을 계산하기 위해 시스템은 다음 5가지 핵심 입력값을 반드시 추적하고 유지해야 합니다.
BM25를 직접 구현한다는 것은 위 통계값들 문서별 토큰 빈도/길이, 스코프별 총 문서 수/토큰 길이, 토큰별 문서 등장 빈도 을 데이터베이스 내에서 직접 관리해야 함을 의미합니다.
bm25tf 및 bm25idf 테이블의 존재 이유이 패키지는 앞서 언급한 필수 입력값들을 명시적으로 저장하기 위해 별도의 테이블을 운용합니다.
bm25tf 테이블
tf 부분의 값을 제공합니다.bm25idf 테이블
tfdoclang: 언어 전체 스코프에서의 문서 빈도를 나타냅니다.tfdoctable: 개별 테이블스코프에서의 사용을 위해 예약된 공간입니다.df에 국한되지 않고, 언어 기반 검색 해당 언어를 공유하는 모든 데이터셋 과 테이블 기반 검색 특정 단일 데이터셋 두 가지 스코프의 IDF를 모두 지원하도록 설계되었습니다. bm25dataset 테이블
recordcount는 데이터셋의 총 문서 수를, lengthsum은 데이터셋의 총 토큰 길이를 제공합니다. 개별 데이터셋 테이블 내의 tokenlength 컬럼
위 테이블들을 조합하면 BM25 계산에 필요한 모든 통계가 다음과 같이 완벽하게 매핑됩니다.
bm25tf.tf에서 조회tokenlength 컬럼에서 조회bm25dataset.lengthsum / bm25dataset.recordcount로 연산bm25dataset.recordcount에서 조회bm25idf.tfdoclang에서 조회bm25idf.tfdoctable 또는 로컬 동등 연산에서 조회결론적으로, 이 패키지는 가공되지 않은 FTS 출력을 적당히 추정하여 랭킹을 매기는 것이 아니라, BM25 수학적 모델에 필요한 모든 통계값을 명시적으로 재구성하여 정확한 랭킹을 구현합니다.
데이터베이스의 레코드는 끊임없이 변화합니다. INSERT, UPDATE, DELETE 트랜잭션이 발생할 때 시스템은 다음 단계를 거칩니다.
BEFORE 트리거 작동: 변경된 데이터에서 파생 필드인 fts와 tokenlength를 새롭게 계산하고 정규화합니다.AFTER 트리거 작동: 이전 상태와 새로운 상태의 순수 델타 값을 bm25task 테이블에 기록합니다.이러한 트리거 기반 접근 방식은 데이터 원본과 BM25 통계 간의 상태 불일치를 원천적으로 차단합니다.
bm25dataset.status는 데이터셋의 라이프사이클 상태를 추적합니다.
특정 데이터셋이 '삭제 중' 상태에 진입하면, 파괴적인 라이프사이클이 완료될 때까지 동일 언어를 공유하는 다른 데이터셋의 쓰기 작업이 차단될 수 있습니다.
이 락은 단순한 SQL 행 경합을 막기 위한 것이 아닙니다. 장기 실행되는 비동기 BM25 유지보수 작업을 보호하기 위해 존재합니다.
N 및 df 값의 영구적인 왜곡 방지, 청크 단위 유지보수 작업의 오염 방지를 위해 필수적인 안전장치입니다.bm25task가 단순 큐가 아닌 시계열 델타를 저장하는 이유bm25task 테이블은 단순히 처리해야 할 행 ID만 모아두는 큐가 아닙니다.
이 테이블은 이전 토큰 상태와 새로운 토큰 상태를 모두 포함하는 시간순 BM25 상태 전이 기록을 저장합니다.
비동기 처리 특성상 색인 작업이 따라잡기 전에 단일 행이 여러 번 변경될 수 있습니다
예: INSERT 후 연달아 UPDATE 2회 발생.
이러한 내역을 개별적으로 모두 처리하면 시스템 자원 낭비가 심해집니다.
이를 방지하기 위해 pullTaskChunk() 프로세스는 (tablename, id)를 기준으로 작업을 병합합니다.
이 과정을 통해 수많은 이력들이 하나의 최소화된 BM25 델타로 압축되어 시스템 부하를 획기적으로 줄입니다.
BM25 업데이트는 철저하게 작업 기반으로 이루어집니다.
이로 인해 데이터 쓰기 속도는 매우 빠르며, 무거운 색인 작업은 분리되어 워커 프로세스가 감당할 수 있는 양 만큼만 순차적으로 처리합니다.
bm25tf 및 bm25idf에 희소 토큰 업데이트 반영 -> 작업 완료 마킹작업의 상태가 메모리가 아닌 bm25task 테이블에 영구적으로 보존되기 때문에 작업 제어가 매우 유연합니다.
일시정지: 추가적인 청크 처리를 멈추기만 하면 됩니다.
남은 작업은 큐에 안전하게 대기합니다.
취소: 인메모리 상의 작업 핸들만 경량화하여 취소시킵니다.
이미 반영된 청크는 독립적으로 커밋되어 있으며, 남은 작업은 나중에 새로운 잡을 생성하여 언제든 재개할 수 있습니다.
앞서 언급한 락 메커니즘 덕분에, 이 과정에서 발생하는 쓰기 작업 지연이 큐의 무결성을 훼손하지 않습니다.
BM25 수학 모델에서 잘못된 df 문서 빈도 스코프를 사용하면 랭킹의 수학적 일관성이 완전히 무너집니다.
검색 스코프가 특정 언어의 모든 데이터셋이라면, N은 해당 언어 전체의 문서 수여야 하고 df는 tfdoclang을 사용해야 합니다.
검색 스코프가 단일 테이블이라면, N은 해당 테이블의 문서 수여야 하고 df는 tfdoctable을 사용해야 합니다.
설계상 언어 스코프와 테이블 스코프 모두의 IDF 동작 공간을 마련해 둔 것은 이처럼 검색 문맥에 맞는 정확한 수학적 가중치를 부여하기 위함입니다.
이 시스템은 궁극적으로 각 색인 작업이 자신만의 독립된 임시 테이블을 생성하여 작업을 마친 후, 라이브 테이블과 원자적으로 교체하는 방식을 지향합니다.
하지만 이 구조는 아직 완전히 구현되지 않았습니다.
현재 구현 상태: 청크 처리 결과가 운영 중인 bm25tf / bm25idf 테이블에 직접 쓰이고 있습니다.
향후 타겟 아키텍처: *_jobid 형태의 임시 BM25 테이블 생성 -> 해당 공간에서 모든 청크 안전하게 처리 -> 처리가 완료되면 기존 BM25 지원 테이블과 순간적으로 교체하는 고립형 모델 도입 예정 입니다.