实现全文检索-基于MySQL

我们经常在网站上能看到搜索框, 可以用空格分割关键词、可以用拼音搜索等…

关键字拆分为一组

例子:搜索懒人快乐, 后台程序就会把懒人快乐拆分成两个词懒人,快乐

说明:可以使用jieba-analysis结巴分词, 用法如下:

1
2
3
4
5
<dependency>
<groupId>com.huaban</groupId>
<artifactId>jieba-analysis</artifactId>
<version>1.0.2</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.kangyonggan.blog.util;


import com.huaban.analysis.jieba.JiebaSegmenter;
import com.huaban.analysis.jieba.SegToken;
import org.apache.commons.lang3.StringUtils;

import java.util.List;

/**
* @author kangyonggan
* @since 16/7/22
*/
public class FenCi {

/**
* 分词器
*/
private static final JiebaSegmenter segmenter = new JiebaSegmenter();

/**
* 中文分词, 并转成拼音
*
* @param data
* @return
*/
public static String process(String data) {
if (StringUtils.isEmpty(data)) {
return "";
}

data = data.replaceAll("\\s", " ");
data = data.replaceAll("'", " ");

List<SegToken> list = segmenter.process(data, JiebaSegmenter.SegMode.INDEX);

StringBuilder sb = new StringBuilder();
list.forEach(segToken -> sb.append(PinYin.converterToSpellWithMuti(segToken.word)).append(","));
sb.deleteCharAt(sb.lastIndexOf(","));

return sb.toString();
}

}

转化为拼音

例子:懒人,快乐会被后台程序转化为lanren,kuaile,kuaiyue,之所以不是lanren,kuaile而是lanren,kuaile,kuaiyue, 是因为是多音字。

说明:可以使用pinyin4j把汉字转化为拼音, 用法如下:

1
2
3
4
5
<dependency>
<groupId>com.belerweb</groupId>
<artifactId>pinyin4j</artifactId>
<version>2.5.1</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
package com.kangyonggan.blog.util;

import net.sourceforge.pinyin4j.PinyinHelper;
import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType;
import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat;
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType;
import net.sourceforge.pinyin4j.format.exception.BadHanyuPinyinOutputFormatCombination;

import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

