test(A4): korean + english trigram matching at FTS level

3개 신규 unit tests in tests/fts.rs §7:

1. fts_trigram_korean_3char_substring_hits — Codex sqlite 3.45.1 검증
   동작 5개 assert pin: raw 3자 substring hit (충돌은/발생한),
   quoted phrase hit (\"해시 충돌\"/\"시 충\"), raw 해시충 0-hit (원문
   미존재).
2. fts_trigram_korean_short_query_zero_hit_pinned — 2자 한국어 query
   (충돌·키) 0-hit 회귀 감지. trigram 구조 변경 시 먼저 fail.
3. fts_trigram_english_substring_hits — substring recall 동작 변경
   pin (token→tokenizer, to 0-hit).

검증: cargo test -p kebab-store-sqlite --test fts → 13/13 PASS
(신규 3 + 기존 10).

Step 1c (multi-token 한국어 query e.g. \"해시 충돌\") 와 Step 5
(lexical BM25 snapshot 갱신) 는 Task A5 의 build_match_string()
재설계 후 진행.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-23 00:57:37 +00:00
parent 753b1ff5e5
commit fe123c0c6d
2 changed files with 117 additions and 11 deletions

View File

@@ -481,3 +481,115 @@ fn fts_store_drop_releases_wal_files() {
.expect("main DB file should be removable after store drop");
}
}
// ── 7. Trigram tokenizer behavior (V007) — Korean + English ──────────
/// V007 의 trigram tokenizer 가 한국어 3자 이상 연속 substring 을
/// 매칭하는지. Codex round 1/2 가 sqlite 3.45.1 로 검증한 동작을 pin:
/// - raw query 가 3자 이상 공백 없는 substring 인 경우 hit.
/// - raw query 가 공백을 포함하면 FTS5 가 토큰 경계로 분리 →
/// 양 토큰이 3자 미만이면 0-hit.
/// - quoted phrase ("..." 안에 공백 포함) 는 통째로 substring 매칭.
#[test]
fn fts_trigram_korean_3char_substring_hits() {
let env = common::TestEnv::new();
let store = SqliteStore::open(&env.config()).unwrap();
store.run_migrations().unwrap();
let conn = raw_conn_no_fk(&env);
insert_chunk(
&conn,
&"k".repeat(32),
&"d".repeat(32),
"[]",
"해시 충돌은 키와 값을 매핑할 때 발생한다",
);
// raw 3+ chars 공백 없는 연속 substring → hit.
assert_eq!(
count_match(&conn, "충돌은"),
1,
"raw 3-char 공백 없는 substring '충돌은' must hit"
);
assert_eq!(
count_match(&conn, "발생한"),
1,
"raw 3-char 공백 없는 substring '발생한' must hit"
);
// quoted phrase (공백 포함) → substring 매칭으로 hit.
assert_eq!(
count_match(&conn, "\"해시 충돌\""),
1,
"quoted whole phrase '해시 충돌' (5 chars including space)"
);
assert_eq!(
count_match(&conn, "\"시 충\""),
1,
"quoted phrase '시 충' across the space boundary"
);
// raw with no whitespace but substring not present in source → 0-hit.
assert_eq!(
count_match(&conn, "해시충"),
0,
"원문에 공백 없는 '해시충' trigram 이 없으므로 0-hit"
);
}
/// V007 trigram 의 핵심 제약: 3 Unicode chars 미만 query 는 색인 단위가
/// 없어 항상 0-hit. design §3.4 + 사용자 결정 (lexical core 정상 0-hit,
/// CLI/TUI wrapper 가 안내 메시지 출력). 회귀 감지 — trigram 구조 변경
/// 또는 다른 tokenizer 도입 시 이 test 가 먼저 fail 한다.
#[test]
fn fts_trigram_korean_short_query_zero_hit_pinned() {
let env = common::TestEnv::new();
let store = SqliteStore::open(&env.config()).unwrap();
store.run_migrations().unwrap();
let conn = raw_conn_no_fk(&env);
insert_chunk(
&conn,
&"k".repeat(32),
&"d".repeat(32),
"[]",
"해시 충돌은 키와 값을 매핑할 때 발생한다",
);
// 2자 한국어 query — 도그푸딩에서 보고된 핵심 케이스 ('충돌'/'값').
assert_eq!(count_match(&conn, "충돌"), 0, "2-char Korean query");
// 1자 한국어 query.
assert_eq!(count_match(&conn, ""), 0, "1-char Korean query");
}
/// V007 trigram 은 영어에도 substring 매칭으로 동작 — recall ↑, 단어
/// 경계 정밀도 ↓. design §3.4 의 동작 변경을 명시적으로 핀.
#[test]
fn fts_trigram_english_substring_hits() {
let env = common::TestEnv::new();
let store = SqliteStore::open(&env.config()).unwrap();
store.run_migrations().unwrap();
let conn = raw_conn_no_fk(&env);
insert_chunk(
&conn,
&"e".repeat(32),
&"d".repeat(32),
"[]",
"the tokenizer normalizes whitespace before matching",
);
// trigram substring — 'token' hits inside 'tokenizer'.
assert_eq!(
count_match(&conn, "token"),
1,
"substring of 'tokenizer' — trigram recall"
);
assert_eq!(
count_match(&conn, "izer"),
1,
"substring of 'tokenizer'"
);
// 3-char-minimum applies to English too.
assert_eq!(count_match(&conn, "to"), 0, "2-char English query");
}

View File

@@ -150,21 +150,15 @@ INSERT INTO chunks_fts(chunk_id, doc_id, heading_path, text)
- raw `MATCH '충돌'` (2자) → 0-hit (trigram 구조)
실 위키 문서 fixture 가 필요한 후속 검증은 별도 task 로 deferral.
- [ ] **Step 1: 한국어 trigram 매칭 테스트 (실패 확인)** — fixture chunk text `"해시 충돌은 키와 값을 매핑할 때 발생한다"` (V007 적용 store). Codex sqlite 3.45.1 검증 기준 동작:
- raw `MATCH '충돌은'` (공백 없는 3자 연속 substring) → hit. ✓
- quoted `MATCH '"해시 충돌"'` (whole phrase) → hit. ✓
- quoted `MATCH '"시 충"'` (phrase 2 chars + space + 1 char) → hit. ✓
- raw `MATCH '해시충'` → 0-hit (원문에 "해시충" 3-gram 이 연속으로 없음 — "해시" 공백 "충돌").
- raw `MATCH '시 충'` (공백 포함 unquoted) → 0-hit (FTS5 가 공백을 토큰 경계로 처리).
위 5개 assert. Expected: V007 적용 store 에서 PASS. store 테스트가 migration 을 V006 까지만 적용한다면 V007 까지 적용되도록 수정.
- [x] **Step 1: 한국어 trigram 매칭 테스트**`fts_trigram_korean_3char_substring_hits` (fts.rs §7). 5개 assert (raw 3자 hit, quoted phrase hit, `해시충` 0-hit) 모두 통과.
- [ ] **Step 1b: 2자 query 0-hit 핀 (회귀 감지)**`MATCH '충돌'` (2 Unicode chars) 이 반드시 0 결과를 반환. trigram 구조 변경 감지 회귀 테스트.
- [x] **Step 1b: 2자 query 0-hit 핀**`fts_trigram_korean_short_query_zero_hit_pinned` (`충돌`/`키` 0-hit).
- [ ] **Step 1c: multi-token 한국어 query 테스트**`crates/kebab-search` 또는 `crates/kebab-app` 통합 레벨. 사용자 query `해시 충돌``build_match_string()` 을 통해 hit 하는지. Expected: A4 시점 FAIL (현재 builder 가 `"해시" "충돌"` AND 로 trigram 0-hit), Task A5 builder 재설계 후 PASS.
- [ ] **Step 1c: multi-token 한국어 query 테스트**`crates/kebab-search` 또는 `crates/kebab-app` 통합 레벨. 사용자 query `해시 충돌``build_match_string()` 을 통해 hit. Expected: A4 시점 FAIL (현재 builder 가 `"해시" "충돌"` AND 로 trigram 0-hit), Task A5 builder 재설계 후 PASS.
- [ ] **Step 2: 영어 substring 동작 핀**영어 텍스트에 대해 trigram substring 매칭 (예: `tokenizer` 텍스트가 `MATCH 'token'` hit) 을 명시적으로 문서화·고정.
- [x] **Step 2: 영어 substring 동작 핀**`fts_trigram_english_substring_hits` (`token``tokenizer`, `to` 0-hit).
- [ ] **Step 3: 통과 확인 (부분)**`cargo test -p kebab-store-sqlite` → Step 1 / 1b / 2 PASS. Step 1c 는 A5 후.
- [x] **Step 3: 통과 확인 (부분)**`cargo test -p kebab-store-sqlite --test fts` → 13/13 PASS (Step 1/1b/2 + 기존 10). Step 1c 는 A5 후.
- [ ] **Step 4: 통합 회귀 확인**`cargo test -p kebab-app search_korean` (`러스트` 3자라 trigram 으로도 통과). `search_korean.rs``해시 충돌` multi-token assert 추가 (A5 후 통과).