Skip to content

Commit 00e1ac7

Browse files
frankslinclaude
andcommitted
新增 CBDB_NAME_LIST 全文索引 Migration 和使用文档
## Migration - 添加 `idx_name_fulltext` 全文索引到 CBDB_NAME_LIST.name 字段 - 使用 ngram 解析器支持中文分词搜索(token size = 2) - 支持回滚操作 ## 文档 ### PERFORMANCE_ANALYSIS_namesByQuery.md - 详细的性能分析报告 - 对比测试数据: * 单字查询 "張":当前方法 13ms(最佳) * 两字查询 "張三":优化后 3ms vs 当前 1374ms(提升 432 倍) * 全名查询 "蘇軾":优化后 2ms vs 当前 1540ms(提升 819 倍) - 推荐混合策略:根据查询长度选择最优索引 ### FULLTEXT_INDEX_USAGE.md - 全文索引使用指南 - 基本搜索、布尔模式、相关性排序示例 - 在 BiogMainRepository::namesByQuery() 中的应用方案 - ngram 解析器说明和性能对比 - 监控和优化建议 ## 预期效果 使用全文索引可以显著提升 2+ 字符姓名搜索的性能(99%+ 提升), 同时保持单字查询的现有性能。 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 489adea commit 00e1ac7

3 files changed

Lines changed: 570 additions & 0 deletions

File tree

