Skip to content

Commit edfa2b9

Browse files
committed
更新姓名搜尋倒排表設計
1 parent e07dc68 commit edfa2b9

1 file changed

Lines changed: 127 additions & 59 deletions

File tree

NAME_SEARCH_PERFORMANCE_IMPROVEMENT.md

Lines changed: 127 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -77,26 +77,29 @@ WHERE search_term LIKE '石%' -- 預計 3ms
7777

7878
## 三、表結構設計
7979

80-
### CBDB_NAME_SEARCH
80+
### CBDB_NAME_SEARCH_FTS
8181

8282
```sql
83-
CREATE TABLE CBDB_NAME_SEARCH (
84-
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '自增主鍵',
85-
c_personid INT NOT NULL COMMENT '人物 ID',
86-
name_type TINYINT NOT NULL COMMENT '名字類型:1=本名, 2=字, 3=號, 4=別名',
87-
search_term VARCHAR(100) NOT NULL COMMENT '搜尋詞(拆分後的後綴)',
88-
full_name VARCHAR(100) NOT NULL COMMENT '完整名字(用於顯示和排序)',
89-
created_at TIMESTAMP NULL DEFAULT NULL,
90-
updated_at TIMESTAMP NULL DEFAULT NULL,
91-
92-
INDEX idx_search_term (search_term, c_personid),
93-
INDEX idx_personid (c_personid),
94-
INDEX idx_name_type (name_type)
95-
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
96-
COMMENT='姓名搜尋倒排索引表';
83+
CREATE TABLE CBDB_NAME_SEARCH_FTS (
84+
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
85+
c_personid INT NOT NULL,
86+
name_type_code SMALLINT UNSIGNED NOT NULL,
87+
name_type_desc VARCHAR(32) NOT NULL,
88+
name_type_desc_chn VARCHAR(32) NOT NULL,
89+
search_term VARCHAR(100) NOT NULL,
90+
full_name VARCHAR(100) NOT NULL,
91+
source VARCHAR(32) NOT NULL,
92+
source_key VARCHAR(255) NULL,
93+
is_simplified TINYINT(1) NOT NULL DEFAULT 0,
94+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
95+
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
96+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
97+
CREATE INDEX idx_cbdb_name_search_term ON CBDB_NAME_SEARCH_FTS(search_term, c_personid);
98+
CREATE INDEX idx_cbdb_name_person ON CBDB_NAME_SEARCH_FTS(c_personid);
99+
CREATE INDEX idx_cbdb_name_type ON CBDB_NAME_SEARCH_FTS(name_type_code);
97100
```
98101

99-
### name_type 欄位說明
102+
### name_type_code 欄位說明與對應描述
100103

101104
|| 類型 | 來源表/欄位 |
102105
|----|------|------------|
@@ -105,34 +108,74 @@ COMMENT='姓名搜尋倒排索引表';
105108
| 3 || `ALTNAME_DATA` (c_alt_name_type_code=5) |
106109
| 4 | 其他別名 | `ALTNAME_DATA` (其他 type_code) |
107110

