FTS5

FTS5 (전문 검색 확장)

FTS5 는 데이터베이스 애플리케이션에 전체 텍스트 검색 기능을 제공하는 SQLite 가상 테이블 모듈이에요. 기본 형태로 전체 텍스트 검색 엔진은 사용자가 대규모 문서 모음에서 검색 용어의 인스턴스를 하나 이상 포함하는 부분집합을 효율적으로 검색할 수 있게 해줘요.

출처: SQLite FTS5 Extension

본문

1. FTS5 개요

FTS5 는 데이터베이스 애플리케이션에 전체 텍스트 검색 기능을 제공하는 SQLite 가상 테이블 모듈이에요. 가장 기본적인 형태에서 전체 텍스트 검색 엔진은 사용자가 검색 용어의 인스턴스를 하나 이상 포함하는 대규모 문서 모음의 부분집합을 효율적으로 검색할 수 있게 해줘요. 월드 와이드 웹 사용자에게 Google 이 제공하는 검색 기능은, 무엇보다도 전체 텍스트 검색 엔진이에요. 예를 들어 "fts5" 용어를 포함하는 웹의 모든 문서를 검색할 수 있게 해주기 때문이에요.

FTS5 를 사용하려면 사용자가 하나 이상의 컬럼을 가진 FTS5 가상 테이블을 만들어요. 예를 들어:

CREATE VIRTUAL TABLE email USING fts5(sender, title, body);

FTS5 테이블을 만드는 데 사용되는 CREATE VIRTUAL TABLE 문에 타입, 제약 조건, PRIMARY KEY 선언을 추가하는 것은 에러예요. 일단 만들어지면 FTS5 테이블은 다른 테이블처럼 INSERT, UPDATE, DELETE 문으로 채워질 수 있어요. PRIMARY KEY 선언이 없는 다른 테이블처럼 FTS5 테이블에는 rowid 라는 암시적 INTEGER PRIMARY KEY 필드가 있어요.

위 예에 나오지 않지만, CREATE VIRTUAL TABLE 문의 일부로 FTS5 에 제공되어 새 테이블의 다양한 측면을 구성할 수 있는 옵션들도 있어요. 이것들은 FTS5 테이블이 문서와 쿼리에서 용어를 추출하는 방식을 수정하거나, 접두어 쿼리를 빠르게 하기 위해 디스크에 추가 인덱스를 만들거나, 다른 곳에 저장된 내용에 대한 인덱스 역할을 하는 FTS5 테이블을 만드는 데 사용될 수 있어요.

채워지면 FTS5 테이블의 내용에 대해 전체 텍스트 쿼리를 실행하는 방법은 세 가지가 있어요:

  • SELECT 문의 WHERE 절에서 MATCH 연산자를 사용하거나,
  • SELECT 문의 WHERE 절에서 등호("=") 연산자를 사용하거나,
  • 테이블-값 함수 구문을 사용한다.

MATCH 나 = 연산자를 사용하면 MATCH 연산자의 왼쪽 표현식은 보통 FTS5 테이블의 이름이에요(예외는 컬럼-필터를 지정할 때예요). 오른쪽 표현식은 검색할 용어를 지정하는 텍스트 값이어야 해요. 테이블-값 함수 구문의 경우 검색할 용어는 첫 번째 테이블 인자로 지정돼요. 예를 들어:

-- Query for all rows that contain at least once instance of the term
-- "fts5" (in any column). The following three queries are equivalent.
SELECT * FROM email WHERE email MATCH 'fts5';
SELECT * FROM email WHERE email = 'fts5';
SELECT * FROM email('fts5');

기본적으로 FTS5 전체 텍스트 검색은 대소문자를 구분하지 않아요. ORDER BY 절을 포함하지 않는 다른 SQL 쿼리처럼 위 예는 임의 순서로 결과를 반환해요. 관련성(가장 관련 있는 것부터 가장 덜 관련 있는 것까지)으로 결과를 정렬하려면 전체 텍스트 쿼리에 ORDER BY 를 다음과 같이 추가할 수 있어요:

-- Query for all rows that contain at least once instance of the term
-- "fts5" (in any column). Return results in order from best to worst
-- match.
SELECT * FROM email WHERE email MATCH 'fts5' ORDER BY rank;

일치하는 행의 컬럼 값과 rowid 뿐만 아니라, 애플리케이션은 FTS5 보조 함수를 사용해 일치하는 행에 대한 추가 정보를 검색할 수 있어요. 예를 들어 보조 함수는 일치하는 행의 컬럼 값 복사본에서 일치하는 용어의 모든 인스턴스가 html 태그로 둘러싸인 것을 검색하는 데 사용될 수 있어요. 보조 함수는 SQLite 스칼라 함수와 같은 방식으로 호출되지만, FTS5 테이블의 이름이 첫 번째 인자로 지정된다는 점이 달라요. 예를 들어:

-- Query for rows that match "fts5". Return a copy of the "body" column
-- of each row with the matches surrounded by <b></b> tags.
SELECT highlight(email, 2, '<b>', '</b>') FROM email('fts5');

사용 가능한 보조 함수에 대한 설명과 특별한 "rank" 컬럼 구성에 대한 자세한 내용은 아래에 있어요. 사용자 정의 보조 함수도 SQLite 코어에 사용자 정의 SQL 함수를 등록할 수 있는 것처럼 C 로 구현해 FTS5 에 등록할 수 있어요.

용어를 포함하는 모든 행을 검색하는 것뿐만 아니라 FTS5 는 사용자가 다음을 포함하는 행을 검색할 수 있게 해줘요:

  • 지정된 접두어로 시작하는 용어,
  • "구문(phrases)" - 문서가 쿼리와 일치하기 위해 포함해야 하는 용어 또는 접두어 용어의 시퀀스,
  • 서로 지정된 근접도 안에 나타나는 용어, 접두어 용어 또는 구문의 집합("NEAR 쿼리"라고 함), 또는
  • 위 중 어떤 것의 부울 조합.

이러한 고급 검색은 MATCH 연산자(또는 = 연산자, 또는 테이블-값 함수 구문의 첫 인자) 오른쪽의 텍스트로 더 복잡한 FTS5 쿼리 문자열을 제공함으로써 요청돼요. 전체 쿼리 구문은 여기에 설명돼 있어요.

2. FTS5 컴파일 및 사용

2.1. SQLite 의 일부로 FTS5 빌드

버전 3.9.0 (2015-10-14) 부터 FTS5 는 SQLite amalgamation 의 일부로 포함돼요. 정식 소스 트리를 사용한다면 configure 스크립트를 실행할 때 "--enable-fts5" 옵션을 지정해 FTS5 를 활성화해요. (FTS5 는 현재 소스-트리 configure 스크립트에서는 기본적으로 비활성화되고 amalgamation configure 스크립트에서는 기본적으로 활성화되지만, 이러한 기본값은 미래에 바뀔 수 있어요.)

또는 sqlite3.c 가 다른 빌드 시스템으로 컴파일된다면 SQLITE_ENABLE_FTS5 전처리기 기호가 정의되도록 준비해요.

2.2. 로드 가능한 확장 빌드

대안으로 FTS5 는 로드 가능한 확장으로 빌드될 수 있어요.

정식 FTS5 소스 코드는 SQLite 소스 트리의 "ext/fts5" 디렉토리의 일련의 *.c 및 기타 파일로 구성돼요. 빌드 과정이 이것을 "fts5.c" 와 "fts5.h" 라는 두 파일로 줄여주는데, 이 파일들로 SQLite 로드 가능한 확장을 빌드할 수 있어요.

  • fossil 에서 최신 SQLite 코드를 얻어요.
  • How To Compile SQLite 에 설명된 대로 Makefile 을 만들어요.
  • fts5.h 도 만드는 "fts5.c" 대상을 빌드해요.
$ wget -c https://sqlite.org/src/tarball/SQLite-trunk.tgz?uuid=trunk -O SQLite-trunk.tgz
.... output ....
$ tar -xzf SQLite-trunk.tgz
$ cd SQLite-trunk
$ ./configure && make fts5.c
... lots of output ...
$ ls fts5.[ch]
fts5.c        fts5.h

"fts5.c" 의 코드는 Compiling Loadable Extensions 에 설명된 대로 로드 가능한 확장으로 컴파일되거나 애플리케이션에 정적으로 링크될 수 있어요. 같은 일을 하는 두 개의 진입점이 정의돼 있어요:

  • sqlite3_fts_init
  • sqlite3_fts5_init

다른 파일 "fts5.h" 는 FTS5 확장을 컴파일하는 데 필요하지 않아요. 사용자 정의 FTS5 토크나이저나 보조 함수를 구현하는 애플리케이션이 이것을 사용해요.

3. 전체 텍스트 쿼리 구문

다음 블록은 BNF 형태의 FTS 쿼리 구문 요약을 포함해요. 자세한 설명이 이어집니다.

<phrase>    := string [*]
<phrase>    := <phrase> + <phrase>
<neargroup> := NEAR ( <phrase> <phrase> ... [, N] )
<query>     := [ [-] <colspec> :] [^] <phrase>
<query>     := [ [-] <colspec> :] <neargroup>
<query>     := [ [-] <colspec> :] ( <query> )
<query>     := <query> AND <query>
<query>     := <query> OR <query>
<query>     := <query> NOT <query>
<colspec>   := colname
<colspec>   := { colname1 colname2 ... }

3.1. FTS5 문자열