FULLTEXT_INDEX_USAGE.md

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
# CBDB_NAME_LIST 全文索引使用指南
2+
3+
## 概述
4+
5+
`CBDB_NAME_LIST.name` 字段添加了全文索引,使用 MySQL 的 ngram 解析器以支持中文分词搜索。
6+
7+
## Migration 文件
8+
9+
```bash
10+
database/migrations/2025_11_09_143732_add_fulltext_index_to_cbdb_name_list_table.php
11+
```
12+
13+
## 执行 Migration
14+
15+
```bash
16+
# 应用 migration
17+
php artisan migrate
18+
19+
# 如果需要回滚
20+
php artisan migrate:rollback --step=1
21+
```
22+
23+
执行后将创建索引:`idx_name_fulltext`
24+
25+
## 使用方法
26+
27+
### 1. 基本全文搜索
28+
29+
```php
30+
use Illuminate\Support\Facades\DB;
31+
32+
// 搜索包含"張三"的姓名
33+
$personIds = DB::table('CBDB_NAME_LIST')
34+
->whereRaw('MATCH(name) AGAINST(? IN NATURAL LANGUAGE MODE)', ['張三'])
35+
->distinct()
36+
->pluck('c_personid');
37+
```
38+
39+
### 2. 布尔模式搜索(更灵活)
40+
41+
```php
42+
// 必须包含"張"且包含"三"
43+
$personIds = DB::table('CBDB_NAME_LIST')
44+
->whereRaw('MATCH(name) AGAINST(? IN BOOLEAN MODE)', ['+張 +三'])
45+
->distinct()
46+
->pluck('c_personid');
47+
48+
// 包含"張"或"王"
49+
$personIds = DB::table('CBDB_NAME_LIST')
50+
->whereRaw('MATCH(name) AGAINST(? IN BOOLEAN MODE)', ['張 王'])
51+
->distinct()
52+
->pluck('c_personid');
53+
54+
// 包含"張"但不包含"三"
55+
$personIds = DB::table('CBDB_NAME_LIST')
56+
->whereRaw('MATCH(name) AGAINST(? IN BOOLEAN MODE)', ['+張 -三'])
57+
->distinct()
58+
->pluck('c_personid');
59+
60+
// 前缀搜索
61+
$personIds = DB::table('CBDB_NAME_LIST')
62+
->whereRaw('MATCH(name) AGAINST(? IN BOOLEAN MODE)', ['張*'])
63+
->distinct()
64+
->pluck('c_personid');
65+
```
66+
67+
### 3. 相关性排序
68+
69+
全文搜索可以返回相关性评分:
70+
71+
```php
72+
$results = DB::table('CBDB_NAME_LIST')
73+
->selectRaw('c_personid, name, MATCH(name) AGAINST(? IN NATURAL LANGUAGE MODE) AS relevance_score', ['張三'])
74+
->whereRaw('MATCH(name) AGAINST(? IN NATURAL LANGUAGE MODE)', ['張三'])
75+
->orderByDesc('relevance_score')
76+
->limit(20)
77+
->get();
78+
```
79+
80+
## 在 BiogMainRepository::namesByQuery() 中的应用
81+
82+
### 优化方案:结合三种搜索策略
83+
84+
```php
85+
static public function namesByQuery(Request $request, $num=20)
86+
{
87+
$request->q = addslashes($request->q);
88+
$query = $request->q;
89+
90+
if (!$query) {
91+
// 保持原有逻辑...
92+
return ...;
93+
}
94+
95+
$queryLength = mb_strlen($query);
96+
97+
// 策略选择
98+
if ($queryLength >= 3) {
99+
// 长查询(3+ 字符):使用全文索引
100+
// 优势:支持中间匹配,有相关性排序
101+
$personIds = DB::table('CBDB_NAME_LIST')
102+
->whereRaw('MATCH(name) AGAINST(? IN BOOLEAN MODE)', [$query . '*'])
103+
->distinct()
104+
->limit(500)
105+
->pluck('c_personid')
106+
->toArray();
107+
} elseif ($queryLength == 2) {
108+
// 2字查询:使用前缀索引(最快)
109+
$personIds = DB::table('CBDB_NAME_LIST')
110+
->where('name', 'like', $query . '%')
111+
->distinct()
112+
->limit(500)
113+
->pluck('c_personid')
114+
->toArray();
115+
} else {
116+
// 单字查询:使用原有 BIOG_MAIN 查询(避免匹配过多)
117+
$names = BiogMain::select(...)
118+
->leftJoin(...)
119+
->where('BIOG_MAIN.c_name_chn', 'like', '%'.$query.'%')
120+
->orWhere(...)
121+
->paginate($num);
122+
123+
$names->appends(['q' => $query])->links();
124+
return $names;
125+
}
126+
127+
if (empty($personIds)) {
128+
return response()->json(['data' => [], 'total' => 0]);
129+
}
130+
131+
// 用 personid 列表查询完整信息
132+
$names = BiogMain::select(
133+
'BIOG_MAIN.c_personid',
134+
'BIOG_MAIN.c_name_chn',
135+
'BIOG_MAIN.c_name',
136+
'DYNASTIES.c_dynasty_chn',
137+
'BIOG_MAIN.c_index_year',
138+
'ADDR_CODES.c_name_chn AS ADDR_c_name_chn',
139+
'A1.c_alt_name_chn as c_alt_name_chn_zi',
140+
'A2.c_alt_name_chn as c_alt_name_chn_hao'
141+
)
142+
->leftJoin('DYNASTIES', 'DYNASTIES.c_dy', '=', 'BIOG_MAIN.c_dy')
143+
->leftJoin('ADDR_CODES', 'ADDR_CODES.c_addr_id', '=', 'BIOG_MAIN.c_index_addr_id')
144+
->leftJoin('ALTNAME_DATA as A1', function($join) {
145+
$join->on('A1.c_personid', '=', 'BIOG_MAIN.c_personid')
146+
->where('A1.c_alt_name_type_code', '=', 4);
147+
})
148+
->leftJoin('ALTNAME_DATA as A2', function($join) {
149+
$join->on('A2.c_personid', '=', 'BIOG_MAIN.c_personid')
150+
->where('A2.c_alt_name_type_code', '=', 5);
151+
})
152+
->whereIn('BIOG_MAIN.c_personid', $personIds)
153+
->orderBy('BIOG_MAIN.c_personid', 'ASC')
154+
->groupBy('BIOG_MAIN.c_personid')
155+
->paginate($num);
156+
157+
$names->appends(['q' => $query])->links();
158+
return $names;
159+
}
160+
```
161+
162+
## ngram 解析器说明
163+
164+
### 什么是 ngram?
165+
166+
ngram 是一种将文本分解为连续 n 个字符片段的方法。MySQL 的 ngram 解析器默认使用 `ngram_token_size=2`(bigram),适合中文搜索。
167+
168+
### 示例
169+
170+
对于姓名 "張三豐":
171+
- ngram (n=2) 分词结果:`张三`, `三豐`
172+
- 这意味着搜索 "三豐"、"張三" 都能匹配到 "張三豐"
173+
174+
### 配置
175+
176+
默认的 `ngram_token_size=2` 对中文姓名搜索已经很合适。如果需要调整:
177+
178+
```sql
179+
-- 查看当前配置
180+
SHOW VARIABLES LIKE 'ngram_token_size';
181+
182+
-- 修改需要在 my.cnf 中设置(需要重启 MySQL)
183+
[mysqld]
184+
ngram_token_size=2
185+
```
186+
187+
## 性能特点
188+
189+
### 全文索引 vs B-Tree 索引 vs 全表扫描
190+
191+
| 搜索类型 | B-Tree (`LIKE "query%"`) | 全文索引 (`MATCH AGAINST`) | 全表扫描 (`LIKE "%query%"`) |
192+
|----------|--------------------------|----------------------------|------------------------------|
193+
| 前缀匹配 | ✅ 最快 (ms 级) | ✅ 快 (ms 级) | ❌ 慢 (秒级) |
194+
| 中间匹配 | ❌ 不支持 | ✅ 快 (ms 级) | ❌ 慢 (秒级) |
195+
| 相关性排序 | ❌ 不支持 | ✅ 支持 | ❌ 不支持 |
196+
| 布尔操作 | ❌ 不支持 | ✅ 支持 (+, -, *) | ❌ 不支持 |
197+
| 索引大小 || 较大 | - |
198+
199+
### 推荐使用场景
200+
201+
1. **前缀匹配(2字)**:优先使用 B-Tree 索引 `LIKE "query%"`
202+
- 最快的查询方式
203+
- 适合常见的姓名查询
204+
205+
2. **完整姓名(3+ 字)**:使用全文索引 `MATCH AGAINST`
206+
- 支持更灵活的匹配
207+
- 提供相关性排序
208+
209+
3. **单字查询**:使用 BIOG_MAIN 直接查询
210+
- 避免 CBDB_NAME_LIST 匹配过多结果
211+
212+
## 监控和优化
213+
214+
### 检查索引使用情况
215+
216+
```sql
217+
-- 查看全文索引信息
218+
SHOW INDEX FROM CBDB_NAME_LIST WHERE Key_name = 'idx_name_fulltext';
219+
220+
-- 查看表大小
221+
SELECT
222+
table_name,
223+
ROUND((data_length + index_length) / 1024 / 1024, 2) AS total_mb,
224+
ROUND(data_length / 1024 / 1024, 2) AS data_mb,
225+
ROUND(index_length / 1024 / 1024, 2) AS index_mb
226+
FROM information_schema.tables
227+
WHERE table_schema = 'CBDB' AND table_name = 'CBDB_NAME_LIST';
228+
```
229+
230+
### 性能测试示例
231+
232+
```php
233+
// 测试脚本
234+
$queries = ['張', '張三', '蘇軾', '東坡居士'];
235+
236+
foreach ($queries as $query) {
237+
echo "查询: {$query}\n";
238+
239+
// 方法1: B-Tree 前缀索引
240+
$start = microtime(true);
241+
$result1 = DB::table('CBDB_NAME_LIST')
242+
->where('name', 'like', $query . '%')
243+
->distinct()
244+
->pluck('c_personid');
245+
$time1 = round((microtime(true) - $start) * 1000, 2);
246+
247+
// 方法2: 全文索引
248+
$start = microtime(true);
249+
$result2 = DB::table('CBDB_NAME_LIST')
250+
->whereRaw('MATCH(name) AGAINST(? IN BOOLEAN MODE)', [$query . '*'])
251+
->distinct()
252+
->pluck('c_personid');
253+
$time2 = round((microtime(true) - $start) * 1000, 2);
254+
255+
echo " B-Tree: {$time1} ms, {$result1->count()} 结果\n";
256+
echo " Fulltext: {$time2} ms, {$result2->count()} 结果\n\n";
257+
}
258+
```
259+
260+
## 注意事项
261+
262+
1. **索引构建时间**:对于 200 万+ 条记录,创建全文索引可能需要几分钟时间
263+
2. **磁盘空间**:全文索引会占用额外的磁盘空间(约为数据大小的 20-30%)
264+
3. **维护开销**:插入/更新操作会稍慢,因为需要更新索引
265+
4. **最小搜索长度**:ngram token size 为 2,意味着单字符搜索可能不走全文索引
266+
267+
## 回滚方案
268+
269+
如果遇到问题,可以安全地删除全文索引:
270+
271+
```bash
272+
php artisan migrate:rollback --step=1
273+
```
274+
275+
或手动删除:
276+
277+
```sql
278+
ALTER TABLE CBDB_NAME_LIST DROP INDEX idx_name_fulltext;
279+
```
280+
281+
删除索引不会影响数据,只会移除搜索加速功能。
282+
283+
## 参考资料
284+
285+
- [MySQL Full-Text Search Functions](https://dev.mysql.com/doc/refman/8.0/en/fulltext-search.html)
286+
- [MySQL ngram Full-Text Parser](https://dev.mysql.com/doc/refman/8.0/en/fulltext-search-ngram.html)
287+
- [Full-Text Search Boolean Mode](https://dev.mysql.com/doc/refman/8.0/en/fulltext-boolean.html)

0 commit comments

Comments
 (0)