111+
對應的描述欄位建議如下:
112+
113+
| 欄位 | 範例值 | 說明 |
114+
|------|--------|------|
115+
| `name_type_desc` | `main_name` / `zi` / `hao` / `altname` | 方便後端依英文字串判斷 |
116+
| `name_type_desc_chn` | `本名` / `` / `` / `別名` | 直接呈現在 UI 或報表 |
117+
| `source` | `biog_main` / `altname_data` | 標記資料來源表 |
118+
| `source_key` | `biog_main:1762` / `altname:1762-4-介甫` | 以字串保存來源主鍵或複合鍵 |
119+
| `is_simplified` | 0 / 1 | 0=原文,1=簡化字版本 |
120+
121+
> `ALTNAME_DATA` 無自增鍵,建議統一使用 `altname:{c_personid}-{c_alt_name_type_code}-{c_alt_name_chn}` 形式存入 `source_key`,能唯一對應原始別名紀錄;本名則可用 `biog_main:{c_personid}`
122+
123+
### 繁簡轉換:CBDB_TRAD_SIMP_MAP
124+
125+
為了支援 `is_simplified=1` 的倒排記錄,建議新增一張通用對照表儲存 OpenCC 釋出的繁簡映射:
126+
127+
```sql
128+
CREATE TABLE CBDB_TRAD_SIMP_MAP (
129+
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
130+
trad_char CHAR(1) NOT NULL,
131+
simp_char CHAR(1) NOT NULL,
132+
variant_set VARCHAR(64) NOT NULL DEFAULT 'OpenCC',
133+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
134+
UNIQUE KEY uniq_trad_variant (trad_char, variant_set),
135+
KEY idx_simp_char (simp_char)
136+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
137+
```
138+
139+
匯入流程:
140+
1. 下載對應的 OpenCC `TSCharacters.txt`/`STCharacters.txt`
141+
2. 將每組字拆成 `trad_char → simp_char` 對(多對多則拆成多列)。
142+
3. 透過簡單的 seed/command 將資料批量寫入 CBDB_TRAD_SIMP_MAP。
143+
144+
使用方式:
145+
- 在產生倒排 suffix 時,先寫入繁體字版本(`is_simplified=0`),再以 CBDB_TRAD_SIMP_MAP 查詢對應簡體字重新組合字串、設定 `is_simplified=1` 後寫入同一筆搜尋詞。
146+
- 查詢時若輸入為簡體,可直接 `WHERE search_term LIKE :input% AND is_simplified=1`;若輸入繁體則比對 `is_simplified=0`。也可在應用層將輸入轉為繁簡各一版並合併結果,以兼容混合輸入。
147+
108148
### 示例資料
109149

110150
```sql
111151
-- 王安石(1021-1086,北宋政治家)
112152
-- 本名:王安石,字:介甫,號:半山
113153

114-
INSERT INTO CBDB_NAME_SEARCH VALUES
154+
INSERT INTO CBDB_NAME_SEARCH_FTS (
155+
c_personid, name_type_code, name_type_desc, name_type_desc_chn,
156+
search_term, full_name, source, source_key, is_simplified
157+
) VALUES
115158
-- 本名(完整 + 後綴)
116-
(NULL, 1001, 1, '王安石', '王安石', NOW(), NOW()),
117-
(NULL, 1001, 1, '安石', '王安石', NOW(), NOW()),
118-
(NULL, 1001, 1, '', '王安石', NOW(), NOW()),
159+
(1762, 1, 'main_name', '本名', '王安石', '王安石', 'biog_main', 'biog_main:1762', 0),
160+
(1762, 1, 'main_name', '本名', '安石', '王安石', 'biog_main', 'biog_main:1762', 0),
161+
(1762, 1, 'main_name', '本名', '', '王安石', 'biog_main', 'biog_main:1762', 0),
119162

120163
-- 字(完整 + 後綴)
121-
(NULL, 1001, 2, '介甫', '介甫', NOW(), NOW()),
122-
(NULL, 1001, 2, '', '介甫', NOW(), NOW()),
164+
(1762, 2, 'zi', '', '介甫', '介甫', 'altname_data', 'altname:1762-4-介甫', 0),
165+
(1762, 2, 'zi', '', '', '介甫', 'altname_data', 'altname:1762-4-介甫', 0),
123166

124167
-- 號(完整)
125-
(NULL, 1001, 3, '半山', '半山', NOW(), NOW()),
126-
(NULL, 1001, 3, '', '半山', NOW(), NOW());
168+
(1762, 3, 'hao', '', '半山', '半山', 'altname_data', 'altname:1762-5-半山', 0),
169+
(1762, 3, 'hao', '', '', '半山', 'altname_data', 'altname:1762-5-半山', 0);
127170

128171
-- 查詢「石」
129-
SELECT DISTINCT c_personid, full_name, name_type
130-
FROM CBDB_NAME_SEARCH
172+
SELECT DISTINCT c_personid, full_name, name_type_code
173+
FROM CBDB_NAME_SEARCH_FTS
131174
WHERE search_term LIKE '石%'
132175
ORDER BY LENGTH(search_term) DESC, c_personid;
133176

134177
-- 結果:
135-
-- 1001, "王安石", 1 (匹配到 search_term="石")
178+
-- 1762, "王安石", 1 (匹配到 search_term="石")
136179
```
137180

138181
---
@@ -233,15 +276,15 @@ c_name_rm: "Sū Shì" → ["Sū Shì"]
233276

234277
```sql
235278
-- 主查詢索引(複合索引)
236-
CREATE INDEX idx_search_term ON CBDB_NAME_SEARCH (search_term, c_personid);
279+
CREATE INDEX idx_cbdb_name_search_term ON CBDB_NAME_SEARCH_FTS (search_term, c_personid);
237280
```
238281

239282
**查詢最佳化:**
240283
```sql
241284
-- ✅ 使用索引
242-
EXPLAIN SELECT * FROM CBDB_NAME_SEARCH
285+
EXPLAIN SELECT * FROM CBDB_NAME_SEARCH_FTS
243286
WHERE search_term LIKE '石%';
244-
-- key: idx_search_term (using index)
287+
-- key: idx_cbdb_name_search_term (using index)
245288

246289
-- ❌ 不使用索引
247290
WHERE search_term LIKE '%石%';
@@ -252,10 +295,10 @@ WHERE search_term LIKE '%石%';
252295

253296
```sql
254297
-- 按人物 ID 查詢(維護時使用)
255-
CREATE INDEX idx_personid ON CBDB_NAME_SEARCH (c_personid);
298+
CREATE INDEX idx_cbdb_name_person ON CBDB_NAME_SEARCH_FTS (c_personid);
256299