FTS 표현식 안에서 string 은 두 가지 방법 중 하나로 지정될 수 있어요:

  • 큰따옴표(")로 감싸서. 문자열 안에 포함된 큰따옴표 문자는 SQL 스타일로 이스케이프될 수 있어요. 두 번째 큰따옴표 문자를 추가하는 방식이에요.

  • "AND", "OR", "NOT" 이 아닌(대소문자 구분) FTS5 bareword 로. FTS5 bareword 는 다음 중 모두인 하나 이상의 연속 문자로 구성된 문자열이에요:

    • 비-ASCII 범위 문자(즉, 127 보다 큰 유니코드 코드포인트), 또는
    • 52 개의 대소문자 ASCII 문자 중 하나, 또는
    • 10 개의 십진 숫자 ASCII 문자 중 하나, 또는
    • 밑줄 문자(유니코드 코드포인트 95), 또는
    • 대체 문자(substitute character, 유니코드 코드포인트 26).

    다른 문자를 포함하는 문자열은 따옴표로 묶어야 해요. 현재 bareword 에 허용되지 않고, 따옴표 문자도 아니며, 현재 FTS5 쿼리 표현식에서 특별한 목적으로 쓰이지 않는 문자는 미래에 bareword 에 허용되거나 새 쿼리 기능을 구현하는 데 사용될 수 있어요. 이는 현재 따옴표로 묶인 문자열 밖에서 그러한 문자를 포함해 구문 에러인 쿼리가 어떤 미래 FTS5 버전에서는 다르게 해석될 수 있다는 뜻이에요.

3.2. FTS5 구문 (Phrases)

fts5 쿼리의 각 문자열은 토크나이저에 의해 파싱("토큰화")되고 0 개 이상의 토큰 또는 용어 목록이 추출돼요. 예를 들어 기본 토크나이저는 "alpha beta gamma" 문자열을 그 순서대로 "alpha", "beta", "gamma" 세 개의 별도 토큰으로 토큰화해요.

FTS 쿼리는 구문(phrases) 으로 구성돼요. 구문은 하나 이상의 토큰의 정렬된 목록이에요. 쿼리의 각 문자열의 토큰이 각각 단일 구문을 만들어요. 두 구문은 "+" 연산자를 사용해 단일 큰 구문으로 결합될 수 있어요. 예를 들어 사용 중인 토크나이저 모듈이 입력 "one.two.three" 를 세 개의 별도 토큰으로 토큰화한다고 가정하면, 다음 네 쿼리는 모두 같은 구문을 지정해요:

... MATCH '"one two three"'
... MATCH 'one + two + three'
... MATCH '"one two" + three'
... MATCH 'one.two.three'

구문이 그 구문을 구성하는 토큰 시퀀스와 일치하는 최소 하나의 토큰 하위 시퀀스를 문서가 포함하면 구문이 문서와 일치해요.

3.3. FTS5 접두어 쿼리

FTS 표현식 안에서 문자열 뒤에 "*" 문자가 따르면 그 문자열에서 추출된 마지막 토큰이 접두어 토큰(prefix token) 으로 표시돼요. 예상대로 접두어 토큰은 그것이 접두어인 어떤 문서 토큰과도 일치해요. 예를 들어 다음 블록의 처음 두 쿼리는 토큰 "one" 바로 뒤에 토큰 "two" 가 오고 그 다음에 "thr" 로 시작하는 어떤 토큰이 오는 문서와 일치할 거예요.

... MATCH '"one two thr" * '
... MATCH 'one + two + thr*'
... MATCH '"one two thr*"'      -- May not work as expected!

블록의 마지막 쿼리는 예상대로 작동하지 않을 수 있어요. "*" 문자가 큰따옴표 안에 있으므로 토크나이저에 전달될 것이고, 토크나이저는 그것을 특별한 FTS 문자로 인식하는 대신 (아마도) 버릴 것이기 때문이에요(또는 사용 중인 특정 토크나이저에 따라 마지막 토큰의 일부로 포함할 수도 있어요).

3.4. FTS5 초기 토큰 쿼리

NEAR 쿼리의 일부가 아닌 구문 바로 앞에 "^" 문자가 나타나면, 그 구문은 컬럼의 첫 토큰에서 시작할 때만 문서와 일치해요. "^" 구문은 컬럼 필터와 결합될 수 있지만 구문의 중간에 삽입될 수는 없어요.

... MATCH '^one'              -- first token in any column must be "one"
... MATCH '^ one + two'       -- phrase "one two" must appear at start of a column
... MATCH '^ "one two"'       -- same as previous
... MATCH 'a : ^two'          -- first token of column "a" must be "two"
... MATCH 'NEAR(^one, two)'   -- syntax error!
... MATCH 'one + ^two'        -- syntax error!
... MATCH '"^one two"'        -- May not work as expected!

3.5. FTS5 NEAR 쿼리

두 개 이상의 구문이 NEAR 그룹으로 그룹화될 수 있어요. NEAR 그룹은 토큰 "NEAR"(대소문자 구분) 뒤에 여는 괄호 문자, 뒤에 공백으로 구분된 두 개 이상의 구문, 선택적으로 쉼표와 숫자 매개변수 N, 뒤에 닫는 괄호로 지정돼요. 예를 들어:

... MATCH 'NEAR("one two" "three four", 10)'
... MATCH 'NEAR("one two" thr* + four)'

N 매개변수를 제공하지 않으면 기본값은 10 이에요. NEAR 그룹은 문서가 다음을 만족하는 최소 하나의 토큰 덩어리를 포함하면 그 문서와 일치해요:

  • 각 구문의 최소 하나의 인스턴스를 포함하고,
  • 덩어리에서 첫 구문의 끝과 마지막 구문의 시작 사이의 토큰 수가 N 보다 작거나 같다.

예를 들어:

CREATE VIRTUAL TABLE ft USING fts5(x);
INSERT INTO ft(rowid, x) VALUES(1, 'A B C D x x x E F x');

... MATCH 'NEAR(e d, 4)';                      -- Matches!
... MATCH 'NEAR(e d, 3)';                      -- Matches!
... MATCH 'NEAR(e d, 2)';                      -- Does not match!

... MATCH 'NEAR("c d" "e f", 3)';              -- Matches!
... MATCH 'NEAR("c"   "e f", 3)';              -- Does not match!

... MATCH 'NEAR(a d e, 6)';                    -- Matches!
... MATCH 'NEAR(a d e, 5)';                    -- Does not match!

... MATCH 'NEAR("a b c d" "b c" "e f", 4)';    -- Matches!
... MATCH 'NEAR("a b c d" "b c" "e f", 3)';    -- Does not match!

3.6. FTS5 컬럼 필터

단일 구문이나 NEAR 그룹은 앞에 컬럼 이름 뒤에 콜론 문자를 붙여 FTS 테이블의 지정된 컬럼 안의 텍스트에 대한 일치로 제한될 수 있어요. 또는 앞에 중괄호("curly brackets")로 감싼 공백으로 구분된 컬럼 이름 목록 뒤에 콜론 문자를 붙여 컬럼 집합으로 제한될 수 있어요. 컬럼 이름은 위에서 문자열에 대해 설명한 두 형태 중 하나로 지정될 수 있어요. 구문의 일부인 문자열과 달리 컬럼 이름은 토크나이저 모듈에 전달되지 않아요. 컬럼 이름은 SQLite 컬럼 이름의 일반적인 방식으로 대소문자를 구분하지 않아요. 대소문자 동등성은 ASCII 범위 문자에 대해서만 이해돼요.

... MATCH 'colname : NEAR("one two" "three four", 10)'
... MATCH '"colname" : one + two + three'

... MATCH '{col1 col2} : NEAR("one two" "three four", 10)'
... MATCH '{col2 col1 col3} : one + two + three'

컬럼 필터 지정 앞에 "-" 문자가 있으면 일치하지 않아야 할 컬럼 목록으로 해석돼요. 예를 들어:

-- Search for matches in all columns except "colname"
... MATCH '- colname : NEAR("one two" "three four", 10)'

-- Search for matches in all columns except "col1", "col2" and "col3"
... MATCH '- {col2 col1 col3} : one + two + three'

컬럼 필터 지정은 괄호로 감싼 임의 표현식에도 적용될 수 있어요. 이 경우 컬럼 필터는 표현식 안의 모든 구문에 적용돼요. 중첩된 컬럼 필터 연산은 일치하는 컬럼 부분집합을 더 제한할 수만 있고, 필터링된 컬럼을 다시 활성화하는 데 사용될 수 없어요. 예를 들어:

-- The following are equivalent:
... MATCH '{a b} : ( {b c} : "hello" AND "world" )'
... MATCH '(b : "hello") AND ({a b} : "world")'

마지막으로 단일 컬럼에 대한 컬럼 필터는 (보통의 테이블 이름 대신) MATCH 연산자의 LHS 로 컬럼 이름을 사용해 지정할 수 있어요. 예를 들어:

-- Given the following table
CREATE VIRTUAL TABLE ft USING fts5(a, b, c);

-- The following are equivalent
SELECT * FROM ft WHERE b MATCH 'uvw AND xyz';
SELECT * FROM ft WHERE ft MATCH 'b : (uvw AND xyz)';

-- This query cannot match any rows (since all columns are filtered out):
SELECT * FROM ft WHERE b MATCH 'a : xyz';

3.7. FTS5 부울 연산자

구문과 NEAR 그룹은 부울 연산자를 사용해 표현식으로 배열될 수 있어요. 우선순위 순서로, 가장 높은 것(가장 촘촘한 그룹화)부터 가장 낮은 것(가장 느슨한 그룹화)까지 연산자는:

Operator Function
NOT Matches if query1 matches and query2 does not match.
AND Matches if both query1 and query2 match.
OR Matches if either query1 or query2 match.

괄호는 보통의 방식으로 연산자 우선순위를 수정하기 위해 표현식을 그룹화하는 데 사용될 수 있어요. 예를 들어:

-- Because NOT groups more tightly than OR, either of the following may
-- be used to match all documents that contain the token "two" but not
-- "three", or contain the token "one".
... MATCH 'one OR two NOT three'
... MATCH 'one OR (two NOT three)'

-- Matches documents that contain at least one instance of either "one"
-- or "two", but do not contain any instances of token "three".
... MATCH '(one OR two) NOT three'

구문과 NEAR 그룹은 암시적 AND 연산자로도 연결될 수 있어요. 단순화를 위해 이 연산자는 위 BNF 문법에 표시되지 않았어요. 기본적으로, 단지 공백으로만 분리된 구문 또는 NEAR 그룹의 어떤 시퀀스든(지정된 컬럼으로의 일치 제한 포함) 각 구문 또는 NEAR 그룹 쌍 사이에 암시적 AND 연산자가 있는 것처럼 처리돼요. 암시적 AND 연산자는 괄호로 감싼 표현식의 뒤나 앞에 절대 삽입되지 않아요. 암시적 AND 연산자는 NOT 을 포함한 모든 다른 연산자보다 더 촘촘하게 그룹화돼요. 예를 들어:

... MATCH 'one two three'         -- 'one AND two AND three'
... MATCH 'three "one two"'       -- 'three AND "one two"'
... MATCH 'NEAR(one two) three'   -- 'NEAR(one two) AND three'
... MATCH 'one OR two three'      -- 'one OR two AND three'
... MATCH 'one NOT two three'     -- 'one NOT (two AND three)'

... MATCH '(one OR two) three'    -- Syntax error!
... MATCH 'func(one two)'         -- Syntax error!

4. FTS5 테이블 생성 및 초기화

"CREATE VIRTUAL TABLE ... USING fts5 ..." 문의 일부로 지정된 각 인자는 컬럼 선언 또는 구성 옵션이에요. 컬럼 선언은 하나 이상의 공백으로 구분된 FTS5 bareword 또는 SQLite 가 허용하는 어떤 방식으로든 따옴표로 묶인 문자열 리터럴로 구성돼요.

컬럼 선언의 첫 번째 문자열 또는 bareword 는 컬럼 이름이에요. fts5 테이블 컬럼을 "rowid" 나 "rank" 로 이름 짓거나, 테이블 자체가 사용하는 것과 같은 이름을 컬럼에 할당하려는 시도는 에러예요. 이것은 지원되지 않아요.

컬럼 선언의 각 후속 문자열 또는 bareword 는 그 컬럼의 동작을 수정하는 컬럼 옵션이에요. 컬럼 옵션은 대소문자를 구분하지 않아요. SQLite 코어와 달리 FTS5 는 인식되지 않는 컬럼 옵션을 에러로 간주해요. 현재 인식되는 유일한 옵션은 "UNINDEXED"(아래 참고)예요.

구성 옵션은 FTS5 bareword - 옵션 이름 - 뒤에 "=" 문자, 그리고 옵션 값으로 구성돼요. 옵션 값은 단일 FTS5 bareword 또는 (다시 SQLite 코어가 허용하는 어떤 방식으로든 따옴표로 묶인) 문자열 리터럴을 사용해 지정돼요. 예를 들어:

CREATE VIRTUAL TABLE mail USING fts5(sender, title, body, tokenize = 'porter ascii');

현재 다음 구성 옵션이 있어요:

  • "tokenize" 옵션, 사용자 정의 토크나이저를 구성하는 데 사용됨.
  • "prefix" 옵션, FTS5 테이블에 접두어 인덱스를 추가하는 데 사용됨.
  • "content" 옵션, FTS5 테이블을 외부 내용 또는 contentless 테이블로 만들기 위해 사용됨.
  • "content_rowid" 옵션, 외부 내용 테이블의 rowid 필드를 설정하는 데 사용됨.
  • "columnsize" 옵션, FTS5 테이블의 각 값의 토큰 크기가 데이터베이스 안에 별도로 저장되는지 여부를 구성하는 데 사용됨.
  • "detail" 옵션. 이 옵션은 일부 정보를 생략해 디스크의 FTS 인덱스 크기를 줄이는 데 사용될 수 있음.

4.1. UNINDEXED 컬럼 옵션

UNINDEXED 컬럼 옵션이 붙은 컬럼의 내용은 FTS 인덱스에 추가되지 않아요. 이는 MATCH 쿼리와 FTS5 보조 함수의 목적상, 그 컬럼에는 일치 가능한 토큰이 없다는 뜻이에요.

예를 들어 "uuid" 필드의 내용을 FTS 인덱스에 추가하지 않으려면:

CREATE VIRTUAL TABLE customers USING fts5(name, addr, uuid UNINDEXED);

4.2. 접두어 인덱스

기본적으로 FTS5 는 문서 집합 안의 각 토큰 인스턴스의 위치를 기록하는 단일 인덱스를 유지해요. 이는 완전한 토큰에 대한 쿼리가 단일 조회를 필요로 하므로 빠르지만, 접두어 토큰에 대한 쿼리는 범위 스캔을 필요로 하므로 느릴 수 있어요. 예를 들어 접두어 토큰 "abc*" 에 대한 쿼리는 "abc" 보다 크거나 같고 "abd" 보다 작은 모든 토큰의 범위 스캔을 필요로 해요.

접두어 인덱스는 접두어 토큰에 대한 쿼리를 빠르게 하는 데 사용되는, 문자 단위의 특정 길이의 접두어 토큰의 모든 인스턴스의 위치를 기록하는 별도 인덱스예요. 예를 들어 접두어 토큰 "abc*" 에 대한 쿼리를 최적화하려면 세 문자 접두어의 접두어 인덱스가 필요해요.

FTS5 테이블에 접두어 인덱스를 추가하려면 "prefix" 옵션을 단일 양의 정수 또는 하나 이상의 양의 정수 값의 공백으로 구분된 목록을 포함하는 텍스트 값으로 설정해요. 지정된 각 정수에 대해 접두어 인덱스가 만들어져요. 단일 CREATE VIRTUAL TABLE 문의 일부로 둘 이상의 "prefix" 옵션이 지정되면 모두 적용돼요.

-- Two ways to create an FTS5 table that maintains prefix indexes for
-- two and three character prefix tokens.
CREATE VIRTUAL TABLE ft USING fts5(a, b, prefix='2 3');
CREATE VIRTUAL TABLE ft USING fts5(a, b, prefix=2, prefix=3);

4.3. 토크나이저

CREATE VIRTUAL TABLE "tokenize" 옵션은 FTS5 테이블이 사용하는 특정 토크나이저를 구성하는 데 사용돼요. 옵션 인자는 FTS5 bareword 이거나 SQL 텍스트 리터럴이어야 해요. 인자의 텍스트 자체는 하나 이상의 FTS5 bareword 또는 SQL 텍스트 리터럴의 공백 시리즈로 처리돼요. 이 중 첫 번째는 사용할 토크나이저의 이름이에요. 두 번째 및 이후 목록 요소는 존재한다면 토크나이저 구현에 전달되는 인자예요.

옵션 값과 컬럼 이름과 달리 토크나이저로 의도된 SQL 텍스트 리터럴은 작은따옴표 문자로 따옴표를 묶어야 해요. 예를 들어:

-- The following are all equivalent
CREATE VIRTUAL TABLE ft USING fts5(x, tokenize = 'porter ascii');
CREATE VIRTUAL TABLE ft USING fts5(x, tokenize = "porter ascii");
CREATE VIRTUAL TABLE ft USING fts5(x, tokenize = "'porter' 'ascii'");
CREATE VIRTUAL TABLE ft USING fts5(x, tokenize = '''porter'' ''ascii''');

-- But this will fail:
CREATE VIRTUAL TABLE ft USING fts5(x, tokenize = '"porter" "ascii"');

-- This will fail too:
CREATE VIRTUAL TABLE ft USING fts5(x, tokenize = 'porter' 'ascii');

FTS5 는 네 개의 내장 토크나이저 모듈을 제공하며, 후속 섹션에서 설명해요:

  • unicode61 토크나이저, Unicode 6.1 표준에 기반. 기본값이에요.
  • ascii 토크나이저, ASCII 코드포인트 범위(0-127) 밖의 모든 문자를 토큰 문자로 취급한다고 가정.
  • porter 토크나이저, porter stemming 알고리즘을 구현.
  • trigram 토크나이저, 각 연속 세 문자 시퀀스를 토큰으로 취급해 FTS5 가 더 일반적인 부분 문자열 일치를 지원하게 함.

FTS5 용 사용자 정의 토크나이저를 만드는 것도 가능해요. 그렇게 하기 위한 API 는 여기에 설명돼 있어요.

4.3.1. Unicode61 토크나이저

unicode 토크나이저는 모든 유니코드 문자를 "구분자(separator)" 또는 "토큰" 문자로 분류해요. 기본적으로 Unicode 6.1 이 정의하는 모든 공백과 문장 부호 문자가 구분자로, 다른 모든 문자가 토큰 문자로 간주돼요. 더 구체적으로, "L" 또는 "N" 으로 시작하는 일반 범주(구체적으로 문자와 숫자) 또는 범주 "Co"("기타, 개인 사용")에 할당된 모든 유니코드 문자가 토큰으로 간주돼요. 다른 모든 문자는 구분자예요.

하나 이상의 토큰 문자의 각 연속 실행(run)이 토큰으로 간주돼요. 토크나이저는 Unicode 6.1 이 정의한 규칙에 따라 대소문자를 구분하지 않아요.

기본적으로 모든 라틴 문자에서 분음 부호가 제거돼요. 이는 예를 들어 "A", "a", "À", "à", "Â", "â" 가 모두 동등한 것으로 간주된다는 뜻이에요.

토큰 사양에서 "unicode61" 뒤에 오는 인자는 번갈아 나타나는 옵션 이름과 값의 목록으로 처리돼요. Unicode61 은 다음 옵션을 지원해요:

Option Usage
remove_diacritics This option should be set to "0", "1" or "2". The default value is "1". If it is set to "1" or "2", then diacritics are removed from Latin script characters as described above. However, if it is set to "1", then diacritics are not removed in the fairly uncommon case where a single unicode codepoint is used to represent a character with more that one diacritic. For example, diacritics are not removed from codepoint 0x1ED9 ("LATIN SMALL LETTER O WITH CIRCUMFLEX AND DOT BELOW"). This is technically a bug, but cannot be fixed without creating backwards compatibility problems. If this option is set to "2", then diacritics are correctly removed from all Latin characters.
categories This option may be used to modify the set of Unicode general categories that are considered to correspond to token characters. The argument must consist of a space separated list of two-character general category abbreviations (e.g. "Lu" or "Nd"), or of the same with the second character replaced with an asterisk (""), interpreted as a glob pattern. The default value is "L N* Co".
tokenchars This option is used to specify additional unicode characters that should be considered token characters, even if they are white-space or punctuation characters according to Unicode 6.1. All characters in the string that this option is set to are considered token characters.
separators This option is used to specify additional unicode characters that should be considered as separator characters, even if they are token characters according to Unicode 6.1. All characters in the string that this option is set to are considered separators.

예를 들어:

-- Create an FTS5 table that does not remove diacritics from Latin
-- script characters, and that considers hyphens and underscore characters
-- to be part of tokens.
CREATE VIRTUAL TABLE ft USING fts5(a, b,
    tokenize = "unicode61 remove_diacritics 0 tokenchars '-_'"
);

또는:

-- Create an FTS5 table that, as well as the default token character classes,
-- considers characters in class "Mn" to be token characters.
CREATE VIRTUAL TABLE ft USING fts5(a, b,
    tokenize = "unicode61 categories 'L* N* Co Mn'"
);

fts5 unicode61 토크나이저는 fts3/4 unicode61 토크나이저와 바이트 단위로 호환돼요.

4.3.2. Ascii 토크나이저

Ascii 토크나이저는 다음을 제외하면 Unicode61 토크나이저와 비슷해요:

  • 모든 비-ASCII 문자(코드포인트 127 초과)는 항상 토큰 문자로 간주돼요. separators 옵션의 일부로 어떤 비-ASCII 문자가 지정되면 무시돼요.
  • 대소문자 접기는 ASCII 문자에 대해서만 수행돼요. 그래서 "A" 와 "a" 가 동등한 것으로 간주되는 반면 "Ã" 와 "ã" 는 구별돼요.
  • remove_diacritics 옵션은 지원되지 않아요.

예를 들어:

-- Create an FTS5 table that uses the ascii tokenizer, but does not
-- consider numeric characters to be part of tokens.
CREATE VIRTUAL TABLE ft USING fts5(a, b,
    tokenize = "ascii separators '0123456789'"
);
4.3.3. Porter 토크나이저

porter 토크나이저는 래퍼 토크나이저예요. 어떤 다른 토크나이저의 출력을 받아 그것을 FTS5 에 반환하기 전에 각 토큰에 porter stemming 알고리즘을 적용해요. 이는 "correction" 같은 검색 용어가 "corrected" 나 "correcting" 같은 유사 단어와 일치하게 해줘요. porter stemmer 알고리즘은 영어 용어에만 사용하도록 설계됐어요. 다른 언어와 함께 사용하면 검색 효용을 개선할 수도 있고 아닐 수도 있어요.

기본적으로 porter 토크나이저는 기본 토크나이저(unicode61)를 감싸는 래퍼로 작동해요. 또는 "tokenize" 옵션에 "porter" 뒤에 하나 이상의 추가 인자가 더해지면, porter stemmer 가 사용하는 기본 토크나이저의 사양으로 처리돼요. 예를 들어:

-- Two ways to create an FTS5 table that uses the porter tokenizer to
-- stem the output of the default tokenizer (unicode61).
CREATE VIRTUAL TABLE ft USING fts5(x, tokenize = porter);
CREATE VIRTUAL TABLE ft USING fts5(x, tokenize = 'porter unicode61');

-- A porter tokenizer used to stem the output of the unicode61 tokenizer,
-- with diacritics removed before stemming.
CREATE VIRTUAL TABLE ft USING fts5(x, tokenize = 'porter unicode61 remove_diacritics 1');
4.3.4. Trigram 토크나이저

trigram 토크나이저는 FTS5 를 확장해 보통의 토큰 일치 대신 일반적인 부분 문자열 일치를 지원해요. trigram 토크나이저를 사용할 때 쿼리 또는 구문 토큰은 행 안의 어떤 문자 시퀀스와도 일치할 수 있고, 완전한 토큰일 필요가 없어요. 예를 들어:

CREATE VIRTUAL TABLE tri USING fts5(a, tokenize="trigram");
INSERT INTO tri VALUES('abcdefghij KLMNOPQRST uvwxyz');

-- The following queries all match the single row in the table
SELECT * FROM tri('cdefg');
SELECT * FROM tri('cdefg AND pqr');
SELECT * FROM tri('"hij klm" NOT stuv');

trigram 토크나이저는 다음 옵션을 지원해요:

Option Usage
case_sensitive This value may be set to 1 or 0 (the default). If it is set to 1, then matching is case sensitive. Otherwise, if this option is set to 0, matching is case insensitive.
remove_diacritics This value may also be set to 1 or 0 (the default). It may only be set to 1 if the case_sensitive options is set to 0 - setting both options to 1 is an error. If this option is set, then diacritics are removed from the text before matching (e.g. so that "á" matches "a").
-- A case-sensitive trigram index
CREATE VIRTUAL TABLE tri USING fts5(a, tokenize="trigram case_sensitive 1");

remove_diacritics 옵션이 설정되지 않으면 trigram 토크나이저를 사용하는 FTS5 테이블도 인덱스된 GLOB 와 LIKE 패턴 일치를 지원해요. 예를 들어:

SELECT * FROM tri WHERE a LIKE '%cdefg%';
SELECT * FROM tri WHERE a GLOB '*ij klm*xyz';

case_sensitive 옵션을 1 로 설정해 FTS5 trigram 토크나이저를 만들면 GLOB 쿼리만 인덱싱할 수 있고 LIKE 는 인덱싱할 수 없어요.

참고 사항:

  • 3 개 미만의 유니코드 문자로 구성된 부분 문자열은 전체 텍스트 쿼리와 함께 사용될 때 어떤 행과도 일치하지 않아요. LIKE 또는 GLOB 패턴이 와일드카드가 아닌 유니코드 문자의 최소 하나의 시퀀스를 포함하지 않으면 FTS5 는 전체 테이블의 선형 스캔으로 대체돼요.
  • FTS5 테이블이 detail=none 또는 detail=column 옵션으로 만들어지면 전체 텍스트 쿼리는 3 유니코드 문자보다 긴 토큰을 포함할 수 없어요. LIKE 와 GLOB 패턴 일치는 약간 더 느릴 수 있지만 여전히 작동해요. 인덱스가 LIKE 및/또는 GLOB 패턴 일치에만 사용된다면 인덱스 크기를 줄이기 위해 이 옵션들을 실험해볼 가치가 있어요.
  • LIKE 연산자에 ESCAPE 절이 있으면 인덱스를 사용해 LIKE 패턴을 최적화할 수 없어요.

4.4. External Content 및 Contentless 테이블

보통 FTS5 테이블에 행이 삽입되면, 인덱스를 구축하는 것 외에도 FTS5 는 원래 행 내용의 복사본을 만들어요. 사용자나 보조 함수 구현이 FTS5 테이블에서 컬럼 값을 요청하면 그 값은 내용의 개인 복사본에서 읽혀요. "content" 옵션은 FTS5 전체 텍스트 인덱스 항목만 저장하는 FTS5 테이블을 만드는 데 사용될 수 있어요. 컬럼 값 자체가 보통 연관된 전체 텍스트 인덱스 항목보다 훨씬 크기 때문에, 이는 상당한 데이터베이스 공간을 절약할 수 있어요.

"content" 옵션을 사용하는 두 가지 방법이 있어요:

  • 빈 문자열로 설정해 contentless FTS5 테이블을 만든다. 이 경우 FTS5 는 쿼리를 처리할 때 원래 컬럼 값을 사용할 수 없다고 가정해요. 전체 텍스트 쿼리와 일부 보조 함수는 여전히 사용할 수 있지만, rowid 외에는 어떤 컬럼 값도 테이블에서 읽을 수 없어요.
  • 컬럼 값을 검색하기 위해 FTS5 가 언제든 쿼리할 수 있는 데이터베이스 객체(테이블, 가상 테이블 또는 뷰)의 이름으로 설정한다. 이것을 "external content" 테이블이라고 불러요. 이 경우 모든 FTS5 기능을 사용할 수 있지만, 전체 텍스트 인덱스의 내용이 지명된 데이터베이스 객체와 일관되도록 하는 것은 사용자의 책임이에요. 일관되지 않으면 쿼리 결과가 예측할 수 없게 될 수 있어요.
4.4.1. Contentless 테이블

contentless FTS5 테이블은 "content" 옵션을 빈 문자열로 설정해 만들어져요. 예를 들어:

CREATE VIRTUAL TABLE ft USING fts5(a, b, c, content='');

Contentless FTS5 테이블은 UPDATE 나 DELETE 문, 또는 rowid 필드에 NULL 이 아닌 값을 공급하지 않는 INSERT 문을 지원하지 않아요. Contentless 테이블은 REPLACE 충돌 처리를 지원하지 않아요. REPLACE 와 INSERT OR REPLACE 문은 일반 INSERT 문으로 처리돼요. 행은 FTS5 delete 명령을 사용해 contentless 테이블에서 삭제될 수 있어요.

contentless FTS5 테이블에서 rowid 를 제외한 어떤 컬럼 값을 읽으려는 시도는 SQL NULL 값을 반환해요.

4.4.2. Contentless-Delete 테이블

버전 3.43.0 부터 contentless-delete 테이블도 사용할 수 있어요. contentless-delete 테이블은 content 옵션을 빈 문자열로 설정하고 contentless_delete 옵션도 1 로 설정해 만들어져요. 예를 들어:

CREATE VIRTUAL TABLE ft USING fts5(a, b, c, content='', contentless_delete=1);

contentless-delete 테이블은 contentless 테이블과 다음 점이 달라요:

  • Contentless-delete 테이블은 DELETE 와 "INSERT OR REPLACE INTO" 문을 모두 지원해요.
  • Contentless-delete 테이블은 UPDATE 문을 지원하지만, fts5 테이블의 모든 사용자 정의 컬럼에 새 값이 공급될 때만 지원해요.
  • Contentless-delete 테이블은 FTS5 delete 명령을 지원하지 않아요.
-- Supported UPDATE statement:
UPDATE ft SET a=?, b=?, c=? WHERE rowid=?;

-- This UPDATE is not supported, as it does not supply a new value
-- for column "c".
UPDATE ft SET a=?, b=? WHERE rowid=?;

하위 호환성이 필요하지 않으면 새 코드는 contentless 테이블보다 contentless-delete 테이블을 선호해야 해요.

4.4.3. External Content 테이블

external content FTS5 테이블은 content 옵션을 같은 데이터베이스 안의 테이블, 가상 테이블 또는 뷰(이하 "content table")의 이름으로 설정해 만들어져요. FTS5 가 컬럼 값을 필요로 할 때마다 다음 쿼리처럼 content table 을 쿼리하는데, 값이 필요한 행의 rowid 가 SQL 변수에 바인딩돼요:

SELECT <content_rowid>, <cols> FROM <content> WHERE <content_rowid> = ?;

위에서 는 content table 의 이름으로 대체돼요. 기본적으로 <content_rowid> 는 리터럴 텍스트 "rowid" 로 대체돼요. 또는 CREATE VIRTUAL TABLE 문 안에 "content_rowid" 옵션이 설정되면 그 옵션의 값으로 대체돼요. 는 FTS5 테이블 컬럼 이름의 쉼표로 구분된 목록으로 대체돼요. 예를 들어:

-- If the database schema is:
CREATE TABLE t1 (a, b, c, d INTEGER PRIMARY KEY);
CREATE VIRTUAL TABLE ft USING fts5(a, c, content=t1, content_rowid=d);

-- Fts5 may issue queries such as:
SELECT d, a, c FROM t1 WHERE d = ?;

content table 은 다음처럼 쿼리될 수도 있어요:

SELECT <content_rowid>, <cols> FROM <content> ORDER BY <content_rowid> ASC;
SELECT <content_rowid>, <cols> FROM <content> ORDER BY <content_rowid> DESC;

external content FTS5 테이블의 내용을 content table 과 최신 상태로 유지하는 것은 여전히 사용자의 책임이에요. 이를 수행하는 한 가지 방법은 트리거를 사용하는 것이에요. 예를 들어:

-- Create a table. And an external content fts5 table to index it.
CREATE TABLE t1(a INTEGER PRIMARY KEY, b, c);
CREATE VIRTUAL TABLE fts_idx USING fts5(b, c, content='t1', content_rowid='a');

-- Triggers to keep the FTS index up to date.
CREATE TRIGGER t1_ai AFTER INSERT ON t1 BEGIN
  INSERT INTO fts_idx(rowid, b, c) VALUES (new.a, new.b, new.c);
END;
CREATE TRIGGER t1_ad AFTER DELETE ON t1 BEGIN
  INSERT INTO fts_idx(fts_idx, rowid, b, c) VALUES('delete', old.a, old.b, old.c);
END;
CREATE TRIGGER t1_au AFTER UPDATE ON t1 BEGIN
  INSERT INTO fts_idx(fts_idx, rowid, b, c) VALUES('delete', old.a, old.b, old.c);
  INSERT INTO fts_idx(rowid, b, c) VALUES (new.a, new.b, new.c);
END;

contentless 테이블처럼 external content 테이블은 REPLACE 충돌 처리를 지원하지 않아요. REPLACE 충돌 처리를 지정하는 어떤 작업이든 ABORT 를 사용해 처리돼요.

4.4.4. External Content 테이블 함정

FTS5 external content 테이블(비어 있지 않은 content= 옵션을 가진 것)을 content itself(content= 옵션이 지명한 테이블)와 일관되게 유지하는 것은 사용자의 책임이에요. 이들이 불일치하도록 방치되면 FTS5 테이블에 대한 쿼리 결과가 직관적이지 않고 불일치해 보일 수 있어요.

이런 상황에서 FTS5 external content 테이블에 대한 쿼리가 만들어내는 겉보기에 불일치하는 결과는 다음과 같이 이해될 수 있어요:

  • 쿼리가 전체 텍스트 인덱스를 사용하지 않으면 - MATCH 연산자나 동등한 테이블-값 함수 구문을 포함하지 않으면 - 그 쿼리는 효과적으로 external content table 로 전달돼요. 이 경우 FTS 인덱스의 내용은 쿼리 결과에 영향을 주지 않아요.
  • 쿼리가 전체 텍스트 인덱스를 사용하면 FTS5 모듈은 쿼리와 일치하는 문서에 해당하는 rowid 값 집합을 그것으로 쿼리해요. 각 rowid 에 대해 필요한 컬럼 값을 검색하려면 다음 유사 쿼리를 실행하는데, '?' 는 rowid 값으로, 와 <content_rowid> 는 content= 와 content_rowid= 옵션에 지정된 값으로 대체돼요:
SELECT <content_rowid>, <cols> FROM <content> WHERE <content_rowid> = ?;

예를 들어 다음 스크립트로 데이터베이스가 만들어지면:

-- Create and populate a table.
CREATE TABLE t1(a INTEGER PRIMARY KEY, t TEXT);
INSERT INTO t1 VALUES(1, 'all that glitters');
INSERT INTO t1 VALUES(2, 'is not gold');

-- Create an external content FTS5 table
CREATE VIRTUAL TABLE ft USING fts5(t, content='t1', content_rowid='a');

content table 은 두 행을 포함하지만 FTS 인덱스에는 그것들에 해당하는 항목이 없어요. 이 경우 다음 쿼리는 다음과 같이 불일치한 결과를 반환해요:

-- Returns 2 rows.  Because the query does not use the FTS index, it is
-- effectively executed against table 't1' directly, and so returns
-- both rows.
SELECT * FROM ft;

-- Returns 0 rows.  This query does use the FTS index, which currently
-- contains no entries. So it returns 0 rows.
SELECT rowid, t FROM ft('gold')

반대로 데이터베이스가 다음과 같이 만들어지고 채워지면:

-- Create and populate a table.
CREATE TABLE t1(a INTEGER PRIMARY KEY, t TEXT);

-- Create an external content FTS5 table
CREATE VIRTUAL TABLE ft USING fts5(t, content='t1', content_rowid='a');
INSERT INTO ft(rowid, t) VALUES(1, 'all that glitters');
INSERT INTO ft(rowid, t) VALUES(2, 'is not gold');

content table 은 비어 있지만 FTS 인덱스는 6 개의 다른 토큰에 대한 항목을 포함해요. 이 경우 다음 쿼리는 다음과 같이 불일치한 결과를 반환해요:

-- Returns 0 rows.  Since it does not use the FTS index, the query is
-- passed directly through to table 't1', which contains no data.
SELECT * FROM ft;

-- Returns 1 row. The "rowid" field of the returned row is 2, and
-- the "t" field set to NULL. "t" is set to NULL because when the external
-- content table "t1" was queried for the data associated with the row
-- with a=2 ("a" is the content_rowid column), none could be found.
SELECT rowid, t FROM ft('gold')

이전 섹션에서 설명한 대로 content table 의 트리거는 FTS5 external content table 을 일관되게 유지하는 좋은 방법이에요. 하지만 트리거는 content table 에서 행이 삽입, 갱신 또는 삭제될 때만 발화돼요. 이는 예를 들어 데이터베이스가 다음과 같이 만들어지면:

-- Create and populate a table.
CREATE TABLE t1(a INTEGER PRIMARY KEY, t TEXT);
INSERT INTO t1 VALUES(1, 'all that glitters');
INSERT INTO t1 VALUES(2, 'is not gold');

-- Create an external content FTS5 table
CREATE VIRTUAL TABLE ft USING fts5(t, content='t1', content_rowid='a');

-- Create triggers to keep the FTS5 table up to date
CREATE TRIGGER t1_ai AFTER INSERT ON t1 BEGIN
  INSERT INTO ft(rowid, t) VALUES (new.a, new.t);
END;
<similar triggers for update + delete>

트리거를 만드는 것이 content table 의 기존 행을 FTS 인덱스로 복사하지 않으므로 content table 과 external content FTS5 table 은 불일치해요. 트리거는 트리거가 만들어진 후 content table 에 이루어진 갱신만 FTS 인덱스에 반영되도록 보장할 수 있어요.

이 경우와 FTS 인덱스와 그 content table 이 불일치하게 된 다른 어떤 상황에서도, 'rebuild' 명령을 사용해 FTS 인덱스의 내용을 완전히 버리고 content table 의 현재 내용에 기반해 다시 만들 수 있어요.

4.4.4.1. Contentless 테이블의 갱신과 삭제

Contentless 테이블은 UPDATE 와 DELETE 문을 지원하지만 주의해서 사용해야 해요. contentless 테이블에 대해 DELETE 가 실행되면 영향받는 각 행에 대해 다음 쿼리처럼 content table 을 쿼리해 현재 값이 검색돼요:

SELECT <content_rowid>, <cols> FROM <content> WHERE <content_rowid> = ?;

검색된 값은 테이블의 구성된 토크나이저에 전달되고, 토크나이저는 FTS 인덱스에서 제거해야 할 토큰 목록을 추출해요. 검색된 값이 그 행에 대한 FTS 인덱스의 항목 집합과 일치하지 않으면 일부 토큰이 FTS 인덱스에 남아 나중에 불일치한 쿼리 결과를 초래할 수 있어요.

실제로 이는 content table 과 FTS5 external content table 둘 다에서 행을 삭제하려면 FTS5 테이블을 먼저 갱신해야 한다는 뜻이에요(content table 행이 여전히 그것에 사용 가능하도록).

FTS5 에서 UPDATE 문은 DELETE 다음에 새 값의 INSERT 가 오는 것으로 구현돼요. 따라서 content table 과 FTS5 external content table 둘 다에서 행을 UPDATE 할 때도 FTS5 테이블을 먼저 갱신해야 해요(다시, content table 행이 그것에 사용 가능하도록).

4.5. Columnsize 옵션

보통 FTS5 는 주 FTS5 테이블에 삽입된 각 컬럼 값을 토큰 단위 크기로 별도 테이블에 저장하는 특별한 백킹 테이블을 데이터베이스 안에 유지해요. 이 백킹 테이블은 xColumnSize API 함수가 사용하며, 이 함수는 차례로 내장 bm25 순위 함수가 사용해요(다른 순위 함수에도 유용할 가능성이 높아요).

공간을 절약하기 위해 columnsize 옵션을 0 으로 설정해 이 백킹 테이블을 생략할 수 있어요. 예를 들어:

-- A table without the xColumnSize() values stored on disk:
CREATE VIRTUAL TABLE ft USING fts5(a, b, c, columnsize=0);

-- Three equivalent ways of creating a table that does store the
-- xColumnSize() values on disk:
CREATE VIRTUAL TABLE ft USING fts5(a, b, c);
CREATE VIRTUAL TABLE ft USING fts5(a, b, c, columnsize=1);
CREATE VIRTUAL TABLE ft USING fts5(a, b, columnsize='1', c);

columnsize 옵션을 0 이나 1 이외의 값으로 설정하는 것은 에러예요.

FTS5 테이블이 columnsize=0 으로 구성됐지만 contentless 테이블이 아니면, xColumnSize API 함수는 여전히 작동하지만 훨씬 느리게 실행돼요. 이 경우 반환할 값을 데이터베이스에서 직접 읽는 대신 텍스트 값 자체를 읽고 그 안의 토큰을 필요할 때 세어요.

또는 테이블이 또한 contentless 테이블이면 다음이 적용돼요:

  • xColumnSize API 는 항상 -1 을 반환해요. columnsize=0 으로 구성된 contentless FTS5 테이블에 저장된 값의 토큰 수를 결정할 방법이 없어요.
  • 각 삽입 행은 명시적으로 지정된 rowid 값이 동반되어야 해요. contentless 테이블이 columnsize=0 으로 구성되면 rowid 에 NULL 값을 삽입하려는 시도는 SQLITE_MISMATCH 에러예요.
  • 테이블에 대한 모든 쿼리는 전체 텍스트 쿼리여야 해요. 즉, MATCH 또는 = 연산자를 테이블-이름 컬럼과 함께 왼쪽 피연산자로 사용하거나, 테이블-값 함수 구문을 사용해야 해요. 전체 텍스트 쿼리가 아닌 어떤 쿼리든 에러를 초래해요.

xColumnSize 값이 저장되는 테이블의 이름(columnsize=0 이 지정되지 않았다면)은 "_docsize" 이고, 여기서 은 FTS5 테이블 자체의 이름이에요. 기존 데이터베이스에서 columnsize=0 을 사용해 FTS5 테이블을 다시 만들면 얼마나 많은 공간을 절약할 수 있는지 결정하기 위해 sqlite3_analyzer 도구를 사용할 수 있어요.

4.6. Detail 옵션

문서의 각 용어에 대해 FTS5 가 유지하는 FTS 인덱스는 문서의 rowid, 용어를 포함하는 컬럼의 컬럼 번호, 컬럼 값 안의 용어 오프셋을 저장해요. "detail" 옵션을 사용해 이 정보 중 일부를 생략할 수 있어요. 이는 인덱스가 데이터베이스 파일 안에서 소비하는 공간을 줄이지만, 시스템의 기능과 효율도 줄여요.

detail 옵션은 "full"(기본값), "column" 또는 "none" 으로 설정될 수 있어요. 예를 들어:

-- The following two lines are equivalent (because the default value
-- of "detail" is "full".
CREATE VIRTUAL TABLE ft USING fts5(a, b, c);
CREATE VIRTUAL TABLE ft USING fts5(a, b, c, detail=full);

CREATE VIRTUAL TABLE ft USING fts5(a, b, c, detail=column);
CREATE VIRTUAL TABLE ft USING fts5(a, b, c, detail=none);

detail 옵션이 column 으로 설정되면 각 용어에 대해 FTS 인덱스는 rowid 와 컬럼 번호만 기록하고 용어 오프셋 정보는 생략해요. 이는 다음 제한을 초래해요:

  • NEAR 쿼리를 사용할 수 없어요.
  • 구문 쿼리를 사용할 수 없어요.
  • 테이블이 또한 contentless 테이블이 아니라고 가정하면, xInstCount, xInst, xPhraseFirst, xPhraseNext 는 평소보다 느려요. 이것은 FTS 인덱스에서 필요한 데이터를 직접 읽는 대신 문서 텍스트를 필요할 때 로드하고 토큰화해야 하기 때문이에요.
  • 테이블이 또한 contentless 테이블이면 xInstCount, xInst, xPhraseFirst, xPhraseNext API 는 현재 행에 구문 일치가 전혀 없는 것처럼 동작해요(즉, xInstCount() 가 0 을 반환해요).

detail 옵션이 none 으로 설정되면 각 용어에 대해 FTS 인덱스는 rowid 만 기록해요. 컬럼과 오프셋 정보가 모두 생략돼요. 위에서 detail=column 모드에 대해 항목으로 나열한 제한 외에도 다음 추가 제한을 부과해요:

  • 컬럼 필터 쿼리를 사용할 수 없어요.
  • 테이블이 또한 contentless 테이블이 아니라고 가정하면, xPhraseFirstColumn 과 xPhraseNextColumn 은 평소보다 느려요.
  • 테이블이 또한 contentless 테이블이면 xPhraseFirstColumn 과 xPhraseNextColumn API 는 현재 행에 구문 일치가 전혀 없는 것처럼 동작해요(즉, xPhraseFirstColumn() 이 반복자를 EOF 로 설정해요).

대규모 이메일 집합(디스크에서 1636 MiB)을 인덱싱한 한 테스트에서 실행 시 FTS 인덱스는 detail=full 일 때 디스크에서 743 MiB, detail=column 일 때 340 MiB, detail=none 일 때 134 MiB 였어요.

4.7. Tokendata 옵션

이 옵션은 사용자 정의 토크나이저를 구현하는 애플리케이션에게만 유용해요. 보통 토크나이저는 0x00 바이트를 포함한 어떤 바이트 시퀀스로 구성된 토큰이라도 반환할 수 있어요. 하지만 테이블이 tokendata=1 옵션을 지정하면, fts5 는 일치 목적상 토큰의 첫 0x00 바이트와 뒤따르는 어떤 후행 데이터든 무시해요. 토크나이저가 반환한 전체 토큰을 여전히 저장하지만, 나머지는 fts5 코어가 무시해요.

0x00 바이트와 후행 데이터를 포함한 토큰의 전체 버전은 xQueryToken 과 xInstToken API 를 통해 사용자 정의 보조 함수에 사용 가능해요.

이것은 순위 함수에 유용할 수 있어요. 사용자 정의 토크나이저는 일부 문서 토큰에 추가 데이터를 더해, 순위 함수가 일부 토큰(예: 문서 제목에 있는 것)의 히트에 더 많은 가중치를 부여하게 할 수 있어요.

사용자 정의 토크나이저와 사용자 정의 보조 함수의 조합은 비대칭 검색을 구현하는 데 사용될 수 있어요. 토크나이저는 (예를 들어) 각 문서 토큰에 대해 토큰의 대소문자 정규화되고 표시가 없는 버전, 그 뒤에 0x00 바이트, 그 뒤에 문서의 토큰 전체 텍스트를 반환할 수 있어요. 쿼리되면 fts5 는 쿼리의 모든 문자가 대소문자 정규화되고 표시가 없는 것처럼 결과를 제공할 거예요. 그런 다음 사용자 정의 보조 함수를 쿼리의 WHERE 절에 사용해 문서나 쿼리 용어의 2차 또는 3차 표시에 기반해 일치하지 않는 행을 걸러낼 수 있어요.

4.8. Locale 옵션

이 옵션은 사용자 정의 토크나이저를 구현하는 애플리케이션에게만 유용해요. "locale=1" 옵션이 지정된 fts5 테이블이 만들어지면, fts5_locale() SQL 함수를 사용해 로케일 값(예: "th_TH" 또는 "en_US")을 FTS5 에 전달되는 문자열과 연관시킬 수 있어요. FTS5 자체는 로케일 값을 사용하지 않지만, 문자열이 토큰화될 때마다 토크나이저 구현에 그것들을 사용 가능하게 해줘요. 그러면 토크나이저는 로케일에 기반해 그 동작을 조정할 수 있어요.

-- The following statement creates an fts5 table with locale support.
-- The "tokenizer=..." option below must be replaced with a real tokenizer
-- specification for a tokenizer that supports locales.
CREATE VIRTUAL TABLE ft USING fts5(a, b, locale=1, tokenizer=...);

-- This statement inserts a row into the table. The value inserted into
-- column "a" uses locale "th_TH", the value written to column "b" uses the
-- tokenizer's default locale
INSERT INTO ft(a, b) VALUES(
     fts5_locale('th_TH', 'Tokenize this in Thai locale'),
     'Tokenize this in the default locale.'
);

-- The "en_US" locale is used to tokenize the query terms in the
-- following query.
SELECT * FROM ft( fts5_locale('en_US', 'query terms') );

fts5_locale() 문자열을 locale=1 옵션으로 만들어지지 않은 fts5 테이블에 전달하려는 시도는 에러예요.

fts5_locale() 문자열이 일반 content table(즉 contentless 나 external content 테이블이 아닌)에 저장되면 첨부된 로케일이 그것과 함께 저장돼요. 그 행이 삭제되거나 보조 함수 평가의 일부로 FTS5 가 문자열을 다시 토큰화하면, 첨부된 로케일이 다시 토크나이저 구현에 전달돼요.

로케일을 지원하기 위해 FTS5 external-content 테이블은 content table 로 fts5_locale() 값을 반환하는 SQL 뷰를 사용할 수 있어요. 예를 들어:

-- Each row of this table contains a string and its locale.
CREATE TABLE t1(val, locale);
INSERT INTO t1 VALUES('a text value', 'en_US');

-- A view to combine the string and locale from table t1.
CREATE VIEW v1 AS SELECT rowid, fts5_locale(val, locale) AS val FROM t1;

-- An FTS5 table to read locale-enabled strings from view v1.
CREATE VIRTUAL TABLE ft USING fts5(val, locale=1, content=v1, tokenize=...);

fts5_locale() 값이 fts5 테이블의 UNINDEXED 컬럼에 기록되면 로케일 값은 버려지고 문자열이 단독으로 저장돼요.

fts5_get_locale() 함수를 사용해 FTS5 테이블에 저장된 값의 로케일을 검색할 수 있어요.

4.9. Contentless-Unindexed 옵션

보통 contentless 테이블에 속한 UNINDEXED 컬럼은 별로 유용하지 않아요. 그것들에 기록된 값은 인덱싱되거나 저장되지 않고, 그러한 UNINDEXED 컬럼에서 읽는 것은 항상 NULL 을 반환해요. 하지만 contentless 테이블에 "contentless_unindexed=1" 옵션이 지정되면, 다른 컬럼에 기록된 값은 그렇지 않더라도 UNINDEXED 컬럼의 값은 영구히 저장돼요.

-- Create a contentless table with the contentless_unindexed=1 option.
-- Of the row written to it, the value 'one' will be indexed and then
-- discarded, and the value "1" will be stored but not indexed.
CREATE VIRTUAL TABLE ft USING fts5(a, b UNINDEXED, content='', contentless_unindexed=1);
INSERT INTO ft(a, b) VALUES('one', 1);

-- This query returns 1 row with 2 columns - (NULL, 1). Reading from
-- column "a" is always NULL, as the table is contentless. But reading from
-- "b" returns the value, as the table uses contentless_unindexed=1.
SELECT a, b FROM ft('one');

contentless 또는 contentless-delete 테이블이 아닌 fts5 테이블에 contentless_unindexed=1 을 지정하는 것은 에러예요.

5. 보조 함수 (Auxiliary Functions)

보조 함수는 SQL 스칼라 함수와 비슷하지만, FTS5 테이블에 대한 전체 텍스트 쿼리(MATCH 연산자 또는 trigram 토크나이저와 함께 LIKE/GLOB 를 사용하는 것) 안에서만 사용될 수 있다는 점이 달라요. 그것들의 결과는 전달된 인자뿐만 아니라 현재 일치와 일치된 행에도 기반해 계산돼요. 예를 들어 보조 함수는 일치의 정확도를 나타내는 숫자 값( bm25() 함수 참고)이나, 검색 용어의 인스턴스를 하나 이상 포함하는 일치된 행의 텍스트 조각(snippet() 함수 참고)을 반환할 수 있어요.

보조 함수를 호출하려면 FTS5 테이블의 이름을 첫 번째 인자로 지정해야 해요. 호출되는 특정 보조 함수에 따라 다른 인자가 첫 번째 다음에 올 수 있어요. 예를 들어 "highlight" 함수를 호출하려면:

-- Assuming fts5 table:
CREATE VIRTUAL TABLE ft USING fts5(a, b, c);

-- Invoke the highlight() function:
SELECT highlight(ft, 2, '<b>', '</b>') FROM ft WHERE ft MATCH 'fts5'

FTS5 의 일부로 제공되는 내장 보조 함수는 다음 섹션에서 설명돼요. 애플리케이션은 C 로 사용자 정의 보조 함수를 구현할 수도 있어요.

5.1. 내장 보조 함수

FTS5 는 세 개의 내장 보조 함수를 제공해요:

  • bm25() 보조 함수는 현재 일치의 정확도를 반영하는 실수 값을 반환해요. 더 나은 일치에는 수치적으로 더 낮은 값이 할당돼요.
  • highlight() 보조 함수는 현재 일치의 컬럼 중 하나의 텍스트 복사본을, 쿼리된 용어의 각 인스턴스가 결과 안에서 지정된 마크업(예: "" 와 "")으로 둘러싸인 채로 반환해요.
  • snippet() 보조 함수는 일치된 행의 컬럼 중 하나에서 짧은 텍스트 조각을 선택하고, highlight() 함수와 같은 방식으로 쿼리된 용어의 각 인스턴스가 마크업으로 둘러싸인 채 반환해요. 텍스트 조각은 포함하는 서로 다른 쿼리된 용어 수를 최대화하도록 선택돼요. 컬럼 값의 시작에서 발생하거나 텍스트에서 "." 또는 ":" 문자 바로 뒤에 오는 스니펫에 더 높은 가중치가 부여돼요.
  • fts5_get_locale() 보조 함수는 FTS5 테이블에 저장된 값과 연관된 로케일(있으면)을 검색하는 데 사용돼요.
5.1.1. bm25() 함수

내장 보조 함수 bm25() 는 현재 행이 전체 텍스트 쿼리와 얼마나 잘 일치하는지를 나타내는 실수 값을 반환해요. 일치가 좋을수록 반환되는 값은 수치적으로 더 작아요. 다음과 같은 쿼리를 사용해 일치를 최고에서 최저 순서로 반환할 수 있어요:

SELECT * FROM ft WHERE ft MATCH ? ORDER BY bm25(ft)

문서 점수를 계산하기 위해 전체 텍스트 쿼리는 구성 구문으로 분리돼요. 문서 D 와 쿼리 Q 에 대한 bm25 점수는 다음과 같이 계산돼요:

bm25 = -1 * SUM_i [ IDF(qi) * f(qi,D) * (k1+1) / ( f(qi,D) + k1 * (1 - b + b*|D|/avgdl) ) ]

위에서 nPhrase 는 쿼리의 구문 수예요. |D| 는 현재 문서의 토큰 수이고, avgdl 은 FTS5 테이블 안의 모든 문서의 평균 토큰 수예요. k1b 는 둘 다 상수로, 각각 1.2 와 0.75 로 하드-코딩돼 있어요.

공식 시작 부분의 "-1" 항은 BM25 알고리즘의 대부분 구현에는 없어요. 그것이 없으면 더 나은 일치에 수치적으로 더 높은 BM25 점수가 할당돼요. 기본 정렬 순서가 "오름차순"이므로, 쿼리에 "ORDER BY bm25(ft)" 를 추가하면 최저에서 최고 순서로 결과가 반환된다는 뜻이에요. 최고 일치를 먼저 반환하려면 "DESC" 키워드가 필요할 거예요. 이 함정을 피하기 위해 FTS5 의 BM25 구현은 반환하기 전에 결과에 -1 을 곱해, 더 나은 일치에 수치적으로 더 낮은 점수가 할당되도록 보장해요.

IDF(qi) 는 쿼리 구문 i 의 역문서-빈도(inverse-document-frequency)예요. 다음처럼 계산돼요. 여기서 N 은 FTS5 테이블의 총 행 수이고 n(qi) 는 구문 i 의 인스턴스를 최소 하나 포함하는 총 행 수예요:

IDF(qi) = ln( (N - n(qi) + 0.5) / (n(qi) + 0.5) )

마지막으로 f(qi,D) 는 구문 i 의 구문 빈도예요. 기본적으로 이는 현재 행 안의 구문 발생 수예요. 하지만 bm25() SQL 함수에 추가 실수 값 인자를 전달함으로써 테이블의 각 컬럼에 다른 가중치를 할당할 수 있고, 구문 빈도는 다음처럼 계산돼요:

f(qi,D) = SUM_c [ wc * n(qi,c) ]

여기서 wc 는 컬럼 c 에 할당된 가중치이고 n(qi,c) 는 현재 행의 컬럼 c 에서 구문 i 의 발생 수예요. 테이블 이름 다음에 bm25() 에 전달되는 첫 인자는 FTS5 테이블의 가장 왼쪽 컬럼에 할당된 가중치예요. 두 번째는 왼쪽에서 두 번째 컬럼에 할당된 가중치이고, 이런 식이에요. 모든 테이블 컬럼에 대한 인자가 충분하지 않으면 나머지 컬럼에는 1.0 가중치가 할당돼요. 끝에 인자가 너무 많으면 추가 것은 무시돼요. 예를 들어:

-- Assuming the following schema:
CREATE VIRTUAL TABLE email USING fts5(sender, title, body);

-- Return results in bm25 order, with each phrase hit in the "sender"
-- column considered the equal of 10 hits in the "body" column, and
-- each hit in the "title" column considered as valuable as 5 hits in
-- the "body" column.
SELECT * FROM email WHERE email MATCH ? ORDER BY bm25(email, 10.0, 5.0);

BM25 와 그 변형에 대한 자세한 정보는 wikipedia 를 참고하세요.

5.1.2. highlight() 함수

highlight() 함수는 현재 행의 지정된 컬럼에서 텍스트 복사본을, 구문 일치의 시작과 끝을 표시하는 추가 마크업 텍스트가 삽입된 채로 반환해요.

highlight() 는 테이블 이름 다음에 정확히 세 개의 인자로 호출되어야 해요. 다음과 같이 해석돼요:

  • 텍스트를 읽을 FTS 테이블 컬럼의 인덱스를 나타내는 정수. 컬럼은 왼쪽에서 오른쪽으로 0 부터 번호가 매겨져요.
  • 각 구문 일치 앞에 삽입할 텍스트.
  • 각 구문 일치 뒤에 삽입할 텍스트.

예를 들어:

-- Return a copy of the text from the leftmost column of the current
-- row, with phrase matches marked using html "b" tags.
SELECT highlight(ft, 0, '<b>', '</b>') FROM ft WHERE ft MATCH ?

두 개 이상의 구문 인스턴스가 겹치는(하나 이상의 토큰을 공유하는) 경우, 겹치는 구문의 각 집합에 대해 단일 여는 표시와 닫는 표시가 삽입돼요. 예를 들어:

-- Assuming this:
CREATE VIRTUAL TABLE ft USING fts5(a);
INSERT INTO ft VALUES('a b c x c d e');
INSERT INTO ft VALUES('a b c c d e');
INSERT INTO ft VALUES('a b c d e');

-- The following SELECT statement returns these three rows:
--   '[a b c] x [c d e]'
--   '[a b c] [c d e]'
--   '[a b c d e]'
SELECT highlight(ft, 0, '[', ']') FROM ft WHERE ft MATCH 'a+b+c AND c+d+e';
5.1.3. snippet() 함수

snippet() 함수는 highlight() 와 비슷하지만, 전체 컬럼 값을 반환하는 대신 처리하고 반환할 짧은 문서 텍스트 조각을 자동으로 선택하고 추출해요. snippet() 함수는 테이블 이름 인자 뒤에 다섯 개의 매개변수가 전달되어야 해요:

  • 반환할 텍스트를 선택할 FTS 테이블 컬럼의 인덱스를 나타내는 정수. 컬럼은 왼쪽에서 오른쪽으로 0 부터 번호가 매겨져요. 음수 값은 컬럼이 자동으로 선택되어야 함을 나타내요.
  • 반환된 텍스트 안의 각 구문 일치 앞에 삽입할 텍스트.
  • 반환된 텍스트 안의 각 구문 일치 뒤에 삽입할 텍스트.
  • 반환된 텍스트가 그 컬럼의 시작이나 끝에서 발생하지 않음을 나타내기 위해 선택된 텍스트의 시작이나 끝에 추가할 텍스트.
  • 반환된 텍스트의 최대 토큰 수. 0 보다 크고 64 보다 작거나 같아야 해요.
5.1.4. fts5_get_locale() 함수

fts5_get_locale() 함수는 FTS5 테이블에 저장된 값과 연관된 로케일(있으면)을 검색하는 데 사용돼요. 테이블 이름 다음에 단일 인자를 받는데, 쿼리할 현재 행의 컬럼 인덱스예요. 컬럼은 CREATE VIRTUAL TABLE 문에 나타난 순서대로 0 부터 번호가 매겨져요.

FTS5 테이블이 로케일을 지원하지 않거나(즉 locale=1 옵션으로 만들어지지 않았거나) 지명된 값과 연관된 로케일이 없으면 이 함수는 NULL 을 반환해요. 그렇지 않으면 해당 값이 연관된 로케일의 이름인 텍스트 값을 반환해요.

CREATE VIRTUAL TABLE ft USING fts5(a, b, c, locale=1);
INSERT INTO ft VALUES(
    'no locale',
    fts5_locale('th_TH', 'Thai locale'),
    fts5_locale('en_US', 'US locale')
);

-- The following statement returns a single row containing three values:
-- NULL, text value 'th_TH', and text value 'en_US'.
SELECT
    fts5_get_locale(ft, 0),
    fts5_get_locale(ft, 1),
    fts5_get_locale(ft, 2)
FROM ft;
5.1.5. fts5_insttoken() 함수

fts5_insttoken() 함수는 쿼리가 xInstToken API 의 추가 데이터가 접두어 쿼리와 효율적으로 작동하기를 요구하는 것으로 표시하는 데 사용될 수 있어요. 자세한 내용은 xInstToken 에 대한 연결된 문서를 참고하세요. 다음과 같이 사용돼요:

-- Use the fts5_insttoken() function to ensure that if the custom API
-- function used by this query invokes the xInstToken() API on a token
-- matched by the prefix 'quer*' , it does not have to make a second pass
-- of the index to gather the required data.
SELECT custom_api_function(ft) FROM ft( fts5_insttoken('prefix quer*') );

 -- This query returns the same results, but may be less efficient if
-- the custom function invokes the xInstToken() API as described above.
SELECT custom_api_function(ft) FROM ft( 'prefix quer*' );

5.2. 보조 함수 결과로 정렬

모든 FTS5 테이블에는 "rank" 라는 특별한 숨은 컬럼이 있어요. 현재 쿼리가 전체 텍스트 쿼리가 아니면(즉 MATCH 연산자를 포함하지 않으면) "rank" 컬럼의 값은 항상 NULL 이에요. 그렇지 않으면 전체 텍스트 쿼리에서 rank 컬럼은 기본적으로 끝 인자 없는 bm25() 보조 함수를 실행해 반환되는 것과 같은 값을 포함해요.

rank 컬럼에서 읽는 것과 쿼리 안에서 bm25() 함수를 직접 사용하는 것의 차이는 반환된 값으로 정렬할 때만 의미가 있어요. 이 경우 "rank" 를 사용하는 것이 bm25() 를 사용하는 것보다 빠르요.

-- The following queries are logically equivalent. But the second may
-- be faster, particularly if the caller abandons the query before
-- all rows have been returned (or if the queries were modified to
-- include LIMIT clauses).
SELECT * FROM ft WHERE ft MATCH ? ORDER BY bm25(ft);
SELECT * FROM ft WHERE ft MATCH ? ORDER BY rank;

끝 인자 없는 bm25() 를 사용하는 대신 rank 컬럼에 매핑된 특정 보조 함수가 쿼리별로, 또는 FTS 테이블의 다른 영구 기본값을 설정해 구성될 수 있어요.

단일 쿼리에 대해 rank 컬럼의 매핑을 바꾸기 위해 다음 중 하나와 유사한 용어가 쿼리의 WHERE 절에 추가돼요:

rank MATCH 'auxiliary-function-name(arg1, arg2, ...)'
rank = 'auxiliary-function-name(arg1, arg2, ...)'

MATCH 또는 = 연산자의 오른쪽은 호출할 보조 함수와 그 뒤에 오는 괄호 안의 0 개 이상의 쉼표로 구분된 인자로 구성된 문자열로 평가되는 상수 표현식이어야 해요. 인자는 SQL 리터럴이어야 해요. 예를 들어:

-- The following queries are logically equivalent. But the second may
-- be faster. See above.
SELECT * FROM ft WHERE ft MATCH ? ORDER BY bm25(ft, 10.0, 5.0);
SELECT * FROM ft WHERE ft MATCH ? AND rank MATCH 'bm25(10.0, 5.0)' ORDER BY rank;

대체 순위 함수를 지정하기 위해 테이블-값 함수 구문도 사용될 수 있어요. 이 경우 순위 함수를 설명하는 텍스트가 두 번째 테이블-값 함수 인자로 지정돼요. 다음 세 쿼리는 동등해요:

SELECT * FROM ft WHERE ft MATCH ? AND rank MATCH 'bm25(10.0, 5.0)' ORDER BY rank;
SELECT * FROM ft WHERE ft = ? AND rank = 'bm25(10.0, 5.0)' ORDER BY rank;
SELECT * FROM ft WHERE ft(?, 'bm25(10.0, 5.0)') ORDER BY rank;

테이블의 rank 컬럼의 기본 매핑은 FTS5 rank 구성 옵션을 사용해 수정될 수 있어요.

6. 특별 INSERT 명령

6.1. 'automerge' 구성 옵션

전체 텍스트 인덱스를 저장하는 데 디스크의 단일 데이터 구조를 사용하는 대신 FTS5 는 일련의 b-tree 를 사용해요. 새 트랜잭션이 커밋될 때마다 커밋된 트랜잭션의 내용을 포함하는 새 b-tree 가 데이터베이스 파일에 기록돼요. 전체 텍스트 인덱스가 쿼리될 때 각 b-tree 를 개별적으로 쿼리하고 사용자에게 반환되기 전에 결과를 병합해야 해요.

데이터베이스의 b-tree 수가 너무 커지는 것(쿼리 속도 저하)을 막기 위해 더 작은 b-tree 들이 주기적으로 같은 데이터를 포함하는 단일 더 큰 b-tree 로 병합돼요. 기본적으로 이는 전체 텍스트 인덱스를 수정하는 INSERT, UPDATE, DELETE 문 안에서 자동으로 발생해요. 'automerge' 매개변수는 한 번에 몇 개의 더 작은 b-tree 가 병합되는지 결정해요. 작은 값으로 설정하면 쿼리를 빠르게 할 수 있지만(더 적은 b-tree 를 쿼리하고 결과를 병합하면 되므로), 데이터베이스 쓰기도 느리게 할 수 있어요(각 INSERT, UPDATE, DELETE 문이 자동 병합 과정의 일부로 더 많은 작업을 해야 하므로).

전체 텍스트 인덱스를 구성하는 각 b-tree 는 크기에 기반해 "레벨(level)"에 할당돼요. 레벨-0 b-tree 는 단일 트랜잭션의 내용을 포함하므로 가장 작아요. 더 높은 레벨의 b-tree 는 두 개 이상의 레벨-0 b-tree 를 병합한 결과이므로 더 커요. FTS5 는 같은 레벨의 b-tree 가 M 개 이상 존재하면 b-tree 병합을 시작하는데, 여기서 M 은 'automerge' 매개변수의 값이에요.

'automerge' 매개변수의 최대 허용 값은 16 이에요. 기본값은 4 예요. 'automerge' 매개변수를 0 으로 설정하면 b-tree 의 자동 점진적 병합을 완전히 비활성화해요.

INSERT INTO ft(ft, rank) VALUES('automerge', 8);

6.2. 'crisismerge' 구성 옵션

'crisismerge' 옵션은 'automerge' 와 비슷한데, 전체 텍스트 인덱스를 구성하는 구성 b-tree 들이 어떻게 그리고 얼마나 자주 병합되는지 결정한다는 점이에요. 전체 텍스트 인덱스 안 단일 레벨에 C 개 이상의 b-tree 가 존재하면, 여기서 C 는 'crisismerge' 옵션의 값이에요, 그 레벨의 모든 b-tree 가 단일 b-tree 로 즉시 병합돼요.

이 옵션과 'automerge' 옵션의 차이는 'automerge' 한계에 도달하면 FTS5 가 b-tree 를 병합하기 시작하기만 한다는 점이에요. 대부분의 작업은 후속 INSERT, UPDATE, DELETE 작업의 일부로 수행돼요. 반면 'crisismerge' 한계에 도달하면 문제의 b-tree 들이 모두 즉시 병합돼요. 이는 crisis-merge 를 트리거하는 INSERT, UPDATE, DELETE 가 완료되는 데 오래 걸릴 수 있다는 뜻이에요.

기본 'crisismerge' 값은 16 이에요. 최대 한계는 없어요. 'crisismerge' 매개변수를 0 이나 1 값으로 설정하려는 시도는 기본값(16)으로 설정하는 것과 동등해요. 'crisismerge' 옵션을 음수 값으로 설정하려는 시도는 에러예요.

INSERT INTO ft(ft, rank) VALUES('crisismerge', 16);

6.3. 'delete' 명령

이 명령은 external content 와 contentless 테이블에서만 사용할 수 있어요. 단일 행과 연관된 인덱스 항목을 전체 텍스트 인덱스에서 삭제하는 데 사용돼요. 이 명령과 delete-all 명령이 contentless 테이블의 전체 텍스트 인덱스에서 항목을 제거하는 유일한 방법이에요.

이 명령으로 행을 삭제하려면 테이블과 같은 이름의 특별한 컬럼에 텍스트 값 'delete' 를 삽입해야 해요. 삭제할 행의 rowid 는 rowid 컬럼에 삽입돼요. 다른 컬럼에 삽입된 값은 테이블에 현재 저장된 값과 일치해야 해요. 예를 들어:

-- Insert a row with rowid=14 into the fts5 table.
INSERT INTO ft(rowid, a, b, c) VALUES(14, $a, $b, $c);

-- Remove the same row from the fts5 table.
INSERT INTO ft(ft, rowid, a, b, c) VALUES('delete', 14, $a, $b, $c);

'delete' 명령의 일부로 텍스트 컬럼에 "삽입된" 값이 테이블에 현재 저장된 값과 같지 않으면 결과는 예측할 수 없을 수 있어요.

그 이유는 이해하기 쉽다: 문서가 FTS5 테이블에 삽입될 때, 새 문서 안의 각 토큰의 위치를 기록하는 항목이 전체 텍스트 인덱스에 추가돼요. 문서가 제거될 때, 전체 텍스트 인덱스에서 제거해야 할 항목 집합을 결정하려면 원래 데이터가 필요해요. 그래서 이 명령으로 행을 삭제할 때 FTS5 에 공급되는 데이터가 삽입 시 토큰 인스턴스 집합을 결정하는 데 사용된 것과 다르면, 일부 전체 텍스트 인덱스 항목이 올바르게 삭제되지 않거나, FTS5 가 존재하지 않는 인덱스 항목을 제거하려고 시도할 수 있어요. 이는 전체 텍스트 인덱스를 예측할 수 없는 상태로 남겨, 이후 쿼리 결과를 신뢰할 수 없게 만들 수 있어요.

6.4. 'delete-all' 명령

이 명령은 external content 와 contentless 테이블(contentless-delete 테이블 포함)에서만 사용할 수 있어요. 전체 텍스트 인덱스에서 모든 항목을 삭제해요.

INSERT INTO ft(ft) VALUES('delete-all');

6.5. 'deletemerge' 구성 옵션

'deletemerge' 옵션은 contentless-delete 테이블에서만 사용돼요.

contentless-delete 테이블에서 행이 삭제될 때, 그것의 토큰과 연관된 항목은 FTS 인덱스에서 즉시 제거되지 않아요. 대신 삭제된 행의 rowid 를 포함하는 "묘비(tombstone)" 표시가 그 행의 FTS 인덱스 항목을 포함하는 b-tree 에 첨부돼요. b-tree 가 쿼리될 때 묘비 표시가 존재하는 어떤 쿼리 결과 행도 결과에서 생략돼요. b-tree 가 다른 b-tree 와 병합될 때 삭제된 행과 그 묘비 표시가 모두 버려져요.

이 옵션은 b-tree 가 병합 자격이 되기 전에 그 안의 행에 묘비 표시가 있어야 하는 최소 백분율을 지정해요. 자동 병합이나 명시적 사용자 'merge' 명령 때문에요. 'automerge' 와 'usermerge' 옵션이 결정하는 보통 기준을 충족하지 않더라도요.

예를 들어 FTS5 가 구성 b-tree 의 행 15% 에 묘비 표시가 있으면 병합을 고려하도록 지정하려면:

INSERT INTO ft(ft, rank) VALUES('deletemerge', 15);

이 옵션의 기본값은 10 이에요. 0 보다 작게 설정하려는 시도는 기본값을 복원해요. 이 옵션을 0 이나 100 보다 크게 설정하면 묘비 표시 때문에 b-tree 가 병합 자격이 되는 일이 절대 없도록 보장해요.

6.6. 'insttoken' 구성 옵션

이 부울 옵션은 기본값이 0 이에요. 1 로 설정하면 FTS5 는 테이블에 대해 이루어진 모든 접두어 쿼리에 대해 xInstToken API 가 요구하는 추가 데이터를 수집해요. 자세한 내용은 xInstToken 에 대한 연결된 문서를 참고하세요.

-- Enable collection of extra xInstToken data for prefix queries.
INSERT INTO ft(ft, rank) VALUES('insttoken', 1);

-- Disable collection of extra xInstToken data for prefix queries.
INSERT INTO ft(ft, rank) VALUES('insttoken', 0);

6.7. 'integrity-check' 명령

이 명령은 전체 텍스트 인덱스가 내부적으로 일관된지, 그리고 선택적으로 어떤 external content table 과 일관된지 검증하는 데 사용돼요.

integrity-check 명령은 FTS5 테이블과 같은 이름의 특별한 컬럼에 텍스트 값 'integrity-check' 를 삽입해 호출돼요. "rank" 컬럼에 값이 공급되면 0 이나 1 이어야 해요. 예를 들어:

INSERT INTO ft(ft) VALUES('integrity-check');
INSERT INTO ft(ft, rank) VALUES('integrity-check', 0);
INSERT INTO ft(ft, rank) VALUES('integrity-check', 1);

위 세 형태는 external content 테이블이 아닌 모든 FTS 테이블에 대해 동등해요. 그것들은 인덱스 데이터 구조가 손상되지 않았는지, 그리고 FTS 테이블이 contentless 가 아니면 인덱스의 내용이 테이블 자체의 내용과 일치하는지 확인해요.

external content 테이블의 경우 인덱스의 내용은 rank 컬럼에 지정된 값이 1 일 때만 external content table 의 내용과 비교돼요.

모든 경우에 어떤 불일치가 발견되면 명령은 SQLITE_CORRUPT_VTAB 에러로 실패해요.

6.8. 'merge' 명령

INSERT INTO ft(ft, rank) VALUES('merge', 500);

이 명령은 대략 N 페이지의 병합된 데이터가 데이터베이스에 기록될 때까지 b-tree 구조를 병합해요. 여기서 N 은 'merge' 명령의 일부로 지정된 매개변수의 절대값이에요. 각 페이지의 크기는 FTS5 pgsz 옵션이 구성하는 대로예요.

매개변수가 양수 값이면 B-tree 구조는 다음 중 하나가 참일 때만 병합 자격이 있어요:

  • 단일 레벨에 U 개 이상의 그러한 b-tree 가 있다( U 는 FTS5 usermerge 옵션에 할당된 값)(b-tree 레벨 설명은 FTS5 automerge 옵션 문서 참고).
  • 병합이 이미 시작됐다(아마도 음수 매개변수를 지정한 'merge' 명령에 의해).

'merge' 명령이 병합할 b-tree 를 찾았는지 여부는 명령이 실행되기 전후에 sqlite3_total_changes() API 가 반환하는 값을 확인해 알 수 있어요. 두 값의 차이가 2 이상이면 작업이 수행된 거예요. 차이가 2 미만이면 'merge' 명령은 no-op 이었어요. 이 경우 적어도 FTS 테이블이 다음에 갱신될 때까지는 같은 'merge' 명령을 다시 실행할 이유가 없어요.

매개변수가 음수이고 FTS 인덱스 안의 둘 이상의 레벨에 B-tree 구조가 있으면, 병합 작업이 시작되기 전에 모든 B-tree 구조가 같은 레벨에 할당돼요. 추가로 매개변수가 음수이면 usermerge 구성 옵션의 값은 존중되지 않아요. 같은 레벨의 b-tree 가 2 개만 있어도 병합될 수 있어요.

위는 sqlite3_total_changes() 의 반환 값의 전후 차이가 2 미만이 될 때까지 음수 매개변수로 'merge' 명령을 실행하는 것이 FTS5 optimize 명령과 같은 방식으로 FTS 인덱스를 최적화한다는 뜻이에요. 하지만 이 과정이 진행되는 동안 FTS 인덱스에 새 b-tree 가 추가되면, FTS5 는 새 b-tree 를 기존 b-tree 와 같은 레벨로 옮기고 병합을 다시 시작할 거예요. 이를 피하려면 첫 번째 'merge' 호출에만 음수 매개변수를 지정해야 해요. 각 후속 'merge' 호출은 양수 값을 지정해서 FTS 인덱스에 새 b-tree 가 추가돼도 첫 호출이 시작한 병합이 완료까지 실행되도록 해야 해요.

6.9. 'optimize' 명령

이 명령은 현재 전체 텍스트 인덱스를 구성하는 모든 개별 b-tree 를 단일 큰 b-tree 구조로 병합해요. 이는 전체 텍스트 인덱스가 데이터베이스 안에서 최소 공간을 소비하고 쿼리하기 가장 빠른 형태가 되도록 보장해요.

전체 텍스트 인덱스와 그 구성 b-tree 사이의 관계에 대한 자세한 내용은 FTS5 automerge 옵션의 문서를 참고하세요.

INSERT INTO ft(ft) VALUES('optimize');

전체 FTS 인덱스를 재구성하므로 optimize 명령은 실행하는 데 오래 걸릴 수 있어요. FTS5 merge 명령을 사용해 FTS 인덱스 최적화의 작업을 여러 단계로 나눌 수 있어요. 그렇게 하려면:

  • 매개변수를 -N 으로 설정해 'merge' 명령을 한 번 호출하고, 그런 다음
  • 매개변수를 N 으로 설정해 'merge' 명령을 0 회 이상 호출한다.

여기서 N 은 merge 명령을 호출할 때마다 병합할 데이터 페이지 수예요. 애플리케이션은 merge 명령 전후 sqlite3_total_changes() 함수가 반환하는 값의 차이가 2 미만으로 떨어질 때 merge 호출을 중지해야 해요. merge 명령은 같은 트랜잭션이나 별도 트랜잭션에서, 그리고 같은 또는 다른 데이터베이스 클라이언트에 의해 발행될 수 있어요. 자세한 내용은 merge 명령의 문서를 참고하세요.

6.10. 'pgsz' 구성 옵션

이 명령은 영구적인 "pgsz" 옵션을 설정하는 데 사용돼요.

FTS5 가 유지하는 전체 텍스트 인덱스는 데이터베이스 테이블 안의 고정 크기 blob 시리즈로 저장돼요. 전체 텍스트 인덱스를 구성하는 모든 blob 이 같은 크기일 필요는 엄밀히 없어요. pgsz 옵션은 이후 인덱스 작성자가 만드는 모든 blob 의 크기를 결정해요. 기본값은 4050 이에요.

INSERT INTO ft(ft, rank) VALUES('pgsz', 4072);

6.11. 'rank' 구성 옵션

이 명령은 영구적인 "rank" 옵션을 설정하는 데 사용돼요.

rank 옵션은 rank 컬럼의 기본 보조 함수 매핑을 바꾸는 데 사용돼요. 옵션은 위에서 "rank MATCH ?" 용어에 대해 설명한 것과 같은 형식의 텍스트 값으로 설정되어야 해요. 예를 들어:

INSERT INTO ft(ft, rank) VALUES('rank', 'bm25(10.0, 5.0)');

6.12. 'rebuild' 명령

이 명령은 먼저 전체 텍스트 인덱스 전체를 삭제한 다음 테이블 또는 content table 의 내용에 기반해 다시 만들어요. contentless 테이블에서는 사용할 수 없어요.

INSERT INTO ft(ft) VALUES('rebuild');

6.13. 'secure-delete' 구성 옵션

이 명령은 영구적인 부울 "secure-delete" 옵션을 설정하는 데 사용돼요. 예를 들어:

INSERT INTO ft(ft, rank) VALUES('secure-delete', 1);

보통 fts5 테이블의 항목이 갱신되거나 삭제될 때, 전체 텍스트 인덱스에서 항목을 제거하는 대신 delete-key 가 트랜잭션이 만든 새 b-tree 에 추가돼요. 이것은 효율적이지만, 이전 전체 텍스트 인덱스 항목이 전체 텍스트 인덱스에 대한 병합 작업에 의해 결국 제거될 때까지 데이터베이스 파일에 남아 있다는 뜻이에요. 데이터베이스에 접근할 수 있는 누구든 이 항목을 사용해 삭제된 FTS5 테이블 행의 내용을 쉽게 재구성할 수 있어요. 하지만 'secure-delete' 옵션이 1 로 설정되면, 기존 FTS5 테이블 행이 갱신되거나 삭제될 때 전체 텍스트 항목이 데이터베이스에서 실제로 제거돼요. 이것은 더 느리지만, 이전 전체 텍스트 항목이 삭제된 테이블 행을 재구성하는 데 사용되는 것을 막아요.

이 옵션은 이전 전체 텍스트 항목이 데이터베이스에 대한 SQL 접근이 있는 공격자에게 사용 가능하지 않도록 보장해요. SQLite 데이터베이스 파일 자체에 접근하는 공격자가 복구할 수 없도록 하려면 애플리케이션은 "PRAGMA secure_delete = 1" 같은 명령으로 SQLite 코어 secure-delete 옵션도 활성화해야 해요.

경고: 이 옵션이 설정된 상태에서 하나 이상의 테이블 행이 갱신되거나 삭제된 후에는, 3.42.0(이 옵션이 처음 사용 가능해진 버전)보다 이른 어떤 FTS5 버전도 FTS5 테이블을 읽거나 쓸 수 없어요. 그렇게 시도하면 "invalid fts5 file format (found 5, expected 4) - run 'rebuild'" 같은 에러 메시지와 함께 에러가 발생해요. 버전 3.42.0 이상을 사용해 테이블에서 'rebuild' 명령을 실행하면 FTS5 파일 형식이 되돌려져 이전 FTS5 버전이 읽을 수 있게 될 수 있어요.

secure-delete 옵션의 기본값은 0 이에요.

6.14. 'usermerge' 구성 옵션

이 명령은 영구적인 "usermerge" 옵션을 설정하는 데 사용돼요.

usermerge 옵션은 automerge 와 crisismerge 옵션과 비슷해요. 양수 매개변수의 'merge' 명령이 함께 병합할 최소 b-tree 세그먼트 수예요. 예를 들어:

INSERT INTO ft(ft, rank) VALUES('usermerge', 4);

usermerge 옵션의 기본값은 4 예요. 최소 허용 값은 2, 최대는 16 이에요.

7. FTS5 확장

FTS5 는 다음으로 확장될 수 있게 해주는 API 를 제공해요:

  • C 로 구현된 새 보조 함수 추가, 그리고
  • 역시 C 로 구현된 새 토크나이저 추가.

이 문서에서 설명하는 내장 토크나이저와 보조 함수는 모두 아래에 설명된 공개적으로 사용 가능한 API 로 구현돼요.

새 보조 함수나 토크나이저 구현을 FTS5 에 등록하기 전에 애플리케이션은 "fts5_api" 구조체에 대한 포인터를 얻어야 해요. FTS5 확장이 등록된 각 데이터베이스 연결마다 하나의 fts5_api 구조체가 있어요. 포인터를 얻기 위해 애플리케이션은 단일 인자로 SQL 사용자 정의 함수 fts5() 를 호출해요. 그 인자는 sqlite3_bind_pointer() 인터페이스를 사용해 fts5_api 객체에 대한 포인터의 포인터로 설정되어야 해요. 다음 예제 코드는 그 기법을 보여줘요:

/*
** Return a pointer to the fts5_api pointer for database connection db.
** If an error occurs, return NULL and leave an error in the database
** handle (accessible using sqlite3_errcode()/errmsg()).
*/
fts5_api *fts5_api_from_db(sqlite3 *db){
  fts5_api *pRet = 0;
  sqlite3_stmt *pStmt = 0;

  if( SQLITE_OK==sqlite3_prepare(db, "SELECT fts5(?1)", -1, &pStmt, 0) ){
    sqlite3_bind_pointer(pStmt, 1, (void*)&pRet, "fts5_api_ptr", NULL);
    sqlite3_step(pStmt);
  }
  sqlite3_finalize(pStmt);
  return pRet;
}

하위 호환성 경고: SQLite 버전 3.20.0 (2017-08-01) 이전에는 fts5() 가 약간 다르게 작동했어요. FTS5 를 확장하는 이전 애플리케이션은 위에 보여준 새 기법을 사용하도록 수정되어야 해요.

fts5_api 구조체는 다음과 같이 정의돼요. 다섯 개의 메서드를 노출해요:

  • xCreateTokenizer() 와 xCreateTokenizer_v2(), 새 사용자 정의 토크나이저 구현을 등록하기 위한 것.
  • xFindTokenizer() 와 xFindTokenizer_v2(), 기존 토크나이저 구현을 검색하기 위한 것. 이는 내장 porter 토크나이저와 유사한 "토크나이저 래퍼" 를 구현하는 데 유용할 수 있어요.
  • xCreateFunction(), 새 보조 함수 구현을 등록하기 위한 것.

위의 두 "v2" 메서드는 fts5_api.iVersion 필드가 3 이상으로 설정된 경우에만 사용할 수 있어요. iVersion 에 대해 더 낮은 값을 가진 fts5_api 객체를 통해 "v2" API 에 접근하려는 시도는 정의되지 않은 동작을 초래해요.

typedef struct fts5_api fts5_api;
struct fts5_api {
  int iVersion;                   /* Currently always set to 3 */

  /* Create a new tokenizer */
  int (*xCreateTokenizer)(
    fts5_api *pApi,
    const char *zName,
    void *pUserData,
    fts5_tokenizer *pTokenizer,
    void (*xDestroy)(void*)
  );

  /* Find an existing tokenizer */
  int (*xFindTokenizer)(
    fts5_api *pApi,
    const char *zName,
    void **ppUserData,
    fts5_tokenizer *pTokenizer
  );

  /* Create a new auxiliary function */
  int (*xCreateFunction)(
    fts5_api *pApi,
    const char *zName,
    void *pUserData,
    fts5_extension_function xFunction,
    void (*xDestroy)(void*)
  );

  /* APIs below this point are only available if iVersion>=3 */

  /* Create a new tokenizer */
  int (*xCreateTokenizer_v2)(
    fts5_api *pApi,
    const char *zName,
    void *pUserData,
    fts5_tokenizer_v2 *pTokenizer,
    void (*xDestroy)(void*)
  );

  /* Find an existing tokenizer */
  int (*xFindTokenizer_v2)(
    fts5_api *pApi,
    const char *zName,
    void **ppUserData,
    fts5_tokenizer_v2 **ppTokenizer
  );
};

fts5_api 객체의 메서드를 호출하려면 fts5_api 포인터 자체를 메서드의 첫 인자로 전달한 다음 다른 메서드별 인자를 전달해야 해요. 예를 들어:

rc = pFts5Api->xCreateTokenizer(pFts5Api, ... other args ...);

fts5_api 구조체 메서드는 다음 섹션에서 개별적으로 설명돼요.

7.1. 사용자 정의 토크나이저

사용자 정의 토크나이저를 만들려면 애플리케이션이 세 가지 함수를 구현해야 해요. 토크나이저 생성자(xCreate), 소멸자(xDelete), 실제 토큰화를 하는 함수(xTokenize). 각 함수의 타입은 fts5_tokenizer_v2 구조체의 멤버 변수와 같아요:

typedef struct Fts5Tokenizer Fts5Tokenizer;
typedef struct fts5_tokenizer_v2 fts5_tokenizer_v2;
struct fts5_tokenizer_v2 {
  int iVersion;             /* Currently always 2 */

  int (*xCreate)(void*, const char **azArg, int nArg, Fts5Tokenizer **ppOut);
  void (*xDelete)(Fts5Tokenizer*);
  int (*xTokenize)(Fts5Tokenizer*,
      void *pCtx,
      int flags,            /* Mask of FTS5_TOKENIZE_* flags */
      const char *pText, int nText,
      const char *pLocale, int nLocale,
      int (*xToken)(
        void *pCtx,         /* Copy of 2nd argument to xTokenize() */
        int tflags,         /* Mask of FTS5_TOKEN_* flags */
        const char *pToken, /* Pointer to buffer containing token */
        int nToken,         /* Size of token in bytes */
        int iStart,         /* Byte offset of token within input text */
        int iEnd            /* Byte offset of end of token within input text */
      )
  );
};

/* Flags that may be passed as the third argument to xTokenize() */
#define FTS5_TOKENIZE_QUERY     0x0001
#define FTS5_TOKENIZE_PREFIX    0x0002
#define FTS5_TOKENIZE_DOCUMENT  0x0004
#define FTS5_TOKENIZE_AUX       0x0008

/* Flags that may be passed by the tokenizer implementation back to FTS5
** as the third argument to the supplied xToken callback. */
#define FTS5_TOKEN_COLOCATED    0x0001      /* Same position as prev. token */

구현은 fts5_tokenizer_v2 구조체의 인스턴스를 채우고 그것에 대한 포인터를 fts5_api 객체의 xCreateTokenizer_v2() 메서드에 전달해 FTS5 모듈에 등록돼요. 이미 같은 이름의 토크나이저가 있으면 대체돼요. xCreateTokenizer() 에 NULL 이 아닌 xDestroy 매개변수가 전달되면, 데이터베이스 핸들이 닫힐 때나 토크나이저가 대체될 때 유일한 인자로 전달된 pUserData 포인터의 복사본으로 호출돼요.

성공하면 xCreateTokenizer() 는 SQLITE_OK 를 반환해요. 그렇지 않으면 SQLite 에러 코드를 반환해요. 이 경우 xDestroy 함수는 호출되지 않아요.

FTS5 테이블이 사용자 정의 토크나이저를 사용할 때 FTS5 코어는 xCreate() 를 한 번 호출해 토크나이저를 만들고, 문자열을 토큰화하기 위해 xTokenize() 를 0 회 이상 호출하며, xCreate() 가 할당한 자원을 해제하기 위해 xDelete() 를 호출해요. 더 구체적으로:

xCreate:

이 함수는 토크나이저 인스턴스를 할당하고 초기화하는 데 사용돼요. 실제로 텍스트를 토큰화하려면 토크나이저 인스턴스가 필요해요.

이 함수에 전달되는 첫 인자는 fts5_tokenizer_v2 객체가 FTS5 에 등록될 때 애플리케이션이 제공한 (void*) 포인터의 복사본이에요(xCreateTokenizer() 에 세 번째 인자). 두 번째와 세 번째 인자는 FTS5 테이블을 만드는 데 사용된 CREATE VIRTUAL TABLE 문의 일부로 토크나이저 이름 다음에 지정된 토크나이저 인자(있다면)를 포함하는 nul-종결 문자열 배열이에요.

마지막 인자는 출력 변수예요. 성공하면 (*ppOut) 이 새 토크나이저 핸들을 가리키도록 설정되고 SQLITE_OK 가 반환돼야 해요. 에러가 발생하면 SQLITE_OK 이외의 어떤 값이 반환되어야 해요. 이 경우 fts5 는 *ppOut 의 최종 값이 정의되지 않았다고 가정해요.

xDelete:

이 함수는 이전에 xCreate() 를 사용해 할당된 토크나이저 핸들을 삭제하기 위해 호출돼요. Fts5 는 xCreate() 에 대한 각 성공적인 호출마다 이 함수가 정확히 한 번 호출될 것을 보장해요.

xTokenize:

이 함수는 인자 pText 가 나타내는 nText 바이트 문자열을 토큰화할 것으로 기대돼요. pText 는 nul-종결일 수도 아닐 수도 있어요. 이 함수에 전달되는 첫 인자는 이전 xCreate() 호출이 반환한 Fts5Tokenizer 객체에 대한 포인터예요.

세 번째 인자는 FTS5 가 공급된 텍스트의 토큰화를 요청하는 이유를 나타내요. 이것은 항상 다음 네 값 중 하나예요:

  • FTS5_TOKENIZE_DOCUMENT - 문서가 FTS 테이블에 삽입되거나 제거되고 있어요. 토크나이저는 FTS 인덱스에 추가(또는 삭제)할 토큰 집합을 결정하기 위해 호출되고 있어요.
  • FTS5_TOKENIZE_QUERY - FTS 인덱스에 대해 MATCH 쿼리가 실행되고 있어요. 토크나이저는 쿼리의 일부로 지정된 bareword 나 따옴표로 묶인 문자열을 토큰화하기 위해 호출되고 있어요.
  • (FTS5_TOKENIZE_QUERY | FTS5_TOKENIZE_PREFIX) - FTS5_TOKENIZE_QUERY 와 같지만, bareword 나 따옴표로 묶인 문자열 뒤에 "*" 문자, 즉 토크나이저가 반환한 마지막 토큰이 토큰 접두어로 취급될 것임을 나타내는 문자가 따라와요.
  • FTS5_TOKENIZE_AUX - 토크나이저는 보조 함수가 만든 fts5_api.xTokenize() 요청을 충족시키기 위해 호출되고 있어요. 또는 같은 것이 columnsize=0 데이터베이스에서 만든 fts5_api.xColumnSize() 요청을 충족시키기 위해.

xTokenize() 에 전달되는 여섯 번째와 일곱 번째 인자 - pLocale 과 nLocale - 은 각각 토큰화에 사용할 로케일(예: "en_US")을 포함하는 버퍼에 대한 포인터와 그 바이트 크기예요. pLocale 버퍼는 nul-종결되지 않아요. pLocale 에 NULL 이 전달될 수 있는데(nLocale 은 항상 0), 토크나이저가 기본 로케일을 사용해야 함을 나타내기 위해서예요.

입력 문자열의 각 토큰에 대해 공급된 콜백 xToken() 이 호출되어야 해요. 그것의 첫 인자는 xTokenize() 의 두 번째 인자로 전달된 포인터의 복사본이어야 해요. 세 번째와 네 번째 인자는 토큰 텍스트를 포함하는 버퍼에 대한 포인터와 토큰의 바이트 크기예요. 네 번째와 다섯 번째 인자는 입력 안에서 토큰이 파생된 텍스트의 첫 바이트 및 첫 바이트 바로 다음의 바이트 오프셋이에요.

xToken() 콜백에 전달되는 두 번째 인자("tflags")는 보통 0 으로 설정되어야 해요. 예외는 토크나이저가 동의어(synonyms)를 지원할 때예요. 이 경우 아래 논의를 참고하세요.

FTS5 는 xToken() 콜백이 입력 텍스트 안에서 발생하는 순서대로 각 토큰에 대해 호출된다고 가정해요.

xToken() 콜백이 SQLITE_OK 이외의 어떤 값을 반환하면 토큰화를 중단하고 xTokenize() 메서드가 xToken() 반환 값의 복사본을 즉시 반환해야 해요. 또는 입력 버퍼가 소진되면 xTokenize() 는 SQLITE_OK 를 반환해야 해요. 마지막으로 xTokenize() 구현 자체에 에러가 발생하면 토큰화를 중단하고 SQLITE_OK 나 SQLITE_DONE 이외의 어떤 에러 코드라도 반환할 수 있어요.

토크나이저가 fts5_tokenizer_v2 객체로 등록되면 xTokenize() 메서드에 두 개의 추가 인자 - pLocale 과 nLocale - 가 있어요. 이것들은 토크나이저가 현재 요청에 사용해야 할 로케일을 지정해요. pLocale 과 nLocale 이 둘 다 0 이면 토크나이저는 기본 로케일을 사용해야 해요. 그렇지 않으면 pLocale 은 사용할 로케일 이름을 utf-8 텍스트로 포함하는 nLocale 바이트 버퍼를 가리켜요. pLocale 은 nul-종결되지 않아요.

또한 fts5_tokenizer 객체가 있어요. 이것은 더 오래되고 더 이상 쓰이지 않는 fts5_tokenizer_v2 버전이에요. 다음을 제외하면 비슷해요:

  • "iVersion" 필드가 없고,
  • xTokenize() 메서드가 로케일 인자를 받지 않는다.

레거시 fts5_tokenizer 토크나이저는 xCreateTokenizer_v2() 대신 레거시 xCreateTokenizer() 함수를 사용해 등록되어야 해요.

두 API 중 하나로 등록된 토크나이저 구현은 xFindTokenizer() 와 xFindTokenizer_v2() 둘 다로 검색될 수 있어요.

7.1.1. 동의어 지원

사용자 정의 토크나이저는 동의어도 지원할 수 있어요. 사용자가 "first place" 같은 구문을 쿼리하려는 경우를 생각해보세요. 내장 토크나이저를 사용하면 FTS5 쿼리 'first + place' 는 문서 집합 안의 "first place" 인스턴스와 일치하지만 "1st place" 같은 대체 형태와는 일치하지 않아요. 일부 애플리케이션에서는 사용자가 MATCH 쿼리 텍스트에 지정한 형태와 무관하게 "first place" 또는 "1st place" 의 모든 인스턴스를 일치시키는 것이 더 나을 거예요.

FTS5 에서 이 문제에 접근하는 방법은 여러 가지가 있어요:

  • 모든 동의어를 단일 토큰에 매핑한다. 이 경우 위 예를 사용하면 토크나이저가 입력 "first" 와 "1st" 에 대해 같은 토큰을 반환한다는 뜻이에요. 그 토큰이 실제로 "first" 라고 하면, 사용자가 문서 "I won 1st place" 를 삽입할 때 토큰 "i", "won", "first", "place" 에 대한 항목이 인덱스에 추가돼요. 사용자가 '1st + place' 를 쿼리하면 토크나이저가 "first" 를 "1st" 에 대체하고 쿼리가 예상대로 작동해요.
  • 문서 안의 각 쿼리 용어의 모든 동의어에 대해 인덱스를 별도로 쿼리한다. 이 경우 쿼리 텍스트를 토큰화할 때 토크나이저가 문서 안의 단일 용어에 대해 여러 동의어를 제공할 수 있어요. 그러면 FTS5 는 각 동의어에 대해 인덱스를 개별적으로 쿼리해요. 예를 들어 쿼리:
... MATCH 'first place'

에 직면하면 토크나이저는 MATCH 쿼리의 첫 토큰에 대한 동의어로 "1st" 와 "first" 를 모두 제공하고 FTS5 는 효과적으로 다음과 같은 쿼리를 실행해요:

... MATCH '(first OR 1st) place'

단, 보조 함수의 목적상 쿼리는 여전히 두 개의 구문만을 포함하는 것처럼 보여요. "(first OR 1st)" 가 단일 구문으로 취급돼요.

  • 단일 용어에 대한 여러 동의어를 FTS 인덱스에 추가한다. 이 방법을 사용하면 문서 텍스트를 토큰화할 때 토크나이저가 각 토큰에 대한 여러 동의어를 제공해요. 그래서 "I won first place" 같은 문서가 토큰화될 때 "i", "won", "first", "1st", "place" 에 대한 항목이 FTS 인덱스에 추가돼요.

이렇게 하면 토크나이저가 쿼리 텍스트를 토큰화할 때 동의어를 제공하지 않더라도(그래서는 안 되며 - 그렇게 하면 비효율적일 거예요), 사용자가 'first + place' 나 '1st + place' 를 쿼리하는지는 중요하지 않아요. FTS 인덱스에 첫 토큰의 두 형태에 해당하는 항목이 있기 때문이에요.

문서나 쿼리 텍스트를 파싱하든, FTS5_TOKEN_COLOCATED 비트가 설정된 tflags 인자를 지정하는 xToken 호출은 이전 토큰에 대한 동의어를 공급하는 것으로 간주돼요. 예를 들어 문서 "I won first place" 를 파싱할 때 동의어를 지원하는 토크나이저는 xToken() 을 5 번 호출할 거예요:

xToken(pCtx, 0, "i",                      1,  0,  1);
xToken(pCtx, 0, "won",                    3,  2,  5);
xToken(pCtx, 0, "first",                  5,  6, 11);
xToken(pCtx, FTS5_TOKEN_COLOCATED, "1st", 3,  6, 11);
xToken(pCtx, 0, "place",                  5, 12, 17);

xToken() 이 처음 호출될 때 FTS5_TOKEN_COLOCATED 플래그를 지정하는 것은 에러예요. 단일 토큰에 대해 여러 동의어는 FTS5_TOKEN_COLOCATED 로 xToken(FTS5_TOKEN_COLOCATED) 을 연속으로 여러 번 호출해 지정될 수 있어요. 단일 토큰에 제공될 수 있는 동의어 수에는 제한이 없어요.

많은 경우 위 방법 (1) 이 최선의 접근이에요. FTS 인덱스에 추가 데이터를 추가하거나 FTS5 가 여러 용어를 쿼리하도록 요구하지 않으므로 디스크 공간과 쿼리 속도 면에서 효율적이에요. 하지만 접두어 쿼리를 잘 지원하지는 않아요. 위에서 제안한 대로 토크나이저가 "1st" 에 대해 토큰 "first" 를 대체하면 쿼리:

... MATCH '1s*'

는 토큰 "1st" 를 포함하는 문서와 일치하지 않을 거예요(토크나이저가 "1s" 를 "first" 의 어떤 접두어에도 매핑하지 않을 것이므로).

완전한 접두어 지원을 위해서는 방법 (3) 이 선호될 수 있어요. 이 경우 인덱스가 "first" 와 "1st" 둘 다에 대한 항목을 포함하므로 'fi*' 나 '1s*' 같은 접두어 쿼리가 올바르게 일치할 거예요. 하지만 FTS 인덱스에 추가 항목이 더해지므로 이 방법은 데이터베이스 안에서 더 많은 공간을 사용해요.

방법 (2) 는 (1) 과 (3) 사이의 중간점을 제공해요. 이 방법을 사용하면 '1s*' 같은 쿼리가 리터럴 토큰 "1st" 를 포함하는 문서와는 일치하지만 "first" 와는 일치하지 않을 거예요(토크나이저가 접두어에 대한 동의어를 제공할 수 없다고 가정). 하지만 '1st' 같은 비접두어 쿼리는 "1st" 와 "first" 둘 다에 대해 일치할 거예요. 이 방법은 FTS 인덱스에 추가 항목이 더해지지 않으므로 추가 디스크 공간을 요구하지 않아요. 반면 각 동의어에 대해 FTS 인덱스의 별도 쿼리가 필요하므로 MATCH 쿼리를 실행하는 데 더 많은 CPU 사이클이 필요할 수 있어요.

방법 (2) 나 (3) 을 사용할 때 토크나이저가 문서 텍스트를 토큰화할 때(방법 3)나 쿼리 텍스트를 토큰화할 때(방법 2)에만 동의어를 제공하고, 둘 다 제공하지 않는 것이 중요해요. 그렇게 해도 어떤 에러도 일으키지 않지만 비효율적이에요.

7.2. 사용자 정의 보조 함수

사용자 정의 보조 함수를 구현하는 것은 스칼라 SQL 함수를 구현하는 것과 비슷해요. 구현은 fts5_extension_function 타입의 C 함수여야 하며, 다음과 같이 정의돼요:

typedef struct Fts5ExtensionApi Fts5ExtensionApi;
typedef struct Fts5Context Fts5Context;
typedef struct Fts5PhraseIter Fts5PhraseIter;

typedef void (*fts5_extension_function)(
  const Fts5ExtensionApi *pApi,   /* API offered by current FTS version */
  Fts5Context *pFts,              /* First arg to pass to pApi functions */
  sqlite3_context *pCtx,          /* Context for returning result/error */
  int nVal,                       /* Number of values in apVal[] array */
  sqlite3_value **apVal           /* Array of trailing arguments */
);

구현은 fts5_api 객체의 xCreateFunction() 메서드를 호출해 FTS5 모듈에 등록돼요. 이미 같은 이름의 보조 함수가 있으면 새 함수로 대체돼요. xCreateFunction() 에 NULL 이 아닌 xDestroy 매개변수가 전달되면, 데이터베이스 핸들이 닫힐 때나 등록된 보조 함수가 대체될 때 유일한 인자로 전달된 pUserData 포인터의 복사본으로 호출돼요.

성공하면 xCreateFunction() 은 SQLITE_OK 를 반환해요. 그렇지 않으면 SQLite 에러 코드를 반환해요. 이 경우 xDestroy 함수는 호출되지 않아요.

보조 함수 콜백에 전달되는 마지막 세 인자(pCtx, nVal, apVal)는 스칼라 SQL 함수의 구현에 전달되는 세 인자와 비슷해요. apVal[] 배열은 보조 함수에 전달된 첫 번째 인자를 제외한 모든 SQL 인자를 포함해요. 구현은 내용 핸들 pCtx 를 통해 결과나 에러를 반환해야 해요.

보조 함수 콜백에 전달되는 첫 인자는 현재 쿼리나 행에 대한 정보를 얻기 위해 호출될 수 있는 메서드를 포함하는 구조체(pApi)에 대한 포인터예요. 두 번째 인자는 그러한 메서드 호출의 첫 인자로 전달되어야 하는 불투명 핸들(pFts)이에요. 예를 들어 다음 보조 함수는 현재 행의 모든 컬럼 안의 총 토큰 수를 반환해요:

/*
** Implementation of an auxiliary function that returns the number
** of tokens in the current row (including all columns).
*/
static void column_size_imp(
  const Fts5ExtensionApi *pApi,
  Fts5Context *pFts,
  sqlite3_context *pCtx,
  int nVal,
  sqlite3_value **apVal
){
  int rc;
  int nToken;
  rc = pApi->xColumnSize(pFts, -1, &nToken);
  if( rc==SQLITE_OK ){
    sqlite3_result_int(pCtx, nToken);
  }else{
    sqlite3_result_error_code(pCtx, rc);
  }
}

다음 섹션은 보조 함수 구현에 제공되는 API 를 자세히 설명해요. 더 많은 예는 소스 코드의 "fts5_aux.c" 파일에서 찾을 수 있어요.

7.2.1. 사용자 정의 보조 함수 API 개요

이 섹션은 보조 함수 API 의 능력에 대한 개요를 제공해요. 모든 함수를 설명하지는 않아요. 완전한 설명은 아래 참조 텍스트를 보세요.

호출될 때 보조 함수 구현은 FTS5 에 다양한 정보를 쿼리할 수 있게 해주는 API 에 접근할 수 있어요. 이 API 중 일부는 방문 중인 FTS5 테이블의 현재 행에 관련된 정보를 반환하고, 일부는 FTS5 쿼리가 방문할 전체 행 집합에 관련되고, 일부는 FTS5 테이블에 관련돼요. 다음과 같이 채워진 FTS5 테이블이 주어지면:

CREATE VIRTUAL TABLE ft USING fts5(a, b);
INSERT INTO ft(rowid, a, b) VALUES
        (1, 'ab cd', 'cd de one'),
        (2, 'de fg', 'fg gh'),
        (3, 'gh ij', 'ij ab three four');

그리고 쿼리:

SELECT my_aux_function(ft) FROM ft('ab')

그러면 사용자 정의 보조 함수는 행 1 과 3(토큰 "ab" 를 포함하고 따라서 쿼리와 일치하는 모든 행)에 대해 호출될 거예요.

테이블의 행/컬럼 수: xRowCount, xColumnCount

시스템은 xRowCount API 를 사용해 FTS5 테이블의 총 행 수를 쿼리할 수 있어요. 이것은 현재 쿼리와 일치하는 수가 아니라 테이블의 총 행 수를 제공해요.

테이블 컬럼은 왼쪽에서 오른쪽으로 0 부터 번호가 매겨져요. "rowid" 컬럼은 세지 않아요. 사용자 선언 컬럼만 세요. 그래서 위 예에서 컬럼 "a" 는 컬럼 0 이고 컬럼 "b" 는 컬럼 1 이에요. 보조 함수 구현 안에서 xColumnCount API 를 사용해 쿼리되는 테이블이 몇 개의 컬럼을 가지는지 결정할 수 있어요. 위 예에서 보조 함수 my_aux_function 의 구현 안에서 xColumnCount() API 가 호출되면 2 를 반환해요.

현재 행의 데이터: xColumnText, xRowid

xRowid API 는 현재 행의 rowid 값을 찾는 데 사용될 수 있어요. xColumnText 는 현재 행의 지정된 컬럼에 저장된 텍스트를 얻는 데 사용될 수 있어요.

토큰 수: xColumnSize, xColumnTotalSize

FTS5 는 fts5 테이블에 삽입된 문서를 토큰으로 나눠요. 이것들은 보통 단지 단어로, 아마도 대문자나 소문자로 접히고 문장 부호가 제거된 것이에요. 예를 들어 기본 unicode61 토크나이저는 텍스트 "The tokenizer is case-insensitive" 를 5 개 토큰 목록 - "the", "tokenizer", "is", "case", "insensitive" 로 토큰화해요. 텍스트에서 토큰이 정확히 어떻게 추출되는지는 토크나이저가 결정해요.

보조 함수 API 는 현재 행의 지정된 컬럼 안의 토큰 수(xColumnSize API)를 쿼리하는 함수와, 테이블의 모든 행의 지정된 컬럼 안의 토큰 수(xColumnTotalSize API)를 쿼리하는 함수를 제공해요. 이 섹션 맨 위의 예에서 행 1 을 방문할 때 xColumnSize 는 컬럼 0 에 대해 2, 컬럼 1 에 대해 3 을 반환해요. xColumnTotalSize 는 현재 행과 무관하게 컬럼 0 에 대해 6, 컬럼 1 에 대해 9 를 반환해요.

현재 전체 텍스트 쿼리: xPhraseCount, xPhraseSize, xQueryToken

FTS5 쿼리는 하나 이상의 구문을 포함해요. xPhraseCount, xPhraseSize, xQueryToken API 는 보조 함수 구현이 현재 쿼리의 세부 사항을 시스템에 쿼리할 수 있게 해줘요. xPhraseCount API 는 현재 쿼리의 구문 수를 반환해요. 예를 들어 FTS5 테이블이 다음과 같이 쿼리되면:

SELECT my_aux_function(ft) FROM ft('ab AND "cd ef gh" OR ij + kl')

그리고 보조 함수의 구현 안에서 xPhraseCount() API 가 호출되면 3 을 반환해요(세 구문은 "ab", "ce ef gh", "ij kl").

구문은 쿼리 안에서 나타나는 순서대로 0 부터 번호가 매겨져요. xPhraseSize() API 는 쿼리의 지정된 구문 안의 토큰 수를 쿼리하는 데 사용될 수 있어요. 위 예에서 구문 0 은 1 토큰, 구문 1 은 3 토큰, 구문 2 는 2 토큰을 포함해요.

xQueryToken API 는 쿼리의 지정된 구문 안의 지정된 토큰의 텍스트에 접근하는 데 사용될 수 있어요. 토큰은 구문 안에서 왼쪽에서 오른쪽으로 0 부터 번호가 매겨져요. 예를 들어 xQueryToken API 를 사용해 위 예의 구문 2 의 토큰 1 을 요청하면 "kl" 텍스트를 반환해요. 구문 0 의 토큰 0 은 "ab" 예요.

현재 행의 구문 히트: xPhraseFirst, xPhraseNext

이 두 API 함수는 현재 행 안의 쿼리의 지정된 구문에 대한 일치를 반복하는 데 사용될 수 있어요. 구문 일치는 현재 행 안의 컬럼과 토큰 오프셋으로 식별돼요. 예를 들어 다음 예제 테이블을 보세요:

CREATE VIRTUAL TABLE ft2 USING fts5(x, y);
INSERT INTO ft2(rowid, x, y) VALUES
        (1, 'xxx one two xxx five xxx six', 'seven four'),
        (2, 'five four four xxx six', 'three four five six four five six');

가 다음과 같이 쿼리된다고 하면:

SELECT my_aux_function(ft2) FROM ft2(
    '("one two" OR "three") AND y:four NEAR(five six, 2)'
);

위 쿼리는 5 개의 구문 - "one two", "three", "four", "five", "six" 를 포함해요. 그것은 테이블의 모든 행과 일치하므로 보조 함수는 각 행에 대해 호출돼요.

행 1 에서 구문 0, "one two" 에 대해 반복할 일치가 정확히 하나 있어요. 컬럼 0 토큰 오프셋 1 에서요. 컬럼 번호는 0 인데, 일치가 가장 왼쪽 컬럼에 나타나기 때문이에요. 토큰 오프셋은 1 인데, 컬럼 값에서 구문 일치 앞에 정확히 하나의 토큰("xxx")이 있기 때문이에요. 구문 1, "three" 에 대해서는 일치가 없어요. 구문 2, "four" 는 컬럼 1 토큰 오프셋 0 에 하나의 일치가 있어요. 구문 3, "five" 는 컬럼 0 토큰 오프셋 4 에 하나의 일치가 있고, 구문 4, "six" 는 컬럼 0 토큰 오프셋 6 에 하나의 일치가 있어요.

예제의 각 행의 각 구문에 대한 일치 집합은 아래 표에 제시돼요. 각 일치는 (column-number, token-offset) 로 표기돼요:

Row Phrase 0 Phrase 1 Phrase 2 Phrase 3 Phrase 4
1 (0, 1) (1, 1) (0, 4) (0, 6)
2 (1,0) (1, 1), (1,4) (1, 2), (1, 5) (1, 3), (1, 6)

두 번째 행은 약간 더 복잡해요. 구문 0 의 발생은 없었어요. 구문 1("three") 이 컬럼 1 토큰 오프셋 0 에 한 번 나타나요. 구문 2("four") 의 인스턴스가 컬럼 0 에 있지만, 구문 4 에 "y:" 컬럼 필터가 있으므로 API 가 그 중 어느 것도 보고하지 않아요. 컬럼 필터로 걸러진 일치는 세지 않아요. 마찬가지로 구문 3 과 4 가 행 2 의 컬럼 "x" 에 실제로 발생하지만, NEAR 필터로 걸러져요. NEAR 필터로 걸러진 일치도 세지 않아요.

현재 행의 구문 히트 (2): xInstCount, xInst

xInstCount 와 xInst API 는 위에서 설명한 xPhraseFirst 와 xPhraseNext 와 같은 정보에 접근을 제공해요. 차이는 단일 지정된 구문에 대한 일치를 반복하는 대신, xInstCount/xInst API 가 모든 일치를 현재 행 안에서 발생 순서로 정렬된 단일 평면 배열로 수집한다는 점이에요. 그러면 이 배열의 요소에 무작위로 접근할 수 있어요.

각 배열 요소는 세 값으로 구성돼요:

  • 구문 번호,
  • 컬럼 번호, 그리고
  • 토큰 오프셋.

위 xPhraseFirst/xPhraseNext 에서 사용한 것과 같은 예제 데이터와 쿼리를 사용하면, xInstCount/xInst 를 통해 접근 가능한 배열은 각 행에 대해 다음 항목으로 구성돼요:

Row xInstCount/xInst array
1 (0, 0, 1), (3, 0, 4), (4, 0, 6), (2, 1, 1)
2 (1, 1, 0), (2, 1, 1), (3, 1, 2), (4, 1, 3), (2, 1, 4), (3, 1, 5), (4, 1, 6)

배열의 각 항목을 구문 일치(phrase match)라고 불러요. 구문 일치는 0 부터 순서대로 번호가 매겨져요. 그래서 위 예에서 행 2 의 구문 일치 3 은 (4, 1, 3) 이에요. 쿼리의 구문 4 가 컬럼 1 토큰 오프셋 3 에서 일치해요.

7.2.2. 사용자 정의 보조 함수 API 참조
struct Fts5ExtensionApi {
  int iVersion;                   /* Currently always set to 4 */

  void *(*xUserData)(Fts5Context*);

  int (*xColumnCount)(Fts5Context*);
  int (*xRowCount)(Fts5Context*, sqlite3_int64 *pnRow);
  int (*xColumnTotalSize)(Fts5Context*, int iCol, sqlite3_int64 *pnToken);

  int (*xTokenize)(Fts5Context*,
    const char *pText, int nText, /* Text to tokenize */
    void *pCtx,                   /* Context passed to xToken() */
    int (*xToken)(void*, int, const char*, int, int, int)       /* Callback */
  );

  int (*xPhraseCount)(Fts5Context*);
  int (*xPhraseSize)(Fts5Context*, int iPhrase);

  int (*xInstCount)(Fts5Context*, int *pnInst);
  int (*xInst)(Fts5Context*, int iIdx, int *piPhrase, int *piCol, int *piOff);

  sqlite3_int64 (*xRowid)(Fts5Context*);
  int (*xColumnText)(Fts5Context*, int iCol, const char **pz, int *pn);
  int (*xColumnSize)(Fts5Context*, int iCol, int *pnToken);

  int (*xQueryPhrase)(Fts5Context*, int iPhrase, void *pUserData,
    int(*)(const Fts5ExtensionApi*,Fts5Context*,void*)
  );
  int (*xSetAuxdata)(Fts5Context*, void *pAux, void(*xDelete)(void*));
  void *(*xGetAuxdata)(Fts5Context*, int bClear);

  int (*xPhraseFirst)(Fts5Context*, int iPhrase, Fts5PhraseIter*, int*, int*);
  void (*xPhraseNext)(Fts5Context*, Fts5PhraseIter*, int *piCol, int *piOff);

  int (*xPhraseFirstColumn)(Fts5Context*, int iPhrase, Fts5PhraseIter*, int*);
  void (*xPhraseNextColumn)(Fts5Context*, Fts5PhraseIter*, int *piCol);

  /* Below this point are iVersion>=3 only */
  int (*xQueryToken)(Fts5Context*,
      int iPhrase, int iToken,
      const char **ppToken, int *pnToken
  );
  int (*xInstToken)(Fts5Context*, int iIdx, int iToken, const char**, int*);

  /* Below this point are iVersion>=4 only */
  int (*xColumnLocale)(Fts5Context*, int iCol, const char **pz, int *pn);
  int (*xTokenize_v2)(Fts5Context*,
    const char *pText, int nText,      /* Text to tokenize */
    const char *pLocale, int nLocale,  /* Locale to pass to tokenizer */
    void *pCtx,                        /* Context passed to xToken() */
    int (*xToken)(void*, int, const char*, int, int, int)       /* Callback */
  );
};

*void (xUserData)(Fts5Context)

확장 함수가 등록될 때 xCreateFunction() API 에 전달된 pUserData 포인터의 복사본을 반환해요.

*int (xColumnTotalSize)(Fts5Context, int iCol, sqlite3_int64 pnToken)

매개변수 iCol 이 0 보다 작으면 출력 변수 *pnToken 을 FTS5 테이블 안의 총 토큰 수로 설정해요. 또는 iCol 이 음수가 아니지만 테이블의 컬럼 수보다 작으면 FTS5 테이블의 모든 행을 고려해 컬럼 iCol 의 총 토큰 수를 반환해요.

매개변수 iCol 이 테이블의 컬럼 수보다 크거나 같으면 SQLITE_RANGE 가 반환돼요. 또는 에러가 발생하면(예: OOM 조건이나 IO 에러) 적절한 SQLite 에러 코드가 반환돼요.

int (xColumnCount)(Fts5Context)

테이블의 컬럼 수를 반환해요.

*int (xColumnSize)(Fts5Context, int iCol, int pnToken)

매개변수 iCol 이 0 보다 작으면 출력 변수 *pnToken 을 현재 행의 총 토큰 수로 설정해요. 또는 iCol 이 음수가 아니지만 테이블의 컬럼 수보다 작으면 *pnToken 을 현재 행의 컬럼 iCol 의 토큰 수로 설정해요.

매개변수 iCol 이 테이블의 컬럼 수보다 크거나 같으면 SQLITE_RANGE 가 반환돼요. 또는 에러가 발생하면 적절한 SQLite 에러 코드가 반환돼요.

이 함수는 "columnsize=0" 옵션으로 만들어진 FTS5 테이블과 함께 사용하면 상당히 비효율적일 수 있어요.

**int (xColumnText)(Fts5Context, int iCol, const char *pz, int pn)

매개변수 iCol 이 0 보다 작거나, 테이블의 컬럼 수보다 크거나 같으면 SQLITE_RANGE 가 반환돼요.

그렇지 않으면 이 함수는 현재 문서의 컬럼 iCol 의 텍스트를 검색하려고 시도해요. 성공하면 (*pz) 가 utf-8 인코딩의 텍스트를 포함하는 버퍼를 가리키도록 설정되고, (*pn) 이 버퍼의 (문자가 아니라) 바이트 크기로 설정되며 SQLITE_OK 가 반환돼요. 그렇지 않으면 에러가 발생할 때 SQLite 에러 코드가 반환되고 (*pz) 와 (*pn) 의 최종 값은 정의되지 않아요.

int (xPhraseCount)(Fts5Context)

현재 쿼리 표현식의 구문 수를 반환해요.

int (xPhraseSize)(Fts5Context, int iPhrase)

매개변수 iCol 이 0 보다 작거나, xPhraseCount 가 반환하는 것처럼 현재 쿼리의 구문 수보다 크거나 같으면 0 이 반환돼요. 그렇지 않으면 이 함수는 쿼리의 구문 iPhrase 안의 토큰 수를 반환해요. 구문은 0 부터 번호가 매겨져요.

*int (xInstCount)(Fts5Context, int pnInst)

*pnInst 를 현재 행 안의 쿼리 안의 모든 구문의 총 발생 수로 설정해요. 성공하면 SQLITE_OK 를, 에러가 발생하면 에러 코드(즉 SQLITE_NOMEM)를 반환해요.

이 API 는 "detail=none" 이나 "detail=column" 옵션으로 만들어진 FTS5 테이블과 함께 사용하면 상당히 느릴 수 있어요. FTS5 테이블이 "detail=none" 이나 "detail=column" 과 "content=" 옵션으로 만들어지면(즉 contentless 테이블이면) 이 API 는 항상 0 을 반환해요.

**int (xInst)(Fts5Context, int iIdx, int *piPhrase, int piCol, int piOff)

현재 행 안의 구문 일치 iIdx 의 세부 사항을 쿼리해요. 구문 일치는 0 부터 번호가 매겨지므로 iIdx 인자는 0 보다 크거나 같고 xInstCount() 가 출력한 값보다 작아야 해요. iIdx 가 0 보다 작거나 xInstCount() 가 반환한 값보다 크거나 같으면 SQLITE_RANGE 가 반환돼요.

그렇지 않으면 출력 매개변수 *piPhrase 가 구문 번호로, *piCol 이 그것이 발생하는 컬럼으로, *piOff 가 구문의 첫 토큰의 토큰 오프셋으로 설정돼요. 성공하면 SQLITE_OK 가, 에러가 발생하면 에러 코드(즉 SQLITE_NOMEM)가 반환돼요.

이 API 는 "detail=none" 이나 "detail=column" 옵션으로 만들어진 FTS5 테이블과 함께 사용하면 상당히 느릴 수 있어요.

sqlite3_int64 (xRowid)(Fts5Context)

현재 행의 rowid 를 반환해요.

*int (xTokenize)(Fts5Context, const char pText, int nText, void pCtx, int (xToken)(void, int, const char, int, int, int) )

FTS5 테이블에 속한 토크나이저를 사용해 텍스트를 토큰화해요.

int (xQueryPhrase)(Fts5Context, int iPhrase, void pUserData, int()(const Fts5ExtensionApi,Fts5Context,void*) )**

이 API 함수는 현재 쿼리의 구문 iPhrase 에 대해 FTS 테이블을 쿼리하는 데 사용돼요. 구체적으로 다음에 해당하는 쿼리가:

... FROM ftstable WHERE ftstable MATCH $p ORDER BY rowid

$p 가 현재 쿼리의 구문 iPhrase 에 해당하는 구문으로 설정된 채 실행돼요. 현재 쿼리의 구문 iPhrase 에 적용되는 어떤 컬럼 필터든 $p 에 포함돼요. 방문하는 각 행에 대해 네 번째 인자로 전달된 콜백 함수가 호출돼요. 콜백 함수에 전달된 컨텍스트와 API 객체는 각 일치된 행의 속성에 접근하는 데 사용될 수 있어요. Api.xUserData() 를 호출하면 세 번째 인자로 pUserData 에 전달된 포인터의 복사본이 반환돼요.

매개변수 iPhrase 가 0 보다 작거나, xPhraseCount() 가 반환하는 것처럼 쿼리의 구문 수보다 크거나 같으면 이 함수는 SQLITE_RANGE 를 반환해요.

콜백 함수가 SQLITE_OK 이외의 어떤 값을 반환하면 쿼리는 중단되고 xQueryPhrase 함수는 즉시 반환해요. 반환된 값이 SQLITE_DONE 이면 xQueryPhrase 는 SQLITE_OK 를 반환해요. 그렇지 않으면 에러 코드가 위로 전파돼요.

쿼리가 사고 없이 완료까지 실행되면 SQLITE_OK 가 반환돼요. 또는 쿼리가 완료되기 전이나 콜백이 중단하기 전에 어떤 에러가 발생하면 SQLite 에러 코드가 반환돼요.

*int (xSetAuxdata)(Fts5Context, void pAux, void(xDelete)(void))

두 번째 인자로 전달된 포인터를 확장 함수의 "보조 데이터" 로 저장해요. 그러면 그 포인터는 같은 MATCH 쿼리의 일부로 이루어진 같은 fts5 확장 함수의 현재 또는 미래 호출이 xGetAuxdata() API 를 사용해 검색할 수 있어요.

각 확장 함수는 각 FTS 쿼리(MATCH 표현식)에 대해 단일 보조 데이터 슬롯을 할당받아요. 확장 함수가 단일 FTS 쿼리에 대해 두 번 이상 호출되면 모든 호출이 단일 보조 데이터 컨텍스트를 공유해요.

이 함수가 호출될 때 이미 보조 데이터 포인터가 있으면 새 포인터로 대체돼요. 원래 포인터와 함께 xDelete 콜백이 지정됐으면 이 시점에 호출돼요.

xDelete 콜백은 지정되면 FTS5 쿼리가 끝난 후에도 보조 데이터 포인터에 대해 호출돼요.

이 함수 안에서 에러(예: OOM 조건)가 발생하면 보조 데이터는 NULL 로 설정되고 에러 코드가 반환돼요. xDelete 매개변수가 NULL 이 아니면 반환하기 전에 보조 데이터 포인터에 대해 호출돼요.

*void (xGetAuxdata)(Fts5Context, int bClear)

fts5 확장 함수의 현재 보조 데이터 포인터를 반환해요. 자세한 내용은 xSetAuxdata() 메서드를 참고하세요.

bClear 인자가 0 이 아니면 이 함수가 반환하기 전에 보조 데이터가 지워져요(NULL 로 설정). 이 경우 xDelete 는 있으면 호출되지 않아요.

*int (xRowCount)(Fts5Context, sqlite3_int64 pnRow)

이 함수는 테이블의 총 행 수를 검색하는 데 사용돼요. 즉, 다음이 반환하는 것과 같은 값이에요:

SELECT count(*) FROM ftstable;

int (xPhraseFirst)(Fts5Context, int iPhrase, Fts5PhraseIter, int, int*)**

이 함수는 타입 Fts5PhraseIter 와 xPhraseNext 메서드와 함께, 현재 행 안의 단일 쿼리 구문의 모든 인스턴스를 반복하는 데 사용돼요. 이것은 xInstCount/xInst API 를 통해 접근 가능한 것과 같은 정보예요. xInstCount/xInst API 가 사용하기 더 편리하지만, 어떤 상황에서는 이 API 가 더 빠를 수 있어요. 구문 iPhrase 의 인스턴스를 반복하려면 다음 코드를 사용해요:

Fts5PhraseIter iter;
int iCol, iOff;
for(pApi->xPhraseFirst(pFts, iPhrase, &iter, &iCol, &iOff);
    iCol>=0;
    pApi->xPhraseNext(pFts, &iter, &iCol, &iOff)
){
  // An instance of phrase iPhrase at offset iOff of column iCol
}

Fts5PhraseIter 구조체는 위에 정의돼 있어요. 애플리케이션은 이 구조체를 직접 수정하면 안 돼요. 위에서 보여준 것처럼 xPhraseFirst() 와 xPhraseNext() API 메서드와만 사용해야 해요(그리고 아래에서 설명하듯 xPhraseFirstColumn() 과 xPhraseNextColumn() 과도).

이 API 는 "detail=none" 이나 "detail=column" 옵션으로 만들어진 FTS5 테이블과 함께 사용하면 상당히 느릴 수 있어요. FTS5 테이블이 "detail=none" 이나 "detail=column" 과 "content=" 옵션으로 만들어지면(즉 contentless 테이블이면) 이 API 는 항상 빈 집합을 반복해요(모든 xPhraseFirst() 호출이 iCol 을 -1 로 설정).

모든 경우에 일치는 (column ASC, offset ASC) 순서로 방문돼요. 즉 컬럼 0 의 모든 것(오프셋으로 정렬) 다음에 컬럼 1 의 것 등.

*void (xPhraseNext)(Fts5Context, Fts5PhraseIter, int piCol, int piOff)

위 xPhraseFirst 를 참고하세요.

int (xPhraseFirstColumn)(Fts5Context, int iPhrase, Fts5PhraseIter, int)**

이 함수와 xPhraseNextColumn() 은 위에서 설명한 xPhraseFirst() 와 xPhraseNext() API 와 비슷해요. 차이는 현재 행 안의 구문의 모든 인스턴스를 반복하는 대신, 이 API 들이 지정된 구문의 인스턴스를 하나 이상 포함하는 현재 행 안의 컬럼 집합을 반복하는 데 사용된다는 점이에요. 예를 들어:

Fts5PhraseIter iter;
int iCol;
for(pApi->xPhraseFirstColumn(pFts, iPhrase, &iter, &iCol);
    iCol>=0;
    pApi->xPhraseNextColumn(pFts, &iter, &iCol)
){
  // Column iCol contains at least one instance of phrase iPhrase
}

이 API 는 "detail=none" 옵션으로 만들어진 FTS5 테이블과 함께 사용하면 상당히 느릴 수 있어요. FTS5 테이블이 "detail=none" "content=" 옵션으로 만들어지면(즉 contentless 테이블이면) 이 API 는 항상 빈 집합을 반복해요(모든 xPhraseFirstColumn() 호출이 iCol 을 -1 로 설정).

이 API 와 그 동반자 xPhraseFirstColumn() 을 사용해 접근하는 정보는 xPhraseFirst/xPhraseNext(xInst/xInstCount)를 사용해서도 얻을 수 있어요. 이 API 의 가장 큰 장점은 "detail=column" 테이블과 함께 사용할 때 그 대안들보다 상당히 더 효율적이라는 점이에요.

void (xPhraseNextColumn)(Fts5Context, Fts5PhraseIter, int piCol)

위 xPhraseFirstColumn 을 참고하세요.

**int (xQueryToken)(Fts5Context, int iPhrase, int iToken, const char *ppToken, int pnToken )

이것은 현재 쿼리의 구문 iPhrase 의 토큰 iToken 에 접근하는 데 사용돼요. 반환하기 전에 출력 매개변수 *ppToken 이 요청된 토큰을 포함하는 버퍼를 가리키도록, *pnToken 이 이 버퍼의 바이트 크기로 설정돼요.

iPhrase 나 iToken 이 0 보다 작거나, iPhrase 가 xPhraseCount() 가 보고하는 것처럼 쿼리의 구문 수보다 크거나 같거나, iToken 이 구문의 토큰 수보다 크거나 같으면 SQLITE_RANGE 가 반환되고 *ppToken 과 *pnToken 이 모두 0 으로 설정돼요.

출력 텍스트는 토큰을 지정한 쿼리 텍스트의 복사본이 아니에요. 그것은 토크나이저 모듈의 출력이에요. tokendata=1 테이블의 경우 여기에 내장된 0x00 와 후행 데이터가 포함돼요.

int (xInstToken)(Fts5Context, int iIdx, int iToken, const char, int*)**

이것은 현재 행 안의 구문 히트 iIdx 의 토큰 iToken 에 접근하는 데 사용돼요. iIdx 가 0 보다 작거나 xInstCount() 가 반환한 값보다 크거나 같으면 SQLITE_RANGE 가 반환돼요. 그렇지 않으면 출력 변수 (*ppToken) 이 일치하는 문서 토큰을 포함하는 버퍼를 가리키도록, (*pnToken) 이 그 버퍼의 바이트 크기로 설정돼요.

출력 텍스트는 토큰화된 문서 텍스트의 복사본이 아니에요. 그것은 토크나이저 모듈의 출력이에요. tokendata=1 테이블의 경우 여기에 내장된 0x00 와 후행 데이터가 포함돼요.

이 API 는 매개변수 iIdx 와 iToken 이 식별하는 토큰이 쿼리 안의 접두어 토큰과 일치하면 어떤 경우에 느릴 수 있어요. 대부분의 경우 쿼리 안의 각 접두어 토큰에 대한 이 API 의 첫 호출은 이 API 가 요구하는 추가 데이터를 수집하기 위해 접두어 토큰과 일치하는 전체 텍스트 인덱스의 부분을 스캔해야 해요. 접두어 토큰이 문서 집합 안의 많은 수의 토큰 인스턴스와 일치하면 이것은 성능 문제가 될 수 있어요.

사용자가 쿼리가 접두어 토큰에 이 API 를 사용할 수 있음을 미리 알면, FTS5 는 전체 텍스트 인덱스의 초기 쿼리의 일부로 필요한 모든 데이터를 수집하도록 구성될 수 있어, 두 번째 스캔을 완전히 피할 수 있어요. 이는 또한 이 API 를 사용하지 않는 접두어 쿼리를 더 느리게 실행하고 더 많은 메모리를 사용하게 해요. FTS5 는 'insttoken' 옵션으로 테이블별로, 또는 fts5_insttoken() 사용자 함수로 쿼리별로 이렇게 구성될 수 있어요.

이 API 는 "detail=none" 이나 "detail=column" 옵션으로 만들어진 FTS5 테이블과 함께 사용하면 상당히 느릴 수 있어요.

**int (xColumnLocale)(Fts5Context, int iCol, const char *pz, int pn)

매개변수 iCol 이 0 보다 작거나, 테이블의 컬럼 수보다 크거나 같으면 SQLITE_RANGE 가 반환돼요.

그렇지 않으면 이 함수는 현재 행의 컬럼 iCol 과 연관된 로케일을 검색하려고 시도해요. 보통 연관된 로케일이 없고, 출력 매개변수 (*pzLocale) 와 (*pnLocale) 이 각각 NULL 과 0 으로 설정돼요. 하지만 fts5_locale() 함수가 값을 fts5 테이블에 삽입할 때 로케일을 값과 연관시키는 데 사용됐으면, (*pzLocale) 이 utf-8 인코딩의 로케일 이름을 포함하는 nul-종결 버퍼를 가리키도록 설정돼요. (*pnLocale) 은 nul-종결자를 포함하지 않은 버퍼의 바이트 크기로 설정돼요.

성공하면 SQLITE_OK 가 반환돼요. 또는 에러가 발생하면 SQLite 에러 코드가 반환돼요. 이 경우 출력 매개변수의 최종 값은 정의되지 않아요.

**int (xTokenize_v2)(Fts5Context, const char pText, int nText, const char pLocale, int nLocale, void pCtx, int (xToken)(void, int, const char, int, int, int) )

FTS5 테이블에 속한 토크나이저를 사용해 텍스트를 토큰화해요. 이 API 는 xTokenize() API 와 같지만 토크나이저 로케일을 지정할 수 있게 한다는 점이 달라요.

8. fts5vocab 가상 테이블 모듈

fts5vocab 가상 테이블 모듈은 사용자가 FTS5 전체 텍스트 인덱스에서 정보를 직접 추출할 수 있게 해줘요. fts5vocab 모듈은 FTS5 의 일부예요. FTS5 가 사용 가능하면 언제든 사용 가능해요.

각 fts5vocab 테이블은 단일 FTS5 테이블과 연관돼요. fts5vocab 테이블은 보통 CREATE VIRTUAL TABLE 문의 컬럼 이름 자리에 두 인자를 지정해 만들어져요. 연관된 FTS5 테이블의 이름과 fts5vocab 테이블의 타입이에요. 현재 "row", "col", "instance" 의 세 가지 타입의 fts5vocab 테이블이 있어요. fts5vocab 테이블이 "temp" 데이터베이스 안에 만들어지지 않으면 연관된 FTS5 테이블과 같은 데이터베이스의 일부여야 해요.

-- Create an fts5vocab "row" table to query the full-text index belonging
-- to FTS5 table "ft1".
CREATE VIRTUAL TABLE ft1_v USING fts5vocab('ft1', 'row');

-- Create an fts5vocab "col" table to query the full-text index belonging
-- to FTS5 table "ft2".
CREATE VIRTUAL TABLE ft2_v USING fts5vocab(ft2, col);

-- Create an fts5vocab "instance" table to query the full-text index
-- belonging to FTS5 table "ft3".
CREATE VIRTUAL TABLE ft3_v USING fts5vocab(ft3, instance);

fts5vocab 테이블이 temp 데이터베이스에 만들어지면 어떤 첨부된 데이터베이스의 FTS5 테이블과도 연관될 수 있어요. fts5vocab 테이블을 "temp" 가 아닌 다른 데이터베이스에 있는 FTS5 테이블에 첨부하려면 CREATE VIRTUAL TABLE 인자에서 FTS5 테이블 이름 앞에 데이터베이스의 이름을 삽입해요. 예를 들어:

-- Create an fts5vocab "row" table to query the full-text index belonging
-- to FTS5 table "ft1" in database "main".
CREATE VIRTUAL TABLE temp.ft1_v USING fts5vocab(main, 'ft1', 'row');

-- Create an fts5vocab "col" table to query the full-text index belonging
-- to FTS5 table "ft2" in attached database "aux".
CREATE VIRTUAL TABLE temp.ft2_v USING fts5vocab('aux', ft2, col);

-- Create an fts5vocab "instance" table to query the full-text index
-- belonging to FTS5 table "ft3" in attached database "other".
CREATE VIRTUAL TABLE temp.ft2_v USING fts5vocab('aux', ft3, 'instance');

"temp" 가 아닌 어떤 데이터베이스에서 fts5vocab 테이블을 만들 때 세 인자를 지정하면 에러가 발생해요.

"row" 타입의 fts5vocab 테이블은 연관된 FTS5 테이블의 각 고유한 용어에 대해 하나의 행을 포함해요. 테이블 컬럼은 다음과 같아요:

Column Contents
term The term, as stored in the FTS5 index.
doc The number of rows that contain at least one instance of the term.
cnt The total number of instances of the term in the entire FTS5 table.

"col" 타입의 fts5vocab 테이블은 연관된 FTS5 테이블의 각 고유한 용어/컬럼 조합에 대해 하나의 행을 포함해요. 테이블 컬럼은 다음과 같아요:

Column Contents
term The term, as stored in the FTS5 index.
col The name of the FTS5 table column that contains the term.
doc The number of rows in the FTS5 table for which column $col contains at least one instance of the term.
cnt The total number of instances of the term that appear in column $col of the FTS5 table (considering all rows).

"instance" 타입의 fts5vocab 테이블은 연관된 FTS 인덱스에 저장된 각 용어 인스턴스에 대해 하나의 행을 포함해요. FTS5 테이블이 'detail' 옵션을 'full' 로 설정해 만들어졌다고 가정하면 테이블 컬럼은 다음과 같아요:

Column Contents
term The term, as stored in the FTS5 index.
doc The rowid of the document that contains the term instance.
col The name of the column that contains the term instance.
offset The index of the term instance within its column. Terms are numbered in order of occurrence starting from 0.

FTS5 테이블이 'detail' 옵션을 'col' 로 설정해 만들어지면 instance 가상 테이블의 offset 컬럼은 항상 NULL 을 포함해요. 이 경우 각 고유한 term/doc/col 조합에 대해 테이블에 하나의 행이 있어요. 또는 FTS5 테이블이 'detail' 을 'none' 으로 설정해 만들어지면 offsetcol 이 모두 항상 NULL 값을 포함해요. detail=none FTS5 테이블의 경우 각 고유한 term/doc 조합에 대해 fts5vocab 테이블에 하나의 행이 있어요.

예:

-- Assuming a database created using:
CREATE VIRTUAL TABLE ft USING fts5(c1, c2);
INSERT INTO ft VALUES('apple banana cherry', 'banana banana cherry');
INSERT INTO ft VALUES('cherry cherry cherry', 'date date date');

-- Then querying the following fts5vocab table (type "col") returns:
--
--    apple  | c1 | 1 | 1
--    banana | c1 | 1 | 1
--    banana | c2 | 1 | 2
--    cherry | c1 | 2 | 4
--    cherry | c2 | 1 | 1
--    date   | c3 | 1 | 3
--
CREATE VIRTUAL TABLE ft_v_col USING fts5vocab(ft, col);

-- Querying an fts5vocab table of type "row" returns:
--
--    apple  | 1 | 1
--    banana | 1 | 3
--    cherry | 2 | 5
--    date   | 1 | 3
--
CREATE VIRTUAL TABLE ft_v_row USING fts5vocab(ft, row);

-- And, for type "instance"
INSERT INTO ft VALUES('apple banana cherry', 'banana banana cherry');
INSERT INTO ft VALUES('cherry cherry cherry', 'date date date');
--
--    apple  | 1 | c1 | 0
--    banana | 1 | c1 | 1
--    banana | 1 | c2 | 0
--    banana | 1 | c2 | 1
--    cherry | 1 | c1 | 2
--    cherry | 1 | c2 | 2
--    cherry | 2 | c1 | 0
--    cherry | 2 | c1 | 1
--    cherry | 2 | c1 | 2
--    date   | 2 | c2 | 0
--    date   | 2 | c2 | 1
--    date   | 2 | c2 | 2
--
CREATE VIRTUAL TABLE ft_v_instance USING fts5vocab(ft, instance);

9. FTS5 데이터 구조

이 섹션은 FTS 모듈이 데이터베이스에 인덱스와 내용을 저장하는 방식을 높은 수준으로 설명해요. 애플리케이션에서 FTS 를 사용하기 위해 이 섹션의 내용을 읽거나 이해할 필요는 없어요. 하지만 FTS 성능 특성을 분석하고 이해하려는 애플리케이션 개발자나 기존 FTS 기능 세트의 개선을 고려하는 개발자에게는 유용할 수 있어요.

FTS5 가상 테이블이 데이터베이스에 만들어질 때 데이터베이스에 3 에서 5 사이의 실제 테이블이 만들어져요. 이것들은 "섀도 테이블"로 알려져 있고, 가상 테이블 모듈이 영구 데이터를 저장하는 데 사용해요. 사용자가 직접 접근해서는 안 돼요. FTS3 과 rtree 를 포함한 많은 다른 가상 테이블 모듈도 섀도 테이블을 만들고 사용해요.

FTS5 는 다음 섀도 테이블을 만들어요. 각 경우 실제 테이블 이름은 FTS5 가상 테이블의 이름에 기반해요(아래에서는 % 를 가상 테이블의 이름으로 대체해 실제 섀도 테이블 이름을 찾아요).

-- This table contains most of the full-text index data.
CREATE TABLE %_data(id INTEGER PRIMARY KEY, block BLOB);

-- This table contains the remainder of the full-text index data.
-- It is almost always much smaller than the %_data table.
CREATE TABLE %_idx(segid, term, pgno, PRIMARY KEY(segid, term)) WITHOUT ROWID;

-- Contains the values of persistent configuration parameters.
CREATE TABLE %_config(k PRIMARY KEY, v) WITHOUT ROWID;

-- Contains the size of each column of each row in the virtual table
-- in tokens. This shadow table is not present if the "columnsize"
-- option is set to 0.
CREATE TABLE %_docsize(id INTEGER PRIMARY KEY, sz BLOB);

-- Contains the actual data inserted into the FTS5 table. There
-- is one "cN" column for each indexed column in the FTS5 table.
-- This shadow table is not present for contentless or external
-- content FTS5 tables.
CREATE TABLE %_content(id INTEGER PRIMARY KEY, c0, c1...);

다음 섹션은 이 다섯 테이블이 FTS5 데이터를 저장하는 데 어떻게 사용되는지 더 자세히 설명해요.

9.1. Varint 형식

아래 섹션들은 "varint" 형태로 저장된 64비트 부호 있는 정수를 참조해요. FTS5 는 SQLite 코어가 다양한 곳에서 사용하는 것과 같은 varint 형식을 사용해요.

varint 는 길이가 1 에서 9 바이트 사이예요. varint 는 최상위 비트가 설정된 0 개 이상의 바이트와 그 뒤에 따르는 최상위 비트가 0 인 단일 바이트, 또는 9 바이트 중 더 짧은 것으로 구성돼요. 처음 8 바이트 각각의 낮은 7 비트와 아홉 번째 바이트의 전체 8 비트가 64비트 2의 보수 정수를 재구성하는 데 사용돼요. varint 는 big-endian 이에요. varint 의 앞쪽 바이트에서 가져온 비트가 뒤쪽 바이트에서 가져온 비트보다 더 중요해요.

9.2. FTS 인덱스 (%_idx 및 %_data 테이블)

FTS 인덱스는 정렬된 키-값 저장소로, 키는 문서 용어 또는 용어 접두어이고 연관된 값은 "doclist" 예요. doclist 는 FTS5 테이블 안의 용어의 각 인스턴스의 위치를 인코딩하는 varint 의 압축 배열이에요. 단일 용어 인스턴스의 위치는 다음의 조합으로 정의돼요:

  • 그것이 나타나는 FTS5 테이블 행의 rowid,
  • 용어 인스턴스가 나타나는 컬럼의 인덱스(컬럼은 왼쪽에서 오른쪽으로 0 부터 번호가 매겨짐), 그리고
  • 컬럼 값 안의 용어 오프셋(즉 이 것 앞에 컬럼 값 안에 나타나는 토큰 수).

FTS 인덱스는 데이터 세트의 각 토큰에 대해 최대 (nPrefix+1) 개의 항목을 포함하는데, 여기서 nPrefix 는 정의된 접두어 인덱스 수예요.

주 FTS 인덱스(접두어 인덱스가 아닌 것)와 연관된 키는 문자 "0" 이 접두로 붙어요. 첫 접두어 인덱스의 키는 "1" 이 접두로 붙어요. 두 번째 접두어 인덱스의 키는 "2" 가 접두로 붙고, 이런 식이에요. 예를 들어 토큰 "document" 가 prefix="2 4" 로 지정된 접두어 인덱스를 가진 FTS5 테이블에 삽입되면 FTS 인덱스에 추가되는 키는 "0document", "1do", "2docu" 예요.

FTS 인덱스 항목은 단일 트리나 해시 테이블 구조에 저장되지 않아요. 대신 "세그먼트 b-tree"라고 불리는 일련의 불변 b-tree 유사 구조에 저장돼요. FTS5 테이블에 대한 쓰기가 커밋될 때마다 새 항목과 삭제된 항목에 대한 묘비 표시를 모두 포함하는 하나 이상(보통 단지 하나)의 새 세그먼트 b-tree 가 추가돼요. FTS 인덱스가 쿼리될 때 독자는 각 세그먼트 b-tree 를 차례로 쿼리하고 결과를 병합하며 새 데이터에 우선권을 줘요.

각 세그먼트 b-tree 에 숫자 레벨이 할당돼요. 트랜잭션 커밋의 일부로 새 세그먼트 b-tree 가 데이터베이스에 기록될 때 레벨 0 에 할당돼요. 단일 레벨에 속한 세그먼트 b-tree 는 주기적으로 병합되어 다음 레벨에 할당되는 단일 더 큰 세그먼트 b-tree 를 만들어요(즉 레벨 0 세그먼트 b-tree 들이 병합되어 단일 레벨 1 세그먼트 b-tree 가 됨). 따라서 수치적으로 더 큰 레벨은 (보통) 더 큰 세그먼트 b-tree 안에 더 오래된 데이터를 포함해요. 병합을 제어하는 방법에 대한 자세한 내용은 'automerge', 'crisismerge', 'usermerge' 옵션과 'merge' 및 'optimize' 명령을 참고하세요.

용어 또는 용어 접두어와 연관된 doclist 가 매우 크면 연관된 doclist 인덱스가 있을 수 있어요. doclist 인덱스는 b-tree 의 내부 노드 집합과 비슷해요. 큰 doclist 를 rowid 나 rowid 범위에 대해 효율적으로 쿼리할 수 있게 해줘요. 예를 들어 다음과 같은 쿼리를 처리할 때:

SELECT ... FROM ft('term') WHERE rowid BETWEEN ? AND ?

FTS5 는 세그먼트 b-tree 인덱스를 사용해 용어 "term" 에 대한 doclist 를 찾은 다음, 그것의 doclist 인덱스(있다고 가정)를 사용해 필요한 범위의 rowid 를 가진 일치의 부분집합을 효율적으로 식별해요.

9.2.1. %_data 테이블 Rowid 공간
CREATE TABLE %_data(
  id INTEGER PRIMARY KEY,
  block BLOB
);

%_data 테이블은 세 가지 타입의 레코드를 저장하는 데 사용돼요:

  • id=10 으로 저장된 특별한 구조 레코드.
  • id=1 로 저장된 특별한 평균(averages) 레코드.
  • 각 세그먼트 b-tree 리프와 doclist 인덱스 리프 및 내부 노드를 저장하는 레코드. 이 레코드들의 id 값이 어떻게 계산되는지는 아래를 참고하세요.

시스템의 각 세그먼트 b-tree 에 고유한 16비트 세그먼트 id 가 할당돼요. 세그먼트 id 는 원래 소유자 세그먼트 b-tree 가 완전히 더 높은 레벨의 세그먼트 b-tree 로 병합된 후에만 재사용될 수 있어요. 세그먼트 b-tree 안에서 각 리프 페이지에 고유한 페이지 번호가 할당돼요. 첫 리프 페이지에 1, 두 번째에 2, 이런 식이에요.

각 doclist 인덱스 리프 페이지에도 페이지 번호가 할당돼요. doclist 인덱스의 첫(가장 왼쪽) 리프 페이지에는 그 용어가 나타나는 세그먼트 b-tree 리프 페이지와 같은 페이지 번호가 할당돼요(doclist 인덱스는 매우 긴 doclist 를 가진 용어에 대해서만 만들어지므로, 세그먼트 b-tree 리프당 최대 하나의 용어가 연관된 doclist 인덱스를 가짐). 이 페이지 번호를 P 라고 하자. doclist 가 두 번째 리프를 필요로 할 정도로 크면 두 번째 리프에는 페이지 번호 P+1 이 할당돼요. 세 번째 리프는 P+2. doclist 인덱스 b-tree 의 각 계층(리프, 리프의 부모, 조부모 등)이 페이지 번호 P 로 시작해 이 방식으로 페이지 번호를 할당받아요.

주어진 세그먼트 b-tree 리프나 doclist 인덱스 리프나 노드를 저장하는 데 %_data 테이블에 사용되는 "id" 값은 다음과 같이 구성돼요:

Rowid Bits Contents
38..43 (16 bit) Segment b-tree id value.
37 (1 bit) Doclist index flag. Set for doclist index pages, clear for segment b-tree leaves.
32..36 (5 bits) Height in tree. This is set to 0 for segment b-tree and doclist index leaves, to 1 for the parents of doclist index leaves, 2 for the grandparents, etc.
0..31 (32 bits) Page number
9.2.2. 구조 레코드 형식

구조 레코드는 현재 FTS 인덱스를 구성하는 세그먼트 b-tree 집합과 진행 중인 어떤 점진적 병합 작업의 세부 사항을 식별해요. 그것은 id=10 으로 %_data 테이블에 저장돼요. 구조 레코드는 단일 32비트 부호 없는 값 - 쿠키 값 - 으로 시작해요. 이 값은 구조가 수정될 때마다 증가해요. 쿠키 값 다음에 다음과 같은 세 개의 varint 값이 나와요:

  • 인덱스의 레벨 수(즉 어떤 세그먼트 b-tree 와도 연관된 최대 레벨 더하기 1).
  • 인덱스의 총 세그먼트 b-tree 수.
  • FTS5 테이블이 만들어진 이후 레벨 0 트리에 기록된 총 세그먼트 b-tree 리프 수.

그런 다음 0 부터 nLevel 까지 각 레벨에 대해:

  • 현재 점진적 병합의 입력으로 사용되는 이전 레벨의 입력 세그먼트 수, 또는 이 레벨에 대한 새 세그먼트 b-tree 를 만들려는 진행 중인 점진적 병합이 없으면 0.
  • 그 레벨의 총 세그먼트 b-tree 수.
  • 그런 다음 각 세그먼트 b-tree 에 대해, 가장 오래된 것부터 가장 새로운 것 순으로:
    • 세그먼트 id.
    • 첫 리프의 페이지 번호(흔히 1, 항상 >0).
    • 마지막 리프의 페이지 번호(항상 >0).
9.2.3. 평균 레코드 형식

항상 id=1 로 %_data 테이블에 저장되는 평균 레코드는 어떤 것의 평균도 저장하지 않아요. 대신 (nCol+1) 개의 압축 varint 값의 벡터를 포함하는데, 여기서 nCol 은 인덱싱되지 않은 컬럼을 포함한 FTS5 테이블의 컬럼 수예요. 첫 varint 는 FTS5 테이블의 총 행 수를 포함해요. 두 번째는 가장 왼쪽 FTS5 테이블 컬럼에 저장된 모든 값의 총 토큰 수를 포함해요. 세 번째는 다음 가장 왼쪽 컬럼의 모든 값의 토큰 수를 포함하고, 이런 식이에요. 인덱싱되지 않은 컬럼의 값은 항상 0 이에요.

9.2.4. 세그먼트 B-Tree 형식
9.2.4.1. 키/Doclist 형식

키/doclist 형식은 정렬된 순서로 일련의 키(문서 용어 또는, 그것들이 속한 특정 인덱스를 식별하는 단일 문자로 접두된 용어 접두어)를 각각의 연관된 doclist 와 함께 저장하는 데 사용되는 형식이에요. 형식은 번갈아 나타나는 키와 doclist 가 함께 압축된 것으로 구성돼요.

첫 키는 다음과 같이 저장돼요:

  • 키의 바이트 수(N)를 나타내는 varint, 그 다음에
  • 키 데이터 자체(N 바이트).

각 후속 키는 다음과 같이 저장돼요:

  • 키가 이전 키와 공유하는 공통 접두어의 크기를 바이트로 나타내는 varint,
  • 공통 접두어 다음에 오는 키의 바이트 수(N)를 나타내는 varint, 그 다음에
  • 키 접미어 데이터 자체(N 바이트).

예를 들어 FTS5 키/doclist 레코드의 처음 두 키가 "0challenger" 와 "0chandelier" 라면, 첫 키는 varint 11 다음에 "0challenger" 의 11 바이트로 저장되고, 두 번째 키는 varint 4 와 7 다음에 "ndelier" 의 7 바이트로 저장돼요.

그림 1 - 용어/Doclist 형식 (키가 번갈아 doclist 와 함께 저장됨)

각 doclist 는 용어 또는 용어 접두어의 인스턴스를 최소 하나 포함하는 행을(rowid 값으로) 식별하고, 각 용어 인스턴스의 행 안의 위치를 나열하는 연관된 위치 목록 또는 "poslist" 를 제공해요. 이런 의미에서 "위치"는 컬럼 번호와 컬럼 값 안의 용어 오프셋으로 정의돼요.

doclist 안에서 문서는 항상 rowid 로 정렬된 순서로 저장돼요. doclist 의 첫 rowid 는 varint 로 그대로 저장돼요. 그것 바로 뒤에 연관된 위치 목록이 따라와요. 그 다음에 첫 rowid 와 두 번째의 차이가 varint 로, 그 다음에 doclist 의 두 번째 rowid 와 연관된 doclist 가 따라와요. 이런 식이에요.

파싱으로 doclist 의 크기를 결정할 방법은 없어요. 이것은 외부에 저장되어야 해요. FTS5 에서 어떻게 이루어지는지에 대한 자세한 내용은 아래 섹션을 참고하세요.

그림 2 - Doclist 형식 (rowid 0, delta-인코딩된 rowid 1, ... 다음에 위치 목록 0, 위치 목록 1, ...)

위치 목록 - 종종 "poslist" 로 줄임 - 은 해당 토큰의 각 인스턴스의 행 안의 컬럼과 토큰 오프셋을 식별해요. poslist 의 형식은:

  • 이 필드를 제외한 poslist 크기의 두 배에, 항목에 "delete" 플래그가 설정되어 있으면 1 을 더한 값으로 설정된 varint.
  • 행의 컬럼 0(가장 왼쪽 컬럼)에 대한 (비어 있을 수 있는) 오프셋 목록. 각 오프셋은 varint 로 저장돼요. 첫 varint 는 첫 오프셋의 값에 2 를 더한 것을 포함해요. 두 번째 varint 는 두 번째와 첫 오프셋의 차이에 2 를 더한 것을 포함해요. 등등. 예를 들어 오프셋 목록이 오프셋 0, 10, 15, 16 을 포함해야 하면, 다음 값들을 varint 로 인코딩해 끝에서 끝으로 압축해 인코딩돼요:
         2, 12, 7, 3
  • 컬럼 0 가 아닌, 토큰의 인스턴스를 하나 이상 포함하는 각 컬럼에 대해:
    • 바이트 값 0x01.
    • 컬럼 번호, varint 로.
    • 컬럼 0 의 오프셋 목록과 같은 형식의 오프셋 목록.

그림 3 - 컬럼 0 과 i 의 오프셋을 가진 위치 목록 (poslist)

9.2.4.2. 페이지 매김

충분히 작으면(기본적으로 4000 바이트보다 작다는 뜻) 세그먼트 b-tree 의 전체 내용이 이전 섹션에서 설명한 키/doclist 형식으로 %_data 테이블 안의 단일 blob 으로 저장될 수 있어요. 그렇지 않으면 키/doclist 는 페이지(기본적으로 각각 약 4000 바이트)로 분할되고 %_data 테이블의 연속 항목 집합에 저장돼요(자세한 내용은 위 참고).

키/doclist 가 페이지로 나뉠 때 형식에 다음 수정이 이루어져요:

  • 단일 varint 나 키 데이터 필드는 두 페이지에 걸치지 않아요.
  • 각 페이지의 첫 키는 접두어 압축되지 않아요. doclist 의 첫 키에 대해 위에서 설명한 형식으로 저장돼요. 크기가 varint 로, 그 다음 키 데이터로.
  • 페이지에 첫 키 앞에 하나 이상의 rowid 가 있으면 처음 것은 delta 압축되지 않아요. 그것이 그것의 doclist(그럴 수도 아닐 수도 있음)의 첫 rowid 인 것처럼 그대로 저장돼요.

각 페이지에는 또한 고정 크기 4 바이트 헤더와 가변 크기 푸터가 있어요. 헤더는 2 개의 16비트 big-endian 정수 필드로 나뉘어요. 포함하는 것:

  • 첫 키보다 앞에 발생하면 페이지의 첫 rowid 값의 바이트 오프셋, 그렇지 않으면 0.
  • 페이지 푸터의 바이트 오프셋.

페이지 푸터는 페이지에 나타나는 각 키의 바이트 오프셋을 포함하는 일련의 varint 로 구성돼요. 페이지에 키가 없으면 페이지 푸터의 크기는 0 바이트예요.

그림 4 - 페이지 형식 (4 바이트 헤더, 수정된 키/doclist 데이터, 가변 크기 푸터)

9.2.4.3. 세그먼트 인덱스 형식

세그먼트 b-tree 의 내용을 키/doclist 형식으로 형식화한 다음 페이지로 분할한 결과는 b+tree 의 리프와 매우 비슷해요. 이 b+tree 의 내부 노드에 대한 형식을 만들고 %_data 테이블에 리프와 함께 저장하는 대신, 그러한 노드에 저장됐을 키가 %_idx 테이블에 추가되는데, 정의는:

CREATE TABLE %_idx(
  segid INTEGER,              -- segment id
  term TEXT,                  -- prefix of first key on page
  pgno INTEGER,               -- (2*pgno + bDoclistIndex)
  PRIMARY KEY(segid, term)
);

최소 하나의 키를 포함하는 각 "리프" 페이지에 대해 항목이 %_idx 테이블에 추가돼요. 필드는 다음과 같이 설정돼요:

Column Contents
segid The integer segment id.
term The smallest prefix of the first key on the page that is larger than all keys on the previous page. For the first page in a segment, this prefix is zero bytes in size.
pgno This field encodes both the page number (within the segment - starting from 1) and the doclist index flag. The doclist index flag is set if the final key on the page has an associated doclist index. The value of this field is: (pgno*2 + bDoclistIndexFlag)

그런 다음 용어 t 를 포함할 수 있는 세그먼트 i 의 리프를 찾으려면 내부 노드를 검색하는 대신 FTS5 는 쿼리를 실행해요:

SELECT pgno FROM %_idx WHERE segid=$i AND term>=$t ORDER BY term LIMIT 1
9.2.4.4. Doclist 인덱스 형식

이전 섹션에서 설명한 세그먼트 인덱스는 세그먼트 b-tree 를 용어나, 필요한 크기의 접두어 인덱스가 있다고 가정하면 용어 접두어로 효율적으로 쿼리할 수 있게 해줘요. 이 섹션에서 설명하는 데이터 구조인 doclist 인덱스는 FTS5 가 단일 용어나 용어 접두어와 연관된 doclist 안에서 rowid 또는 rowid 범위를 효율적으로 검색할 수 있게 해줘요.

모든 키에 연관된 doclist 인덱스가 있는 것은 아니에요. 기본적으로 doclist 인덱스는 그 doclist 가 4 개 이상의 세그먼트 b-tree 리프 페이지에 걸칠 때만 키에 추가돼요. Doclist 인덱스 자체는 b-tree 이고, 리프와 내부 노드 모두 %_data 테이블의 항목으로 저장되지만, 실제로 대부분의 doclist 는 단일 리프에 들어갈 만큼 작아요. FTS5 는 doclist 인덱스 노드와 리프에 세그먼트 b-tree 리프와 같은 대략적인 크기(기본적으로 4000 바이트)를 사용해요.

Doclist 인덱스 리프와 내부 노드는 같은 페이지 형식을 사용해요. 첫 바이트는 "flags" 바이트예요. 이것은 doclist 인덱스 b-tree 의 루트 페이지에 대해 0x00 으로, 다른 모든 페이지에 대해 0x01 로 설정돼요. 페이지의 나머지는 다음과 같은 일련의 촘촘히 압축된 varint 로 구성돼요:

  • 가장 왼쪽 자식 페이지의 페이지 번호, 그 다음에
  • 가장 왼쪽 자식 페이지의 가장 작은 rowid 값, 그 다음에
  • 각 후속 자식 페이지에 대해 하나의 varint 로 다음 값을 포함:
    • 자식 페이지에 rowid 가 없으면 0x00(이것은 "자식" 페이지가 실제로 세그먼트 b-tree 리프일 때만 발생할 수 있음), 또는
    • 자식 페이지의 가장 작은 rowid 와 doclist 인덱스 페이지에 저장된 이전 rowid 값의 차이.

doclist 인덱스의 가장 왼쪽 doclist 인덱스 리프에 대해, 가장 왼쪽 자식 페이지는 키 자체를 포함하는 것 다음의 첫 세그먼트 b-tree 리프예요.

9.3. 문서 크기 테이블 (%_docsize 테이블)

CREATE TABLE %_docsize(
    id INTEGER PRIMARY KEY,   -- id of FTS5 row this record pertains to
    sz BLOB                   -- blob containing nCol packed varints
);

많은 흔한 검색 결과 순위 함수는 결과 문서의 토큰 크기를 입력으로 요구해요(짧은 문서의 검색 용어 히트가 긴 문서의 것보다 더 중요하다고 여겨지므로). 이 정보에 빠른 접근을 제공하기 위해 FTS5 테이블의 각 행에 대해 (같은 rowid 를 가진) %_docsize 섀도 테이블에 행의 각 컬럼 값의 크기를 토큰 단위로 포함하는 해당 레코드가 존재해요.

컬럼 값 크기는 FTS5 테이블의 각 컬럼에 대해 하나의 압축 varint 를 포함하는 blob 에 왼쪽에서 오른쪽으로 저장돼요. varint 는 물론 해당 컬럼 값의 총 토큰 수를 포함해요. 인덱싱되지 않은 컬럼도 이 varint 벡터에 포함돼요. 그것들에 대해 값은 항상 0 으로 설정돼요.

이 테이블은 xColumnSize API 가 사용해요. columnsize=0 옵션을 지정해 완전히 생략될 수 있어요. 그 경우 xColumnSize API 는 보조 함수에 여전히 사용 가능하지만 훨씬 느리게 실행돼요.

9.4. 테이블 내용 (%_content 테이블)

-- locale=0 (the default) table
CREATE TABLE %_content(id INTEGER PRIMARY KEY, c0, c1...);

-- locale=1 table
CREATE TABLE %_content(id INTEGER PRIMARY KEY, c0, c1..., l0, l1...);

실제 테이블 내용 - FTS5 테이블에 삽입된 값 - 은 %_content 테이블에 저장돼요. 이 테이블은 인덱싱되지 않은 컬럼을 포함해 FTS5 테이블의 각 컬럼에 대해 하나의 "c*" 컬럼으로 만들어져요. 가장 왼쪽 FTS5 테이블 컬럼의 값은 %_content 테이블의 "c0" 컬럼에, 다음 FTS5 테이블 컬럼의 값은 "c1" 컬럼에 저장되는 식이에요.

locale 옵션이 1 로 설정된 FTS5 테이블의 경우 %_content 테이블은 테이블의 각 인덱싱된(즉 UNINDEXED 가 아닌) 컬럼에 대해 하나의 "l*" 컬럼도 포함해요. 기본 로케일을 사용해 fts5 테이블에 기록된 값에 대해 이 컬럼은 NULL 을 포함해요. 또는 연관된 로케일(fts5_locale() 값)로 기록된 값에 대해 이 컬럼은 텍스트로 로케일의 이름을 포함해요.

각 "l*" 컬럼 이름은 연관된 "c*" 컬럼과 같은 정수 구성 요소를 가져요. 이는 fts5 테이블에 UNINDEXED 컬럼이 하나 이상 있으면 "l*" 컬럼 이름 집합이 연속된 정수 구성 요소 집합을 포함하지 않을 수 있다는 뜻이에요. 예를 들어:

-- This fts5 table:
CREATE VIRTUAL TABLE ft USING fts5(a, b UNINDEXED, c, locale=1);

-- uses a %_content table with no "l1" column:
CREATE TABLE ft_content(id INTEGER PRIMARY KEY, c0, c1, c2, l0, l2);

contentless_unindexed=1 옵션이 지정되지 않으면 이 테이블은 external content 또는 contentless FTS5 테이블에 대해 완전히 생략돼요. contentless_unindexed=1 옵션을 지정하는 contentless 테이블의 경우 %_content 테이블이 만들어지지만, fts5 테이블의 UNINDEXED 컬럼에 해당하는 "c*" 컬럼만 포함해요. 예를 들어:

-- This fts5 table:
CREATE VIRTUAL TABLE ft USING fts5(a, b UNINDEXED, c, contentless_unindexed=1);

-- uses a %_content table with only the "c1" (b) column
CREATE TABLE ft_content(id INTEGER PRIMARY KEY, c1);

9.5. 구성 옵션 (%_config 테이블)

CREATE TABLE %_config(k PRIMARY KEY, v) WITHOUT ROWID;

이 테이블은 영구 구성 옵션 중 어떤 것의 값이라도 저장해요. 컬럼 "k" 는 옵션의 이름(텍스트)을, 컬럼 "v" 는 값을 저장해요. 예제 내용:

sqlite> SELECT * FROM ft_config;
┌─────────────┬──────┐
│      k      │  v   │
├─────────────┼──────┤
│ crisismerge │ 8    │
│ pgsz        │ 8000 │
│ usermerge   │ 4    │
│ version     │ 4    │
└─────────────┴──────┘

부록 A: FTS3/4 와의 비교

또한 유사하지만 더 성숙한 FTS3/4 모듈도 사용 가능해요. FTS5 는 FTS4 의 새 버전으로, 하위 호환성을 희생하지 않고는 FTS4 에서 고칠 수 없었던 문제에 대한 다양한 수정과 해결책을 포함해요. 그 문제 중 일부는 아래에 설명돼요.

애플리케이션 이식 가이드

FTS3 이나 FTS4 대신 FTS5 를 사용하려면 애플리케이션은 보통 최소한의 수정을 요구해요. 대부분은 세 범주에 속해요. FTS 테이블을 만들기 위해 사용되는 CREATE VIRTUAL TABLE 문에 필요한 변경, 테이블에 대한 쿼리를 실행하는 데 사용되는 SELECT 쿼리에 필요한 변경, FTS 보조 함수를 사용하는 애플리케이션에 필요한 변경.

CREATE VIRTUAL TABLE 문에 대한 변경
  • 모듈 이름을 "fts3" 또는 "fts4" 에서 "fts5" 로 바꿔야 해요.
  • 컬럼 정의에서 모든 타입 정보나 제약 조건 지정을 제거해야 해요. FTS3/4 는 컬럼 정의에서 컬럼 이름 다음의 모든 것을 무시하지만, FTS5 는 그것을 파싱하려고 시도해요(그리고 실패하면 에러를 보고할 거예요).
  • "matchinfo=fts3" 옵션은 사용할 수 없어요. "columnsize=0" 옵션이 동등해요.
  • notindexed= 옵션은 사용할 수 없어요. 컬럼 정의에 UNINDEXED 를 추가하는 것이 동등해요.
  • ICU 토크나이저는 사용할 수 없어요.
  • compress=, uncompress=, languageid= 옵션은 사용할 수 없어요. 현재 그 기능에 대한 동등한 것이 없어요.
 -- FTS3/4 statement
CREATE VIRTUAL TABLE ft USING fts4(
  linkid INTEGER,
  header CHAR(20),
  text VARCHAR,
  notindexed=linkid,
  matchinfo=fts3,
  tokenizer=unicode61
);

 -- FTS5 equivalent (note - the "tokenizer=unicode61" option is not
 -- required as this is the default for FTS5 anyway)
CREATE VIRTUAL TABLE ft USING fts5(
  linkid UNINDEXED,
  header,
  text,
  columnsize=0
);
SELECT 문에 대한 변경
  • "docid" 별칭은 존재하지 않아요. 애플리케이션은 대신 "rowid" 를 사용해야 해요.
  • 컬럼-필터가 FTS 쿼리의 일부로 그리고 MATCH 연산자의 LHS 로 컬럼을 사용해 모두 지정될 때의 쿼리 동작은 약간 달라요. "a" 와 "b" 컬럼을 가진 테이블과 다음과 같은 쿼리에 대해:
... a MATCH 'b: string'

FTS3/4 는 컬럼 "b" 에서 일치를 검색해요. 하지만 FTS5 는 항상 0 행을 반환해요. 결과가 먼저 컬럼 "b" 에 대해, 그 다음 컬럼 "a" 에 대해 필터링되어 결과가 남지 않기 때문이에요. 즉, FTS3/4 에서는 내부 필터가 외부를 재정의하지만, FTS5 에서는 두 필터가 모두 적용돼요.

  • FTS 쿼리 구문(MATCH 연산자의 오른쪽)은 어떤 방식으로 바뀌었어요. FTS5 구문은 FTS4 "enhanced syntax" 와 꽤 가까워요. 주요 차이는 FTS5 가 쿼리 문자열 안의 인식되지 않는 문장 부호 문자와 같은 것에 대해 더 까다롭다는 점이에요. FTS3/4 에서 작동하는 대부분의 쿼리는 FTS5 에서도 작동해야 하고, 그렇지 않은 것은 파싱 에러를 반환해야 해요.
보조 함수 변경

FTS5 에는 matchinfo() 나 offsets() 함수가 없고, snippet() 함수는 FTS3/4 만큼 완전한 기능이 아니에요. 하지만 FTS5 는 애플리케이션이 사용자 정의 보조 함수를 만들 수 있는 API 를 제공하므로, 필요한 어떤 기능이든 애플리케이션 코드 안에서 구현될 수 있어요.

FTS5 가 제공하는 내장 보조 함수 집합은 미래에 개선될 수 있어요.

기타 문제
  • fts4aux 모듈이 제공하는 기능은 이제 fts5vocab 이 제공해요. 이 두 테이블의 스키마는 약간 달라요.
  • FTS3/4 "merge=X,Y" 명령은 FTS5 merge 명령으로 대체됐어요.
  • FTS3/4 "automerge=X" 명령은 FTS5 automerge 옵션으로 대체됐어요.

기술적 차이 요약

FTS5 는 각각의 주요 작업이 각 고유 토큰에서 일련의 문서 안의 그 토큰 인스턴스 목록으로 매핑하는 인덱스를 유지하는 것이라는 점에서 FTS3/4 와 비슷해요. 여기서 각 인스턴스는 나타나는 문서와 그 문서 안의 위치로 식별돼요. 예를 들어:

-- Given the following SQL:
CREATE VIRTUAL TABLE ft USING fts5(a, b);
INSERT INTO ft(rowid, a, b) VALUES(1, 'X Y', 'Y Z');
INSERT INTO ft(rowid, a, b) VALUES(2, 'A Z', 'Y Y');

-- The FTS5 module creates the following mapping on disk:
A --> (2, 0, 0)
X --> (1, 0, 0)
Y --> (1, 0, 1) (1, 1, 0) (2, 1, 0) (2, 1, 1)
Z --> (1, 1, 1) (2, 0, 1)

위 예에서 각 삼중항은 rowid, 컬럼 번호(컬럼은 왼쪽에서 오른쪽으로 0 부터 순차적으로 번호가 매겨짐), 컬럼 값 안의 위치(컬럼 값의 첫 토큰은 0, 두 번째는 1 등)로 토큰 인스턴스의 위치를 식별해요. 이 인덱스를 사용해 FTS5 는 "토큰 'A' 를 포함하는 모든 문서의 집합" 또는 "시퀀스 'Y Z' 를 포함하는 모든 문서의 집합" 같은 쿼리에 시기적절한 답을 제공할 수 있어요. 단일 토큰과 연관된 인스턴스 목록을 "instance-list" 라고 불러요.

FTS3/4 와 FTS5 의 주요 차이는 FTS3/4 에서 각 instance-list 가 단일 큰 데이터베이스 레코드로 저장되는 반면, FTS5 에서는 큰 instance-list 가 여러 데이터베이스 레코드로 나뉜다는 점이에요. 이는 큰 목록을 포함하는 큰 데이터베이스를 다룰 때 다음 함의를 가져요:

  • FTS5 는 메모리 사용량과 최대 할당 크기를 줄이기 위해 instance-list 를 메모리에 점진적으로 로드할 수 있어요. FTS3/4 는 매우 자주 전체 instance-list 를 메모리에 로드해요.
  • 둘 이상의 토큰을 특징으로 하는 쿼리를 처리할 때 FTS5 는 때때로 큰 instance-list 의 부분집합을 검사해 쿼리에 답할 수 있다고 결정할 수 있어요. FTS3/4 는 거의 항상 전체 instance-list 를 탐색해야 해요.
  • instance-list 가 너무 커져 SQLITE_MAX_LENGTH 한계를 초과하면 FTS3/4 는 그것을 처리할 수 없어요. FTS5 에는 이 문제가 없어요.

이러한 이유로 많은 복잡한 쿼리가 FTS5 를 사용하면 더 적은 메모리를 사용하고 더 빠르게 실행될 수 있어요.

FTS5 가 FTS3/4 와 다른 다른 몇 가지 방식:

  • FTS5 는 감소하는 관련성 순서로 결과를 반환하기 위한 "ORDER BY rank" 를 지원해요.
  • FTS5 는 사용자가 고급 순위 및 텍스트 처리 애플리케이션을 위한 사용자 정의 보조 함수를 만들 수 있는 API 를 제공해요. 특별한 "rank" 컬럼은 사용자 정의 보조 함수에 매핑될 수 있어서 쿼리에 "ORDER BY rank" 를 추가해도 예상대로 작동해요.
  • FTS5 는 기본적으로 유니코드 구분자 문자와 대소문자 동등성을 인식해요. 이것은 FTS3/4 를 사용해서도 가능하지만 명시적으로 활성화해야 해요.
  • 쿼리 구문은 모호함을 제거하고 쿼리 용어에서 특수 문자를 이스케이프할 수 있게 하기 위해 필요한 곳에서 개정됐어요.
  • 기본적으로 FTS3/4 는 사용자가 실행한 INSERT, UPDATE, DELETE 문 안에서 전체 텍스트 인덱스를 구성하는 b-tree 중 두 개 이상을 가끔 병합해요. 이는 FTS3/4 가 그 안에서 두 개 이상의 큰 b-tree 를 병합하도록 예측할 수 없게 선택할 수 있으므로 FTS3/4 테이블에 대한 어떤 작업도 놀랍게 느릴 수 있다는 뜻이에요. FTS5 는 기본적으로 점진적 병합을 사용하는데, 이는 주어진 어떤 INSERT, UPDATE, DELETE 작업 안에서 발생할 수 있는 처리량을 제한해요.

더 알아보기 (Learn more)