Recent Posts
Recent Comments
반응형
«   2026/02   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
Archives
Today
Total
관리 메뉴

오늘도 공부

SQLite FTS5 전문 검색 가이드 본문

개발상식

SQLite FTS5 전문 검색 가이드

행복한 수지아빠 2026. 2. 6. 10:45
반응형

SQLite에 내장된 FTS5(Full-Text Search 5)를 사용하여 대화 내용을 빠르게 검색하는 방법을 설명합니다.


FTS5란?

FTS5는 SQLite에 내장된 전문 검색 엔진입니다. 일반적인 LIKE '%키워드%' 검색보다 훨씬 빠르고 강력한 텍스트 검색을 제공합니다.

일반 검색 vs FTS5

비교 항목LIKE 검색FTS5 검색

속도 O(n) - 전체 스캔 O(log n) - 인덱스 사용
검색 방식 단순 패턴 매칭 토큰화된 인덱스
랭킹 없음 BM25 알고리즘
고급 기능 없음 AND/OR/NOT, 근접 검색, 구문 검색
한글 지원 부분 문자열 매칭 공백 기준 토큰 매칭

동작 원리

1. 토큰화 (Tokenization)

FTS5는 텍스트를 토큰(단어) 단위로 분리하여 인덱싱합니다.

원본: "API 인증은 JWT로 하자"
토큰: ["API", "인증은", "JWT로", "하자"]

2. 역인덱스 (Inverted Index)

각 토큰이 어떤 문서(행)에 있는지 역으로 매핑합니다.

"API"   → [문서1, 문서5, 문서12]
"JWT"   → [문서1, 문서3]
"인증"  → [문서1, 문서7, 문서9]

3. BM25 랭킹

검색 결과를 관련도 순으로 정렬합니다. BM25는 TF-IDF의 개선된 버전으로:

  • TF (Term Frequency): 해당 문서에서 검색어가 얼마나 자주 나오는가
  • IDF (Inverse Document Frequency): 전체 문서에서 얼마나 희귀한 단어인가
-- BM25 점수로 정렬 (낮을수록 관련도 높음)
SELECT *, bm25(messages_fts) as rank
FROM messages_fts
WHERE messages_fts MATCH 'API'
ORDER BY rank;

구현 방법

1. FTS5 가상 테이블 생성

-- Content 테이블 연동 방식 (권장)
CREATE VIRTUAL TABLE messages_fts USING fts5(
  content,                    -- 검색할 컬럼
  content='messages',         -- 원본 테이블
  content_rowid='rowid'       -- 원본 테이블의 rowid
);

content= 옵션: 원본 테이블과 연동하여 데이터 중복을 방지합니다.

2. 동기화 트리거

원본 테이블이 변경될 때 FTS 인덱스를 자동으로 업데이트합니다.

-- INSERT 트리거
CREATE TRIGGER messages_fts_ai AFTER INSERT ON messages BEGIN
  INSERT INTO messages_fts(rowid, content)
  VALUES (new.rowid, new.content);
END;

-- DELETE 트리거
CREATE TRIGGER messages_fts_ad AFTER DELETE ON messages BEGIN
  INSERT INTO messages_fts(messages_fts, rowid, content)
  VALUES('delete', old.rowid, old.content);
END;

-- UPDATE 트리거
CREATE TRIGGER messages_fts_au AFTER UPDATE ON messages BEGIN
  INSERT INTO messages_fts(messages_fts, rowid, content)
  VALUES('delete', old.rowid, old.content);
  INSERT INTO messages_fts(rowid, content)
  VALUES (new.rowid, new.content);
END;

3. 기존 데이터 인덱싱

이미 존재하는 데이터를 FTS 테이블에 추가합니다.

INSERT INTO messages_fts(rowid, content)
SELECT rowid, content FROM messages;

검색 문법

기본 검색

-- 단일 키워드
SELECT * FROM messages_fts WHERE messages_fts MATCH 'API';

-- 여러 키워드 (AND)
SELECT * FROM messages_fts WHERE messages_fts MATCH 'API JWT';

-- OR 검색
SELECT * FROM messages_fts WHERE messages_fts MATCH 'API OR OAuth';

-- NOT 검색
SELECT * FROM messages_fts WHERE messages_fts MATCH 'API NOT OAuth';

정확한 구문을 검색합니다.

-- "API 인증" 구문 검색
SELECT * FROM messages_fts WHERE messages_fts MATCH '"API 인증"';
-- "API"로 시작하는 단어
SELECT * FROM messages_fts WHERE messages_fts MATCH 'API*';

근접 검색 (NEAR)

두 단어가 가까이 있는 문서를 찾습니다.

-- API와 JWT가 10단어 이내에 있는 문서
SELECT * FROM messages_fts WHERE messages_fts MATCH 'NEAR(API JWT, 10)';

실제 구현 코드

TypeScript (better-sqlite3)

// FTS5 검색 prepared statement
const searchMessagesFts = db.prepare(`
  SELECT
    m.id,
    m.session_id,
    m.role,
    m.content,
    m.timestamp,
    bm25(messages_fts) as rank
  FROM messages_fts
  JOIN messages m ON messages_fts.rowid = m.rowid
  WHERE messages_fts MATCH ?
  ORDER BY rank
  LIMIT ?
`);

// 검색 함수
function searchMessages(query: string, limit: number = 10) {
  // 특수문자 이스케이프
  const escapedQuery = query.replace(/"/g, '""');
  const ftsQuery = `"${escapedQuery}"`;

  return searchMessagesFts.all(ftsQuery, limit);
}

// 사용 예시
const results = searchMessages('API 설계', 5);

세션 필터링 추가

const searchWithSession = db.prepare(`
  SELECT
    m.id,
    m.session_id,
    m.role,
    m.content,
    m.timestamp,
    bm25(messages_fts) as rank
  FROM messages_fts
  JOIN messages m ON messages_fts.rowid = m.rowid
  WHERE messages_fts MATCH ?
    AND m.session_id LIKE ?
  ORDER BY rank
  LIMIT ?
`);

// 특정 채널에서만 검색
const results = searchWithSession.all(
  '"API 설계"',
  '%slack:C123456%',
  10
);

성능 비교

테스트 환경

  • 메시지 100,000개
  • 평균 메시지 길이: 200자

결과

검색 방식검색 시간비고

LIKE '%API%' ~500ms 전체 테이블 스캔
FTS5 MATCH ~5ms 인덱스 사용

100배 빠른 검색 속도를 얻을 수 있습니다.


한글 검색 주의사항

기본 토크나이저의 한계

FTS5의 기본 토크나이저(unicode61)는 공백 기준으로 토큰을 분리합니다.

"API 인증은 JWT로 하자"
→ ["API", "인증은", "JWT로", "하자"]

따라서:

  • "인증" 검색 → ❌ 매칭 안됨 ("인증은"만 있음)
  • "인증은" 검색 → ✅ 매칭됨

해결 방법

1. 구문 검색 사용 (권장)

-- 부분 문자열도 검색 가능
SELECT * FROM messages_fts WHERE messages_fts MATCH '"인증"';

2. trigram 토크나이저 사용

-- 3글자씩 분리하여 인덱싱
CREATE VIRTUAL TABLE messages_fts USING fts5(
  content,
  tokenize='trigram'
);

단점: 인덱스 크기가 커지고 검색 정확도가 떨어질 수 있음

3. 형태소 분석기 연동

한글 전용 토크나이저를 커스텀으로 만들 수 있지만, SQLite 확장 개발이 필요합니다.


우리 시스템에서의 활용

아키텍처

[Slack 사용자]
    │
    ▼ "지난번에 API 얘기한 거 뭐였지?"
[Claude]
    │
    ▼ conversation_search 도구 호출
[MCP Server]
    │
    ▼ REST API 호출
[Gateway Server]
    │
    ▼ chatDb.searchMessages()
[SQLite FTS5]
    │
    ▼ 검색 결과 반환
[Claude]
    │
    ▼ 컨텍스트로 활용하여 답변
[Slack 사용자]

MCP 도구

// conversation_search 도구
server.tool(
  'conversation_search',
  '과거 대화 내용을 검색합니다.',
  {
    query: z.string().describe('검색할 키워드'),
    session_id: z.string().optional(),
    limit: z.number().default(5),
  },
  async ({ query, session_id, limit }) => {
    const results = await apiCall('GET',
      `/api/messages/search?q=${query}&limit=${limit}`
    );
    return formatResults(results);
  }
);

REST API

GET /api/messages/search?q=API&session_id=slack:C123&limit=10

장단점 정리

장점

  1. 빠른 검색 속도: 대량의 데이터에서도 밀리초 단위 검색
  2. 랭킹 지원: BM25로 관련도 순 정렬
  3. 추가 비용 없음: SQLite 내장 기능
  4. 간단한 구현: 별도 서버 불필요
  5. 트랜잭션 지원: 원본 테이블과 동기화

단점

  1. 한글 토큰화 한계: 공백 기준 분리
  2. 의미론적 검색 불가: 동의어, 유사어 검색 어려움
  3. 실시간 업데이트 오버헤드: 트리거로 인한 쓰기 성능 저하

대안과 비교

기능FTS5Elasticsearch벡터 검색

설치 내장 별도 서버 임베딩 API 필요
비용 무료 서버 비용 API 비용
한글 제한적 형태소 분석 의미 기반
의미 검색
복잡도 낮음 높음 중간

결론

FTS5는 간단하면서도 효과적인 전문 검색 솔루션입니다.

  • 별도 인프라 없이 SQLite만으로 빠른 검색 구현
  • 대화 기록, 문서, 로그 등 텍스트 검색에 적합
  • 한글 검색은 구문 검색("키워드")으로 보완 가능

더 고급 검색(동의어, 의미 기반)이 필요하면 벡터 검색(임베딩)을 추가로 고려할 수 있습니다.

 

참고 소스 파일:

  • src/db/database.ts - FTS5 스키마 및 검색 구현
  • src/mcp/server.ts - conversation_search MCP 도구
  • src/websocket/server.ts - REST API 엔드포인트
반응형