java 拼音搜索功能设计与实现 |
您所在的位置:网站首页 › 查找字的读音的软件 › java 拼音搜索功能设计与实现 |
前言
在搜索场景中,有下面这种需求,即搜索用户的中文拼音,简拼或全拼,甚至拼音的前几位字母时,能够快速检索出来,如下所示 我们希望得到下面这种效果 1、借助es 如果您的用户数据是放在es里面的,那么在存储用户数据的时候,考虑为用户的索引中冗余一个用户中文名称的拼音字段,那么检索的时候,可以将这个拼英字段作为搜索条件进行搜索,es对拼英提供分词的能力 2、直接在mysql中做 如果您的用户数据直接存在mysql表中,同样,冗余出一个拼音字段来,查询的时候可以考虑mysql自身的模糊匹配,或者locate函数,将符合条件的数据查询出来 以上是2种基本实现此功能的思路,但从中,可以捕捉到一个关键的信息就是,需要在入库(es或mysql)的时候,生成一个账户对应的拼音字段,这个转换是关键,这里就需要借助一个外部的组件,本文采用pinyin4j 功能设计点有了上面的基础实现思路,这还不够,还需要考虑的点包括, 该搜索功能支持哪些场景的搜索,如前缀拼音?中间任何一个拼音?全拼?中文名字简拼?如果中文姓名是多音字,又该如何?在调研了一部分真实用户的实际需求场景后发现下面的线索: 使用拼音检索希望缩小检索的范围,用户有时候会忘记目标检索对象的全名,只记得姓氏更偏向于姓氏前几位,即输入姓氏的某几位,就能给出一批大致符合条件的用户列表希望一些多音字的名字,也可以支持搜索基于上面已知的业务信息,下面就用代码实现这个功能吧 功能实现步骤前置准备 准备一张用户表,注意需要冗余一个拼音字段搭建一个springboot工程 CREATE TABLE `db_user` ( `user_id` varchar(32) NOT NULL COMMENT '用户ID', `tenant_id` varchar(32) NOT NULL COMMENT '租户ID', `realname` varchar(64) DEFAULT NULL COMMENT '昵称,表示用户真实姓名', `account` varchar(64) DEFAULT NULL COMMENT '帐号', `email` varchar(64) DEFAULT NULL COMMENT '邮箱', `mobile` varchar(32) DEFAULT NULL COMMENT '手机号', `passwd` varchar(256) NOT NULL COMMENT '密码', `skin` varchar(36) DEFAULT NULL COMMENT '皮肤', `key_word` varchar(36) DEFAULT NULL COMMENT '关键词', PRIMARY KEY (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;1、引入pinyin4j依赖 com.belerweb pinyin4j 2.5.0紧接着我们需要考虑的是,在什么样的场景下,需要将这个用户名称的拼音字段存进去呢?很容易想到,新增一个用户,或者修改用户信息的时候,所以需要提供2个基础的接口,接口实现本身并不难,也就是入库的操作 public String save(DbUser dbUser) { DbUser insertUser = new DbUser(); String userId = UUIDUtils.random(); BeanUtils.copyProperties(dbUser,insertUser); insertUser.setUserId(userId); //设置拼音字段 setKeyWordField(insertUser ); dbUserMapper.insert(insertUser); return "success"; }重点考虑的是,保存到key_word 这个字段的拼音存储姓氏,即 realname ——> key_word 的映射 ,那么就需要使用到pinyin4j的提供的相关api做转换操作了,所以接下来,我们需要提供相关的工具类,对生成key_word 的数据做转换 这个key_word 里面要存储什么样的数据呢?结合上文的业务分析,这里为了后续支持的搜索的方式更丰富,考虑存储的格式如下,以 : 黄小斌 这个名字为例,最后希望转换得到的结果是: huangxiaobin,hxb,即全拼和简拼,为了提升姓氏的检索效率,在将姓氏前缀也提取出来一起拼进去,那么最后的结果是: huangxiaobin,hxb,huang ,中间以逗号分割 2、转换工具类 package com.congge.util; import com.alibaba.dubbo.common.utils.CollectionUtils; import lombok.extern.slf4j.Slf4j; 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.HanyuPinyinVCharType; import net.sourceforge.pinyin4j.format.exception.BadHanyuPinyinOutputFormatCombination; import org.apache.commons.lang3.StringUtils; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * 中文名字转拼音工具类 * * @author zhangcy * @date 2021-11-04 */ @Slf4j public class PinYinUtils { private final static int[] li_SecPosValue = {1601, 1637, 1833, 2078, 2274, 2302, 2433, 2594, 2787, 3106, 3212, 3472, 3635, 3722, 3730, 3858, 4027, 4086, 4390, 4558, 4684, 4925, 5249, 5590}; private final static String[] lc_FirstLetter = {"a", "b", "c", "d", "e", "f", "g", "h", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "w", "x", "y", "z"}; /** * 取得给定汉字串的首字母串,即声母串 * * @param str 给定汉字串 * @return 声母串 */ public static String getAllFirstLetter(String str) { if (str == null || str.trim().length() == 0) { return ""; } String _str = ""; for (int i = 0; i < str.length(); i++) { _str = _str + getFirstLetter(str.substring(i, i + 1)); } return _str; } /** * 取得给定汉字的首字母,即声母 * * @param chinese 给定的汉字 * @return 给定汉字的声母 */ public static String getFirstLetter(String chinese) { if (chinese == null || chinese.trim().length() == 0) { return ""; } chinese = conversionStr(chinese, "GB2312", "ISO8859-1"); if (chinese.length() > 1) // 判断是不是汉字 { int li_SectorCode = (int) chinese.charAt(0); // 汉字区码 int li_PositionCode = (int) chinese.charAt(1); // 汉字位码 li_SectorCode = li_SectorCode - 160; li_PositionCode = li_PositionCode - 160; int li_SecPosCode = li_SectorCode * 100 + li_PositionCode; // 汉字区位码 if (li_SecPosCode > 1600 && li_SecPosCode < 5590) { for (int i = 0; i < 23; i++) { if (li_SecPosCode >= li_SecPosValue[i] && li_SecPosCode < li_SecPosValue[i + 1]) { chinese = lc_FirstLetter[i]; break; } } } else // 非汉字字符,如图形符号或ASCII码 { chinese = conversionStr(chinese, "ISO8859-1", "GB2312"); chinese = chinese.substring(0, 1); } } return chinese; } /** * 字符串编码转换 * * @param str 要转换编码的字符串 * @param charsetName 原来的编码 * @param toCharsetName 转换后的编码 * @return 经过编码转换后的字符串 */ public static String conversionStr(String str, String charsetName, String toCharsetName) { try { str = new String(str.getBytes(charsetName), toCharsetName); } catch (UnsupportedEncodingException ex) { System.out.println("字符串编码转换异常:" + ex.getMessage()); } return str; } /** * 首字母大写 * * @param name 参数中文字符串 * @return result * @throws {@link BadHanyuPinyinOutputFormatCombination} */ public static String getChinesePinyinFromName(String name) { String result = null; try { HanyuPinyinOutputFormat pyFormat = new HanyuPinyinOutputFormat(); pyFormat.setCaseType(HanyuPinyinCaseType.LOWERCASE); pyFormat.setToneType(HanyuPinyinToneType.WITHOUT_TONE); pyFormat.setVCharType(HanyuPinyinVCharType.WITH_V); result = PinyinHelper.toHanyuPinyinString(name, pyFormat, ""); } catch (Exception e) { e.printStackTrace(); } return result; } public static boolean isChineseName(String name) { boolean result = true; if (StringUtils.isNotEmpty(name)) { String[] strChars = name.split(""); for (String singleStr : strChars) { if (!isContainChinese(singleStr)) { result = false; break; } } } return result; } public static boolean isContainChinese(String str) { Pattern p = Pattern.compile("[\u4e00-\u9fa5]"); Matcher m = p.matcher(str); if (m.find()) { return true; } return false; } public static String getChineseFirstPingYingName(String str) { String[] split = str.split(""); return PinYinUtils.getChinesePinyinFromName(split[0]); } public static String convert(String chineseName) { String nameVar1 = getChinesePinyinFromName(chineseName); String nameVar2 = getAllFirstLetter(chineseName); String nameVar3 = getChineseFirstPingYingName(chineseName); String result = nameVar1 + "," + nameVar2 + "," + nameVar3; return result; } public static boolean isMixedStr(String realname) { String[] splitStr = realname.split(""); List allStrs = Arrays.asList(splitStr); List allLastStr = new ArrayList(); boolean hasChinese = false; for (String single : splitStr) { if (isChineseName(single)) { hasChinese = true; } else { allLastStr.add(single); } } if (hasChinese) { if (CollectionUtils.isNotEmpty(allStrs) && CollectionUtils.isNotEmpty(allLastStr) && allLastStr.size() < allStrs.size()) { hasChinese = true; } } return hasChinese; } public static void main(String[] args) { PinYinUtils pinYinUtils = new PinYinUtils(); String chineseName = "胡"; String nameVar1 = pinYinUtils.getChinesePinyinFromName(chineseName); String nameVar2 = pinYinUtils.getAllFirstLetter(chineseName); String nameVar3 = pinYinUtils.getChineseFirstPingYingName(chineseName); System.out.println(nameVar1); System.out.println(nameVar2); System.out.println(nameVar3); } private static PinYinMultiCharactersUtils pinYinMultiCharactersUtils = new PinYinMultiCharactersUtils(); /** * 如果是中文何字符串等混合过来的,只需原样解析,比如:111董aaa飞飞333 ,解析为:111dongaaafeifei333 * * @param realname * @return */ public String getMixPinyinStr(String realname) { if (StringUtils.isEmpty(realname)) { return null; } String[] splitStr = realname.split(""); StringBuilder stringBuilder = new StringBuilder(); int firstIndex = 0; for (String single : splitStr) { if (isChineseName(single)) { //只有第一个中文多音字做解析 if (firstIndex == 0 && pinYinMultiCharactersUtils.isMultiChineseWord(single)) { String chinesePinyinFromName = pinYinMultiCharactersUtils.getMultiCharactersPinYin(single); stringBuilder.append(chinesePinyinFromName); continue; } String chinesePinyinFromName = getChinesePinyinFromName(single); stringBuilder.append(chinesePinyinFromName); firstIndex++; } else { stringBuilder.append(single); } } return stringBuilder.toString(); } }关于工具类中的一些方法,通过注释想必大家也能看懂,下面要重点说下,如何使用这个工具类呢?还是回到上面那个saveUser的方法中,如何设置这个keyWord的属性值上面来,请看下面这个方法,我们以这个方法为例做深入的剖析 private void setKeyWordField(DbUser userRequest) { if(StringUtils.isEmpty(userRequest.getRealname())){ return; } /** * 1、pinYinUtils.isChineseName 判断传入过来的名称是否是中文呢?如果全部是中文的话做基础的解析 * 2、pinYinUtils.convert 做拼音转换 * 3、pinYinMultiCharactersUtils.getMultiCharactersPinYin 如果名字中的姓氏是多音字时,还需要做一下特别处理 * 4、如果用户名中不全是中文,比如: 周小斌_bank_1 ,类似这样的,或者 : bank_1_周小斌 ,只转换其中的中文,不改变整个字符串的顺序 */ if (pinYinUtils.isChineseName(userRequest.getRealname())) { String originalConvert = pinYinUtils.convert(userRequest.getRealname()); String multiConvertResult = pinYinMultiCharactersUtils.getMultiCharactersPinYin(userRequest.getRealname()); if(StringUtils.isNotEmpty(multiConvertResult)){ userRequest.setKeyWord(originalConvert.concat(",").concat(multiConvertResult)); return; } userRequest.setKeyWord(originalConvert ); }else { //如果不全部是中文,即除了中文之外,还有其他字符混在一起的话,这种才做解析 if(pinYinUtils.isMixedStr(userRequest.getRealname())){ userRequest.setKeyWord(pinYinUtils.getMixPinyinStr(userRequest.getRealname())); } } }参考其中的4条解释说明, pinYinUtils.isChineseName 判断传入过来的名称是否是中文呢?如果全部是中文的话做基础的解析pinYinUtils.convert 做拼音转换pinYinMultiCharactersUtils.getMultiCharactersPinYin 如果名字中的姓氏是多音字时,还需要做一下特别处理如果用户名中不全是中文,比如: 周小斌_bank_1 ,类似这样的,或者 : bank_1_周小斌 ,只转换其中的中文,不改变整个字符串的顺序该方法即把上面拼音转换工具类中的所有方法全部调起来使用了,工具类方法本身并不太难,但是需要结合自身的业务场景合理使用 关于多音字处理 在上午中,我们还提到,在实际的用户名称中,存在那些多音字的场景,比如: 单,正常解析出来就是 “dan” ,很明显这是不符合要求的,姓氏中应该解析为 “shan” (忽略 chan) ,或 “解” ,就应该解析为 “xie” ,这样分析之后发现,解析 “解小龙” 这个名字时,如果按照上面的工具类,解析出来的应该是 : jiexiaobin,jxb,如果再经过多音字的解析,还应该解析出 “xiexiaobin” 这个拼音,那么完整的冗余 keyWord字段值为:jiexiaobin,jxb,jie,xiexiaobin(考虑到使用系统的用户并不知道哪些是多音字) 解析多音字比较常用的做法是,维护一个常用的多音字的字典对照表,这个和 es中维护的停用词字典很像,这里直接列出提供参考,后续可以手动添加 最后再提供一个解析多音字的工具类 package com.congge.utils.pyin; 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 org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 处理多音字的扩展工具类 * * @author zhangcy * @date 2021-12-02 */ public class PinYinMultiCharactersUtils { private static final Logger logger = LoggerFactory.getLogger(PinYinMultiCharactersUtils.class); private static Map pinyinMap = new HashMap(); private static Map otherSpecialWord = new HashMap(); static { //这里如果apollo上面没有配置任何的值,默认初始化一些常用的 otherSpecialWord.put("解", Arrays.asList("xie")); otherSpecialWord.put("查", Arrays.asList("zha")); otherSpecialWord.put("单", Arrays.asList("shan")); otherSpecialWord.put("朴", Arrays.asList("piao")); otherSpecialWord.put("区", Arrays.asList("ou")); otherSpecialWord.put("仇", Arrays.asList("qiu")); otherSpecialWord.put("阚", Arrays.asList("kan")); otherSpecialWord.put("种", Arrays.asList("chong")); otherSpecialWord.put("盖", Arrays.asList("ge")); otherSpecialWord.put("繁", Arrays.asList("po")); } public static String toPinyin(String str) { try { initPinyin("/duoyinzi.dic.txt"); String py = convertChineseToPinyin(str); System.out.println(str + " = " + py); return py; } catch (Exception e) { logger.error("convert pinyin error,e : {}", e); return null; } } /** * 通过拆分名字的方式 获取多音字的名字的完整拼音 * * @param chinese * @return */ public static String getMultiCharactersPinYin(String chinese) { if (StringUtils.isEmpty(chinese)) { return null; } String result = null; if (chinese.length() >= 2) { String[] nameElements = chinese.split(""); String firstName = nameElements[0]; if (!isMultiChineseWord(firstName)) { return null; } String secondName = null; StringBuilder sb = new StringBuilder(); for (String str : nameElements) { if (!str.equals(firstName)) { sb.append(str); } } secondName = sb.toString(); //获取多音字的拼音 String partOne = PinYinMultiCharactersUtils.toPinyin(firstName); String partTwo = PinYinMultiCharactersUtils.toPinyin(secondName); result = partOne.concat(partTwo).toLowerCase(); } else { result = PinYinMultiCharactersUtils.toPinyin(chinese); } return result; } /** * 将某个字符串的首字母大写 * * @param str * @return */ public static String convertInitialToUpperCase(String str) { if (str == null) { return null; } StringBuffer sb = new StringBuffer(); char[] arr = str.toCharArray(); for (int i = 0; i < arr.length; i++) { char ch = arr[i]; if (i == 0) { sb.append(String.valueOf(ch).toUpperCase()); } else { sb.append(ch); } } return sb.toString(); } /** * 判断当前中文字是否多音字 * * @param chinese * @return */ public static boolean isMultiChineseWord(String chinese) { HanyuPinyinOutputFormat defaultFormat = new HanyuPinyinOutputFormat(); defaultFormat.setCaseType(HanyuPinyinCaseType.LOWERCASE); defaultFormat.setToneType(HanyuPinyinToneType.WITHOUT_TONE); char[] arr = chinese.toCharArray(); for (int i = 0; i < arr.length; i++) { char ch = arr[i]; if (ch > 128) { // 非ASCII码,取得当前汉字的所有全拼 try { String[] results = PinyinHelper.toHanyuPinyinStringArray(ch, defaultFormat); if (results == null) { //非中文 return false; } else { int len = results.length; if (len == 1) { // 不是多音字 return false; } else if (results[0].equals(results[1])) { //非多音字 有多个音,默认取第一个 if (otherSpecialWord.containsKey(chinese)) { return true; } return false; } else { // 多音字 return true; } } } catch (BadHanyuPinyinOutputFormatCombination e) { logger.error("BadHanyuPinyinOutputFormatCombination ,e :{}", e); } } } return false; } /** * 汉字转拼音 最大匹配优先 * * @param chinese * @return */ private static String convertChineseToPinyin(String chinese) { StringBuffer pinyin = new StringBuffer(); HanyuPinyinOutputFormat defaultFormat = new HanyuPinyinOutputFormat(); defaultFormat.setCaseType(HanyuPinyinCaseType.LOWERCASE); defaultFormat.setToneType(HanyuPinyinToneType.WITHOUT_TONE); char[] arr = chinese.toCharArray(); for (int i = 0; i < arr.length; i++) { char ch = arr[i]; if (ch > 128) { // 非ASCII码 取得当前汉字的所有全拼 try { String[] results = PinyinHelper.toHanyuPinyinStringArray(ch, defaultFormat); if (results == null) { //非中文 return ""; } else { int len = results.length; if (len == 1) { // 不是多音字 String py = results[0]; if (py.contains("u:")) { //过滤 u: py = py.replace("u:", "v"); logger.info("filter u: {}", py); } pinyin.append(convertInitialToUpperCase(py)); } else if (results[0].equals(results[1])) { //非多音字 有多个音,取第一个 if (otherSpecialWord.containsKey(chinese)) { return otherSpecialWord.get(chinese).get(0); } pinyin.append(convertInitialToUpperCase(results[0])); } else { logger.info("多音字:{}", ch); if (otherSpecialWord.containsKey(chinese)) { pinyin.append(otherSpecialWord.get(chinese).get(0)); continue; } int length = chinese.length(); boolean flag = false; String s = null; List keyList = null; for (int x = 0; x < len; x++) { String py = results[x]; if (py.contains("u:")) { py = py.replace("u:", "v"); logger.info("filter u :{}", py); } keyList = pinyinMap.get(py); if (i + 3 |
今日新闻 |
推荐新闻 |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |