전문 검색
전문 검색 (Full-Text Search)
Turso는 Tantivy 엔진으로 구동되는 전문 검색(FTS)을 제공해요. 커스텀 FTS 인덱스와 세 가지 SQL 함수 — 필터링용 fts_match, 관련도 순위 매기기용 fts_score, 검색어 하이라이트용 fts_highlight — 로 검색을 구현할 수 있어요.
출처: 문서
본문
**Turso 확장 기능**: Turso의 전문 검색(FTS)은 SQLite의 FTS3/FTS4/FTS5 모듈이 아니라 [Tantivy](https://github.com/quickwit-oss/tantivy)로 구동돼요. 문법과 함수도 SQLite FTS와 달라요.Turso는 커스텀 FTS 인덱스와 세 가지 SQL 함수로 전문 검색을 제공해요: 필터링용 fts_match, 관련도 순위용 fts_score, 일치 용어를 강조해서 보여주는 fts_highlight.
FTS 인덱스 만들기
FTS 인덱스는 CREATE INDEX에 USING fts 절을 붙여서 만들어요. 인덱스에 넣은 각 컬럼이 전문 검색에 참여해요.
CREATE INDEX index_name ON table_name USING fts (column1, column2, ...);
토크나이저 설정
WITH 절로 컬럼마다 다른 토크나이저를 쓸 수 있어요.
-- All columns use the default tokenizer
CREATE INDEX idx_articles ON articles USING fts (title, body);
-- Per-column tokenizer configuration
CREATE INDEX idx_articles ON articles USING fts (
title WITH tokenizer=simple,
body WITH tokenizer=ngram
);
-- Global tokenizer for all columns
CREATE INDEX idx_tags ON tags USING fts (tag) WITH (tokenizer = 'raw');
사용 가능한 토크나이저
| 토크나이저 | 설명 | 용도 |
|---|---|---|
default |
소문자 변환과 문장부호 분리를 하는 유니코드 인식 토크나이저 (40자 제한) | 범용 텍스트 검색 |
raw |
토큰화하지 않음 -- 필드 값 전체를 하나의 토큰으로 취급 | ID, UUID, 태그, 정확 일치 필드 |
simple |
소문자 변환 없이 공백과 문장부호로 분리 | 대소문자 무시가 필요 없는 단순 텍스트 |
whitespace |
공백으로만 분리 | 공백으로 구분된 토큰 |
ngram |
텍스트에서 2-3자 n-그램을 생성 | 자동완성, 부분 문자열 매칭 |
토크나이저 예제:
| 입력 | default | raw | ngram |
|---|---|---|---|
Hello World |
hello, world |
Hello World |
He, Hel, el, ell, ll, llo, ... |
user-123 |
user, 123 |
user-123 |
us, use, se, ser, ... |
필드 가중치
인덱스 컬럼에 상대 가중치를 부여해서 BM25 관련도 점수에 영향을 줄 수 있어요.
CREATE INDEX idx_articles ON articles USING fts (title, body)
WITH (weights = 'title=2.0,body=1.0');
| 매개변수 | 기본값 | 설명 |
|---|---|---|
weights |
모든 필드 1.0 |
쉼표로 구분된 column=weight 쌍. 가중치가 높을수록 점수 기여도가 커져요. |
함수들
fts_match
행이 전문 검색 쿼리와 일치하면 1, 아니면 0을 반환해요. WHERE 절에서 행을 걸러낼 때 써요.
fts_match(column1, column2, ..., query)
| 매개변수 | 타입 | 설명 |
|---|---|---|
column1, column2, ... |
TEXT | FTS 인덱스가 뒤받쳐주는 컬럼 하나 이상 |
query |
TEXT | 검색 쿼리 문자열 (아래 쿼리 문법 참고) |
반환: INTEGER -- 일치하면 1, 아니면 0.
fts_match에 넘기는 컬럼은 반드시 기존 FTS 인덱스의 컬럼과 대응돼야 해요. Turso의 쿼리 플래너가 WHERE 절에서 fts_match를 감지하면, 효율적인 조회를 위해 쿼리를 FTS 인덱스 쪽으로 보내요.
SELECT id, title FROM articles
WHERE fts_match(title, body, 'database');
-- Single column
SELECT id, title FROM articles
WHERE fts_match(body, 'machine learning');
fts_score
일치하는 각 행에 대해 BM25 관련도 점수를 계산해요. 점수가 낮을수록 관련도가 높아요.
fts_score(column1, column2, ..., query)
| 매개변수 | 타입 | 설명 |
|---|---|---|
column1, column2, ... |
TEXT | FTS 인덱스가 뒤받쳐주는 컬럼 하나 이상 |
query |
TEXT | 검색 쿼리 문자열 |
반환: REAL -- BM25 관련도 점수. 값이 낮을수록 관련도가 높아요.
결과 순서 취향에 따라 ORDER BY score ASC 또는 ORDER BY score DESC를 쓰면 돼요. SELECT에서 사용하면 FTS 인덱스가 자동으로 점수 매긴 결과를 제공해요.
SELECT
id,
title,
fts_score(title, body, 'database') AS score
FROM articles
ORDER BY score DESC
LIMIT 10;
fts_highlight
일치하는 쿼리 용어를 지정한 태그로 감싼 텍스트를 반환해요. 검색 결과에서 일치 용어를 시각적으로 강조해 보여줄 때 유용해요.
fts_highlight(column1, column2, ..., open_tag, close_tag, query)
| 매개변수 | 타입 | 설명 |
|---|---|---|
column1, column2, ... |
TEXT | 하이라이트할 텍스트 컬럼 하나 이상 |
open_tag |
TEXT | 각 일치 용어 앞에 넣을 태그 (예: '<b>') |
close_tag |
TEXT | 각 일치 용어 뒤에 넣을 태그 (예: '</b>') |
query |
TEXT | 검색 쿼리 문자열 |
반환: TEXT -- 일치 용어가 지정한 태그로 감싸진 입력 텍스트. 일치가 없으면 원본 텍스트를 반환해요. query, open_tag, close_tag 중 하나라도 NULL이면 NULL을 반환해요.
여러 컬럼을 넘기면 그 텍스트들을 공백으로 이어 붙여요.
SELECT fts_highlight(title, '<b>', '</b>', 'database') AS highlighted
FROM articles
WHERE fts_match(title, body, 'database');
-- <b>Database</b> Design Patterns
-- Multiple columns are concatenated
SELECT fts_highlight(title, body, '<mark>', '</mark>', 'database') AS highlighted
FROM articles
WHERE fts_match(title, body, 'database');
-- Standalone usage (without an FTS index)
SELECT fts_highlight('The quick brown fox', '<em>', '</em>', 'quick fox');
-- The <em>quick</em> brown <em>fox</em>
쿼리 문법
fts_match와 fts_score에 넘기는 쿼리 문자열은 Tantivy의 쿼리 파서 문법을 지원해요.
기본 쿼리
| 문법 | 예제 | 설명 |
|---|---|---|
| 단일 용어 | 'database' |
"database"가 들어간 행 매칭 |
| 여러 용어 (OR) | 'database search' |
"database" 또는 "search"가 들어간 행 매칭 |
| 불리언 AND | 'database AND search' |
두 용어가 모두 들어간 행 매칭 |
| 불리언 NOT | 'database NOT nosql' |
"database"는 매칭하되 "nosql"은 제외 |
고급 쿼리
| 문법 | 예제 | 설명 |
|---|---|---|
| 구(phrase) 검색 | '"exact phrase"' |
정확한 구를 매칭 |
| 접두사 검색 | 'data*' |
"data"로 시작하는 용어 매칭 |
| 컬럼 지정 | 'title:database' |
title 필드에서만 "database" 매칭 |
| 부스팅 | 'title:database^2' |
title에서의 매칭에 2배 가중치 부여 |
예제
-- Simple term search
SELECT * FROM articles WHERE fts_match(title, body, 'database');
-- Phrase search: match "full text search" as an exact phrase
SELECT * FROM articles WHERE fts_match(title, body, '"full text search"');
-- Boolean AND: both terms must be present
SELECT * FROM articles WHERE fts_match(title, body, 'database AND performance');
-- Prefix search: match "optim", "optimize", "optimization", etc.
SELECT * FROM articles WHERE fts_match(title, body, 'optim*');
-- Column-specific: only match "rust" in the title field
SELECT * FROM articles WHERE fts_match(title, body, 'title:rust');
-- Boosted search: title matches count 2x more toward the score
SELECT id, title, fts_score(title, body, 'title:database^2 body:database') AS score
FROM articles
ORDER BY score DESC;
-- Exclusion: match "database" but not "nosql"
SELECT * FROM articles WHERE fts_match(title, body, 'database NOT nosql');
전체 예제
이 예제는 테이블 만들기, FTS 인덱스 추가, 데이터 삽입, 점수 매기기와 하이라이트를 곁들인 쿼리 실행까지 차례로 살펴봐요.
-- Create a table
CREATE TABLE articles (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
body TEXT,
author TEXT
);
-- Create an FTS index on title and body with field weights
CREATE INDEX idx_articles_fts ON articles USING fts (title, body)
WITH (weights = 'title=2.0,body=1.0');
-- Insert sample data
INSERT INTO articles VALUES (1, 'Introduction to Databases', 'Databases store and organize data for efficient retrieval.', 'Alice');
INSERT INTO articles VALUES (2, 'Full-Text Search in Practice', 'Full-text search allows finding documents by content.', 'Bob');
INSERT INTO articles VALUES (3, 'Database Performance Tuning', 'Optimizing database queries requires understanding indexes.', 'Alice');
INSERT INTO articles VALUES (4, 'Getting Started with Rust', 'Rust is a systems programming language focused on safety.', 'Carol');
-- Simple search: find articles mentioning "database"
SELECT id, title FROM articles
WHERE fts_match(title, body, 'database');
-- 1 | Introduction to Databases
-- 3 | Database Performance Tuning
-- Ranked search: order by relevance
SELECT
id,
title,
fts_score(title, body, 'database') AS score
FROM articles
WHERE fts_match(title, body, 'database')
ORDER BY score DESC
LIMIT 10;
-- Highlighted results
SELECT
id,
fts_highlight(title, '<b>', '</b>', 'database') AS title,
fts_highlight(body, '<b>', '</b>', 'database') AS body
FROM articles
WHERE fts_match(title, body, 'database');
-- Combine FTS with regular SQL filters
SELECT id, title, author,
fts_score(title, body, 'database') AS score
FROM articles
WHERE fts_match(title, body, 'database')
AND author = 'Alice'
ORDER BY score DESC;
인덱스 유지 관리
OPTIMIZE INDEX
모든 Tantivy 세그먼트를 하나의 최적화된 세그먼트로 병합해요. 쿼리 성능을 개선하고 저장 오버헤드를 줄여줘요. 특히 대량 삽입 이후에 효과가 커요.
-- Optimize a specific FTS index
OPTIMIZE INDEX idx_articles_fts;
-- Optimize all FTS indexes in the database
OPTIMIZE INDEX;
언제 쓰면 좋아요:
- INSERT 문을 많이 쓰는 대량 데이터 가져오기 이후
- 시간이 지나면서 쿼리 성능이 떨어졌을 때
- 예정된 유지 보수 창구 작업 중에
하는 일:
- 대기 중인 문서를 디스크로 플러시
- 모든 세그먼트를 하나의 세그먼트로 병합
- 삭제된 문서 톰스톤 제거
- 새로 읽을 수 있도록 내부 캐시 무효화
DML과 인덱스 갱신
기반 테이블을 수정하면 FTS 인덱스가 자동으로 갱신돼요.
| 연산 | FTS 동작 |
|---|---|
INSERT |
새 행은 즉시 인덱싱돼요 (1000개 문서마다 배치 커밋) |
UPDATE |
내부적으로 DELETE + INSERT로 구현돼요 |
DELETE |
톰스톤으로 문서를 삭제 표시하고, OPTIMIZE 때 정리돼요 |
-- All of these automatically update the FTS index
INSERT INTO articles VALUES (5, 'New Article', 'Content here.', 'Dave');
UPDATE articles SET body = 'Updated content.' WHERE id = 5;
DELETE FROM articles WHERE id = 5;
SQLite FTS5와 비교
| 기능 | SQLite FTS5 | Turso FTS |
|---|---|---|
| Filtering | WHERE t MATCH 'query' |
WHERE fts_match(cols, 'query') |
| Ranking | bm25(t), rank column |
fts_score(cols, 'query') |
| Highlighting | highlight(t, col_idx, open, close) |
fts_highlight(cols, open, close, query) |
| Snippets | snippet(t, ...) |
Not supported |
| Boolean operators | AND, OR, NOT | AND, OR, NOT |
| Phrase search | "exact phrase" |
"exact phrase" |
| Prefix search | word* |
word* |
| Column filter | col:term |
col:term |
| Tokenizers | unicode61, ascii, porter | default, raw, simple, whitespace, ngram |
| Segment management | Automatic | Manual via OPTIMIZE INDEX |
| Transaction visibility | Immediate | After COMMIT |
현재 제한 사항
- snippet 함수 없음: 용어 강조는
fts_highlight를 쓰세요. 컨텍스트 스니펫은 아직 제공되지 않아요. - 자동 세그먼트 병합 없음: 대량 쓰기 후에는 주기적으로
OPTIMIZE INDEX를 실행하세요. - 트랜잭션 안 read-your-writes 없음: 트랜잭션 안에서의 FTS 변경은 트랜잭션이 커밋되기 전까지는 쿼리에 보이지 않아요. ROLLBACK은 테이블 변경과 FTS 변경을 모두 올바르게 버려요.
- MATCH 연산자 문법 없음:
WHERE table MATCH 'query'대신fts_match()함수 호출을 쓰세요.
더 알아보기 (Learn more)
- CREATE INDEX -
CREATE INDEX ... USING fts전체 문법 - 벡터 함수 - 임베딩으로 유사도 검색하기