@@ -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
131174WHERE search_term LIKE ' 石%'
132175ORDER 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
243286WHERE search_term LIKE ' 石%' ;
244- -- key: idx_search_term (using index)
287+ -- key: idx_cbdb_name_search_term (using index)
245288
246289-- ❌ 不使用索引
247290WHERE 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
561618class 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