257300
-- 按名字類型過濾
258-
CREATE INDEX idx_name_type ON CBDB_NAME_SEARCH (name_type);
301+
CREATE INDEX idx_cbdb_name_type ON CBDB_NAME_SEARCH_FTS (name_type_code);
259302
```
260303

261304
### 5.3 索引大小估算
@@ -295,7 +338,7 @@ public static function namesByQuery(Request $request, $num=20)
295338
}
296339

297340
// 新增:使用倒排表搜尋
298-
$personIds = DB::table('CBDB_NAME_SEARCH')
341+
$personIds = DB::table('CBDB_NAME_SEARCH_FTS')
299342
->where('search_term', 'LIKE', $request->q . '%')
300343
->orderByRaw('LENGTH(search_term) ASC') // 優先精確匹配
301344
->limit(500) // 限制最多 500 個候選人
@@ -373,7 +416,7 @@ class RebuildNameSearchIndex extends Command
373416
{
374417
if ($this->option('force')) {
375418
$this->warn('清空現有索引資料...');
376-
DB::table('CBDB_NAME_SEARCH')->truncate();
419+
DB::table('CBDB_NAME_SEARCH_FTS')->truncate();
377420
}
378421

379422
$chunk = (int) $this->option('chunk');
@@ -396,6 +439,12 @@ class RebuildNameSearchIndex extends Command
396439
protected function indexPerson($person)
397440
{
398441
$records = [];
442+
$typeMeta = [
443+
1 => ['desc' => 'main_name', 'desc_chn' => '本名', 'source' => 'biog_main'],
444+
2 => ['desc' => 'zi', 'desc_chn' => '字', 'source' => 'altname_data'],
445+
3 => ['desc' => 'hao', 'desc_chn' => '號', 'source' => 'altname_data'],
446+
4 => ['desc' => 'altname', 'desc_chn' => '別名', 'source' => 'altname_data'],
447+
];
399448

400449
// 1. 本名
401450
if ($person->c_surname && $person->c_mingzi) {
@@ -407,11 +456,15 @@ class RebuildNameSearchIndex extends Command
407456
foreach ($suffixes as $suffix) {
408457
$records[] = [
409458
'c_personid' => $person->c_personid,
410-
'name_type' => 1,
459+
'name_type_code' => 1,
460+
'name_type_desc' => $typeMeta[1]['desc'],
461+
'name_type_desc_chn' => $typeMeta[1]['desc_chn'],
411462
'search_term' => $suffix,
412463
'full_name' => $person->c_name_chn,
464+
'source' => $typeMeta[1]['source'],
465+
'source_key' => "biog_main:{$person->c_personid}",
466+
'is_simplified' => 0,
413467
'created_at' => now(),
414-
'updated_at' => now(),
415468
];
416469
}
417470
}
@@ -433,17 +486,21 @@ class RebuildNameSearchIndex extends Command
433486
foreach ($suffixes as $suffix) {
434487
$records[] = [
435488
'c_personid' => $person->c_personid,
436-
'name_type' => $type,
489+
'name_type_code' => $type,
490+
'name_type_desc' => $typeMeta[$type]['desc'],
491+
'name_type_desc_chn' => $typeMeta[$type]['desc_chn'],
437492
'search_term' => $suffix,
438493
'full_name' => $alt->c_alt_name_chn,
494+
'source' => $typeMeta[$type]['source'],
495+
'source_key' => "altname:{$person->c_personid}-{$alt->c_alt_name_type_code}-{$alt->c_alt_name_chn}",
496+
'is_simplified' => 0,
439497
'created_at' => now(),
440-
'updated_at' => now(),
441498
];
442499
}
443500
}
444501

445502
if (!empty($records)) {
446-
DB::table('CBDB_NAME_SEARCH')->insert($records);
503+
DB::table('CBDB_NAME_SEARCH_FTS')->insert($records);
447504
}
448505
}
449506

@@ -470,15 +527,15 @@ class RebuildNameSearchIndex extends Command
470527

471528
protected function displayStatistics()
472529
{
473-
$stats = DB::table('CBDB_NAME_SEARCH')
474-
->selectRaw('name_type, COUNT(*) as count')
475-
->groupBy('name_type')
530+
$stats = DB::table('CBDB_NAME_SEARCH_FTS')
531+
->selectRaw('name_type_code, COUNT(*) as count')
532+
->groupBy('name_type_code')
476533
->get();
477534

478535
$this->table(
479536
['類型', '記錄數'],
480537
$stats->map(fn($s) => [
481-
match($s->name_type) {
538+
match($s->name_type_code) {
482539
1 => '本名',
483540
2 => '字',
484541
3 => '號',
@@ -488,7 +545,7 @@ class RebuildNameSearchIndex extends Command
488545
])
489546
);
490547

491-
$total = DB::table('CBDB_NAME_SEARCH')->count();
548+
$total = DB::table('CBDB_NAME_SEARCH_FTS')->count();
492549
$this->info("總計:" . number_format($total) . " 條倒排記錄");
493550
}
494551
}
@@ -560,6 +617,13 @@ use Illuminate\Support\Facades\DB;
560617

561618
class NameSearchIndexService
562619
{
620+
protected $typeMeta = [
621+
1 => ['desc' => 'main_name', 'desc_chn' => '本名', 'source' => 'biog_main'],
622+
2 => ['desc' => 'zi', 'desc_chn' => '字', 'source' => 'altname_data'],
623+
3 => ['desc' => 'hao', 'desc_chn' => '號', 'source' => 'altname_data'],
624+
4 => ['desc' => 'altname', 'desc_chn' => '別名', 'source' => 'altname_data'],
625+
];
626+
563627
public function indexPerson($person)
564628
{
565629
// 同 RebuildNameSearchIndex::indexPerson()
@@ -569,9 +633,9 @@ class NameSearchIndexService
569633
{
570634
DB::transaction(function() use ($person) {
571635
// 刪除舊索引
572-
DB::table('CBDB_NAME_SEARCH')
636+
DB::table('CBDB_NAME_SEARCH_FTS')
573637
->where('c_personid', $person->c_personid)
574-
->where('name_type', 1) // 只刪除本名
638+
->where('name_type_code', 1) // 只刪除本名
575639
->delete();
576640

577641
// 重建索引
@@ -581,12 +645,12 @@ class NameSearchIndexService
581645

582646
public function removePerson(int $personId)
583647
{
584-
DB::table('CBDB_NAME_SEARCH')
648+
DB::table('CBDB_NAME_SEARCH_FTS')
585649
->where('c_personid', $personId)
586650
->delete();
587651
}
588652

589-
public function indexAltname(int $personId, int $typeCode, string $altname)
653+
public function indexAltname(int $personId, int $typeCode, string $altname, ?string $sourceKey = null)
590654
{
591655
$type = match($typeCode) {
592656
4 => 2,
@@ -600,15 +664,19 @@ class NameSearchIndexService
600664
foreach ($suffixes as $suffix) {
601665
$records[] = [
602666
'c_personid' => $personId,
603-
'name_type' => $type,
667+
'name_type_code' => $type,
668+
'name_type_desc' => $this->typeMeta[$type]['desc'],
669+
'name_type_desc_chn' => $this->typeMeta[$type]['desc_chn'],
604670
'search_term' => $suffix,
605671
'full_name' => $altname,
672+
'source' => $this->typeMeta[$type]['source'],
673+
'source_key' => $sourceKey ?? "altname:{$personId}-{$typeCode}-{$altname}",
674+
'is_simplified' => 0,
606675
'created_at' => now(),
607-
'updated_at' => now(),
608676
];
609677
}
610678

611-
DB::table('CBDB_NAME_SEARCH')->insert($records);
679+
DB::table('CBDB_NAME_SEARCH_FTS')->insert($records);
612680
}
613681

614682
protected function splitAltname(string $altname): array
@@ -709,22 +777,22 @@ class NameSearchIndexController extends Controller
709777
protected function getStatistics()
710778
{
711779
$totalPersons = DB::table('BIOG_MAIN')->count();
712-
$totalIndexed = DB::table('CBDB_NAME_SEARCH')
780+
$totalIndexed = DB::table('CBDB_NAME_SEARCH_FTS')
713781
->distinct('c_personid')
714782
->count('c_personid');
715783

716-
$byType = DB::table('CBDB_NAME_SEARCH')
717-
->selectRaw('name_type, COUNT(*) as count')
718-
->groupBy('name_type')
784+
$byType = DB::table('CBDB_NAME_SEARCH_FTS')
785+
->selectRaw('name_type_code, COUNT(*) as count')
786+
->groupBy('name_type_code')
719787
->get()
720-
->mapWithKeys(fn($item) => [$item->name_type => $item->count]);
788+
->mapWithKeys(fn($item) => [$item->name_type_code => $item->count]);
721789

722790
$tableSize = DB::select("
723791
SELECT
724792
ROUND(((data_length + index_length) / 1024 / 1024), 2) AS size_mb
725793
FROM information_schema.TABLES
726794
WHERE table_schema = DATABASE()
727-
AND table_name = 'CBDB_NAME_SEARCH'
795+
AND table_name = 'CBDB_NAME_SEARCH_FTS'
728796
")[0]->size_mb ?? 0;
729797

730798
return [
@@ -741,8 +809,8 @@ class NameSearchIndexController extends Controller
741809
'altname' => $byType[4] ?? 0,
742810
],
743811
'table_size_mb' => $tableSize,
744-
'last_updated' => DB::table('CBDB_NAME_SEARCH')
745-
->max('updated_at'),
812+
'last_updated' => DB::table('CBDB_NAME_SEARCH_FTS')
813+
->max('created_at'),
746814
];
747815
}
748816
}

0 commit comments

Comments
 (0)