/**
* @author kangyonggan
* @since 16/7/22
*/
public class PinYin {

/**
* 汉字转换位汉语拼音首字母,英文字符不变,特殊字符丢失 支持多音字,生成方式如(长沙市长:cssc,zssz,zssc,cssz)
*
* @param chines 汉字
* @return 拼音
*/
public static String converterToFirstSpell(String chines) {
StringBuffer pinyinName = new StringBuffer();
char[] nameChar = chines.toCharArray();
HanyuPinyinOutputFormat defaultFormat = new HanyuPinyinOutputFormat();
defaultFormat.setCaseType(HanyuPinyinCaseType.LOWERCASE);
defaultFormat.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
for (int i = 0; i < nameChar.length; i++) {
if (nameChar[i] > 128) {
try {
// 取得当前汉字的所有全拼
String[] strs = PinyinHelper.toHanyuPinyinStringArray(
nameChar[i], defaultFormat);
if (strs != null) {
for (int j = 0; j < strs.length; j++) {
// 取首字母
pinyinName.append(strs[j].charAt(0));
if (j != strs.length - 1) {
pinyinName.append(",");
}
}
}
// else {
// pinyinName.append(nameChar[i]);
// }
} catch (BadHanyuPinyinOutputFormatCombination e) {
e.printStackTrace();
}
} else {
pinyinName.append(nameChar[i]);
}
pinyinName.append(" ");
}
// return pinyinName.toString();
return parseTheChineseByObject(discountTheChinese(pinyinName.toString()));
}

/**
* 汉字转换位汉语全拼,英文字符不变,特殊字符丢失
* 不支持多音字,生成方式如(重当参:zhongdangcen)
*
* @param chines 汉字
* @return 拼音
*/
public static String converterToSpell(String chines) {
StringBuffer pinyinName = new StringBuffer();
char[] nameChar = chines.toCharArray();
HanyuPinyinOutputFormat defaultFormat = new HanyuPinyinOutputFormat();
defaultFormat.setCaseType(HanyuPinyinCaseType.LOWERCASE);
defaultFormat.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
for (int i = 0; i < nameChar.length; i++) {
if (nameChar[i] > 128) {
try {
// 取得当前汉字的所有全拼
String[] strs = PinyinHelper.toHanyuPinyinStringArray(
nameChar[i], defaultFormat);
if (strs != null && strs.length > 0) {
pinyinName.append(strs[0]);
}
} catch (BadHanyuPinyinOutputFormatCombination e) {
e.printStackTrace();
}
} else {
pinyinName.append(nameChar[i]);
}
pinyinName.append(" ");
}
// return pinyinName.toString();
return parseTheChineseByObject(discountTheChinese(pinyinName.toString()));
}

/**
* 汉字转换位汉语全拼,英文字符不变,特殊字符丢失
* 支持多音字,生成方式如(重当参:zhongdangcen,zhongdangcan,chongdangcen
* ,chongdangshen,zhongdangshen,chongdangcan)
*
* @param chines 汉字
* @return 拼音
*/
public static String converterToSpellWithMuti(String chines) {
StringBuffer pinyinName = new StringBuffer();
char[] nameChar = chines.toCharArray();
HanyuPinyinOutputFormat defaultFormat = new HanyuPinyinOutputFormat();
defaultFormat.setCaseType(HanyuPinyinCaseType.LOWERCASE);
defaultFormat.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
for (int i = 0; i < nameChar.length; i++) {
if (nameChar[i] > 128) {
try {
// 取得当前汉字的所有全拼
String[] strs = PinyinHelper.toHanyuPinyinStringArray(
nameChar[i], defaultFormat);
if (strs != null) {
for (int j = 0; j < strs.length; j++) {
pinyinName.append(strs[j]);
if (j != strs.length - 1) {
pinyinName.append(",");
}
}
}
} catch (BadHanyuPinyinOutputFormatCombination e) {
e.printStackTrace();
}
} else {
pinyinName.append(nameChar[i]);
}
pinyinName.append(" ");
}
// return pinyinName.toString();
return parseTheChineseByObject(discountTheChinese(pinyinName.toString()));
}

/**
* 去除多音字重复数据
*
* @param theStr
* @return
*/
private static List<Map<String, Integer>> discountTheChinese(String theStr) {
// 去除重复拼音后的拼音列表
List<Map<String, Integer>> mapList = new ArrayList<Map<String, Integer>>();
// 用于处理每个字的多音字,去掉重复
Map<String, Integer> onlyOne = null;
String[] firsts = theStr.split(" ");
// 读出每个汉字的拼音
for (String str : firsts) {
onlyOne = new Hashtable();
String[] china = str.split(",");
// 多音字处理
for (String s : china) {
Integer count = onlyOne.get(s);
if (count == null) {
onlyOne.put(s, new Integer(1));
} else {
onlyOne.remove(s);
count++;
onlyOne.put(s, count);
}
}
mapList.add(onlyOne);
}
return mapList;
}

/**
* 解析并组合拼音,对象合并方案(推荐使用)
*
* @return
*/
private static String parseTheChineseByObject(
List<Map<String, Integer>> list) {
Map<String, Integer> first = null; // 用于统计每一次,集合组合数据
// 遍历每一组集合
for (int i = 0; i < list.size(); i++) {
// 每一组集合与上一次组合的Map
Map<String, Integer> temp = new Hashtable<String, Integer>();
// 第一次循环,first为空
if (first != null) {
// 取出上次组合与此次集合的字符,并保存
for (String s : first.keySet()) {
for (String s1 : list.get(i).keySet()) {
String str = s + s1;
temp.put(str, 1);
}
}
// 清理上一次组合数据
if (temp != null && temp.size() > 0) {
first.clear();
}
} else {
for (String s : list.get(i).keySet()) {
String str = s;
temp.put(str, 1);
}
}
// 保存组合数据以便下次循环使用
if (temp != null && temp.size() > 0) {
first = temp;
}
}
String returnStr = "";
if (first != null) {
// 遍历取出组合字符串
for (String str : first.keySet()) {
returnStr += (str + ",");
}
}
if (returnStr.length() > 0) {
returnStr = returnStr.substring(0, returnStr.length() - 1);
}
return returnStr;
}
}

单独创建一个表用于全文检索

例子:现在有一个文章表article, 它有titlebody等字段,我们想要实现的效果是~如果我们检索的关键字在title或body中, 那么我们就把这条文章记录检索出来。

  • 创建一个用于检索的表article_index, 它有article_idtitlebody等字段, 其中title和body字段要给的大一点, 因为这两个字段将要存储文章表的title和body所对应的拼音,具体给多大请自行分析。
  • ALTER TABLE article_index ENGINE = MyISAM; 默认是InnoDB。MyISAM:支持全文索引, 但不支持事务。InnoDB:支持事务, 但不支持全文索引。
  • ALTER TABLE article_index ADD FULLTEXT INDEX (title, body); 给title和body字段添加全文索引。
  • 在发表一篇新的文章时, 把文章的title结巴分词分成多个词, 再用pinyin4j转化为拼音, body字段也做相同处理,最后把转化后的article_id、title和body存储到article_index表中,用于全文检索。

说明:关于全文索引的一些常用知识SHOW VARIABLES LIKE \'ft_min_word_len\';REPAIR TABLE article_index QUICK; 作用以及用法请自行学习。

全文检索

例子:检索的关键字为懒人快乐

  • 先把分词, 变为懒人,快乐
  • 再把汉字变拼音lanren,kuaile,kuaiyue
  • 检索:SELECT * FROM article_index WHERE match(title, body) against(\'lanren,kuaile,kuaiyue\' IN BOOLEAN MODE)
  • 根据检索到article_id去文章表查询对应的文章

说明:此检索是基于MySQL的, 其他数据库的检索请自行学习, 检索语句还有很多模式, 请自行学习。

结巴分词的时候会返回偏移量,所以你可以根据偏移量定位到文章中的关键词, 然后变成红色。