本篇分享一個hanlp分詞工具應用的案例,簡單來說就是做一圖庫,讓商家輕鬆方便的配置商品的圖片,最好是可以一鍵完成配置的。
先看一下效果圖吧:
商品單個推薦效果:匹配度高的放在最前面
這個想法很好,那怎麼實現了。分析了一下解決方案步驟:
1、圖庫建設:至少要有圖片吧,圖片肯定要有關聯的商品名稱、商品類別、商品規格、關鍵字等信息。
2、商品分詞算法:由於商品名稱是商家自己設置的,不是規範的,所以不可能完全匹配,要有好的分詞庫來找出關鍵字。還有一點,分詞庫要能夠自定義詞庫,最好能動態添加。如果讀者不知道什麼是分詞,請自行百度,本文不普及這個。
3、推薦匹配度算法:肯定要最匹配的放在前面,而且要有匹配度分數。商家肯定有圖庫沒有的商品,自動匹配的時候,不能隨便配置不相關的圖片。
先說明一下,本文企業沒有搜索引擎之類的工具,所以本質就靠的是數據庫檢索。
首頁讓我們先分析一下圖庫,下面是圖庫的設置界面。
讓我們先貼一下圖庫的表結構
CREATE TABLE `wj_tbl_gallery` (
`gallery_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`fileid` int(11) NOT NULL COMMENT '文件服務器上的文件ID',
`ptype` tinyint(4) NOT NULL DEFAULT '0' COMMENT '圖片類型,0 點歌屏點餐圖片',
`materialsort` varchar(50) DEFAULT NULL COMMENT '商品分類',
`materialbrand` varchar(50) DEFAULT NULL COMMENT '商品品牌',
`materialname` varchar(100) NOT NULL COMMENT '商品名稱',
`material_spec` varchar(50) DEFAULT NULL COMMENT '商品規格',
`material_allname` varchar(200) DEFAULT NULL COMMENT '商品完整名稱',
`status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '狀態,0正常,1停用,2刪除',
`updatedatetime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
`keyword` varchar(200) DEFAULT NULL COMMENT '商品關鍵字,用逗號隔開',
`bstorage` tinyint(4) NOT NULL DEFAULT '0' COMMENT '關鍵字是否入庫 0沒有,1有',
PRIMARY KEY (`gallery_id`),
KEY `idx_fileid` (`fileid`)
) ENGINE=InnoDB AUTO_INCREMENT=435 DEFAULT CHARSET=utf8 COMMENT='圖庫信息表';
數據示例:
簡單說一下material_allname是幹什麼用的呢,主要就是拼接商品名稱、規則 、關鍵字字段。用來寫sql的時候比較方便。關鍵字字段是幹什麼用的呢,作用有兩個。1是商品可能有多個名字,補充名稱的。二是給分詞庫動態添加詞庫。圖庫簡單說到這。
再說一下分詞庫,筆者選擇的是開源的漢語言分詞庫-hanlp分詞工具
優點是詞庫大,有詞性分析,可以自定義詞庫。缺點當然也有,就是不支持數據庫方法動態讀取詞庫。後面說一下我自己的解決辦法。
上代碼:
分詞代碼,這時差會去掉一些沒用字符。
我們分詞,就是調用SegmentUtils.segmentTerm(materialname);
動態添加詞庫方法:
private void addCustomerDictory(){
Integer max = galleryRepository.getMaxGallery();
if(CommonUtils.isNotEmpty(max) && max > 0 && max > SegmentUtils.CACHE_GALLERY_ID){
int oldid = SegmentUtils.CACHE_GALLERY_ID;
SegmentUtils.CACHE_GALLERY_ID = max;
List<String> gallery = galleryRepository.getGallery(oldid,max);
if(CommonUtils.isNotEmpty(gallery)){
Map<String,Boolean> dicMap = new HashMap<>();
for(String w : gallery){
if(CommonUtils.isNotEmpty(w)){
String[] array = w.split(",");
if(CommonUtils.isNotEmpty(array)){
for(String item : array){
String value = item.trim();
if(CommonUtils.isNotEmpty(value)){
dicMap.put(value, true);
}
}
}
}
}
Set<String> keys = dicMap.keySet();
if(CommonUtils.isNotEmpty(keys)){
SegmentUtils.insertCustomDictory(keys);
}
}
}
}
/**
* 獲取關鍵字
*
* @author deng
* @date 2019年3月13日
* @param galleryId
* @return
*/
@Query("select keyword from Gallery a where galleryId > ?1 and galleryId<=?2 and a.keyword !='' and bstorage=0")
public List<String> getGallery(int bgalleryId, int egalleryId);
@Cacheable(value = CacheConstants.CACHE_GALLERY, keyGenerator = CacheConstants.KEY_GENERATOR_METHOD)
@Query(value = "select gallery_id from wj_tbl_gallery a where a.keyword !='' and bstorage=0 order by gallery_id desc limit 1", nativeQuery = true)
public Integer getMaxGallery();
說一下解決思路,由於hanlp文檔上沒有看到從mysql上動態添加詞庫方法,只有CustomDictionary.insert能動態添加單個實例詞庫,系統如果重啓,就要重新添加。我就想出一個辦法,就是分詞的時候,查一下類的保存的最大圖庫表的主鍵是什麼,如果跟數據庫一樣,就不動態添加。如果小於圖庫的主鍵,就把沒有的那一段用CustomDictionary.insert添加進去。系統一般不重啓,如果重啓就在分詞的時候重新添加一下。查詢數據庫當然都有緩存,編輯圖庫的時候,把對應緩存清除一下。這種方式也能支持分佈式環境,多個實例都是一樣處理的。每過一段時間,就把圖庫表的關鍵字詞庫搞成文件的詞庫,避免動態添加太多,佔用太多內存。自定義詞庫其實是很重要的,任何分詞庫都不可能包含所有的詞庫,而分詞算法是根據詞庫來展開的,可以說詞庫決定了分詞結果的準確性。
讓我們看一下分詞的效果
商品名稱爲”雪碧(大)“的分詞結果 雪碧/nz, 大/a ,其中nz表示專有詞彙,a表示形容詞。
再看一下不理想的分詞結果:
商品品名稱:”蕾芙曼金棕色啤酒“,類別名稱:啤酒,
分詞結果:蕾/ng,芙/n,曼/ag,金/ng,棕色/n,啤酒/nz
很明顯,分詞結果不理想,蕾芙曼金棕色其實是一個商品名,不能分開。怎麼辦呢,這時候動態添加詞彙功能就派上用場了。
再圖庫關鍵字時差添加蕾芙曼金棕色啤酒,保存一下,再看一下分詞效果:
物品名稱:蕾芙曼金棕色啤酒,類別名稱:啤酒,分詞結果:蕾芙曼金棕色/nz,啤酒/nz
蕾芙曼金棕色被分到了一起,達到預期效果,這其實就是 CustomDictionary.insert(data, "nz 1024");再起作用。hanlp具體API功能,請參考官方文檔,本文就不介紹了。
最後重頭戲來了,商品圖片匹配度分析。作者就是採用了mysql的sql詞句的方法搞定了,其實就用到了LOCATE函數,很簡單。SQL示例如下
SELECT gallery_id, fileid, materialname, material_allname, score
, ROUND(score / 4 * 100, 0) AS rate
FROM (
SELECT a.gallery_id, a.fileid, materialname, material_allname
, IF(LOCATE('雪碧', a.material_allname), 2, 0) + IF(LOCATE('大', a.material_allname), 1, 0) + IF(LOCATE('飲料', a.material_allname), 1, 0) AS score
FROM wj_tbl_gallery a
WHERE a.STATUS = 0
AND (a.material_allname LIKE '%雪碧%'
OR a.material_allname LIKE '%大%'
OR a.material_allname LIKE '%飲料%')
) b
ORDER BY score DESC, materialname
LIMIT 0, 8
執行結果:
可以看出gallery_id是第一條,它的rate的是75,滿分是100,匹配度蠻高的。
說一下匹配度算法原則,如果完全匹配就是1百分,肯定就上了。然後去除某些關鍵字後,也匹配上了就是90分。最後採用分詞算法,按照1百分打分,其中如果高於50分,可以算基本匹配,自動配置圖片的時候,就可以當成匹配成功。總體原則就是匹配詞彙越多,分數越多。但是兩個字的詞彙,和5個字的詞彙,分數是不一樣的。還有詞性,專屬詞彙理論上應該比形容詞分數高。詳見下面的calculateWeight代碼,自己體會了。
public List<Map<String, Object>> queryList(String searchstr, int pagenumber, int pagesize, String materialsortname,
List<Term> segmentList) {
String name = "%" + searchstr + "%";
// 先簡單搜索 ,完全匹配100分
List<Map<String, Object>> list = queryList(name, pagenumber, pagesize, 100);
if (CommonUtils.isEmpty(list)) {
searchstr = searchstr.replaceAll("\\s", "");
String regEx = "(特價)|(/)|(\\()|(\\))|(()|())|(\\d+ml)|(買.送.)|(/)|(\\*)";
searchstr = searchstr.replaceAll(regEx, "");
if (CommonUtils.isNotEmpty(searchstr)) {
name = "%" + searchstr + "%";
// 簡單過濾 90分
list = queryList(name, pagenumber, pagesize, 90);
}
// 剩下分詞 靠計算
if (CommonUtils.isEmpty(list)) {
if (CommonUtils.isNotEmpty(segmentList)) {
list = queryListTerm(pagenumber, pagesize, segmentList, materialsortname);
}
// 如果只有分類,先定10分
else if (CommonUtils.isNotEmpty(materialsortname))
list = queryList(materialsortname, pagenumber, pagesize, 10);
}
}
return list;
}
private List<Map<String, Object>> queryList(String name, int pagenumber, int pagesize, int rate) {
String sql = "SELECT\n" + " a.gallery_id,\n" + " a.fileid,a.material_allname,a.materialname \n, " + rate
+ " rate FROM\n" + " wj_tbl_gallery a\n" + "WHERE\n"
+ " a.material_allname LIKE :searchstr and a.status = 0 order by length(materialname) LIMIT :pagenumber,:pagesize ";
Dto param = new BaseDto();
param.put("searchstr", name).put("pagenumber", pagenumber * pagesize).put("pagesize", pagesize);
return namedParameterJdbcTemplate.queryForList(sql, param);
private List<Map<String, Object>> queryListTerm(int pagenumber, int pagesize, List<Term> segmentList,
String materialsortname) {
Dto param = new BaseDto();
StringBuffer sb = new StringBuffer();
StringBuffer wsb = new StringBuffer(" (");
// 總權重
int tw = 0;
if (CommonUtils.isNotEmpty(segmentList)) {
for (int i = 0; i < segmentList.size(); i++) {
String str = segmentList.get(i).word;
int w = SegmentUtils.calculateWeight(segmentList.get(i));
str = StringUtils.escapeMysqlSpecialChar(str);
tw += w;
sb.append("if(LOCATE('").append(str).append("', a.material_allname),").append(w).append(",0) ");
wsb.append(" a.material_allname like '%").append(str).append("%' ");
if (i < segmentList.size() - 1) {
sb.append(" + ");
wsb.append(" or ");
}
}
// 類別單獨處理,目前權重較低
// 表示字符串是否爲空
int emptylen = 3;
if (CommonUtils.isNotEmpty(materialsortname)) {
if (sb.length() > emptylen) {
sb.append(" + ");
wsb.append(" or ");
}
tw += SegmentUtils.DWEIGHT;
materialsortname = StringUtils.escapeMysqlSpecialChar(materialsortname);
sb.append(" if(LOCATE('").append(materialsortname).append("', a.material_allname),")
.append(SegmentUtils.DWEIGHT).append(",0) ");
wsb.append(" a.material_allname like '%").append(materialsortname)
.append("%' ");
}
if (sb.length() > emptylen) {
sb.append(" as score ");
wsb.append(") ");
String scoreSelect = sb.toString();
String scorewhere = wsb.toString();
String sql = "select gallery_id,fileid,materialname,material_allname,score,ROUND(score/" + tw
+ "*100, 0) rate from (SELECT " + " a.gallery_id, "
+ " a.fileid,materialname,material_allname, " + scoreSelect + " FROM "
+ " wj_tbl_gallery a " + "WHERE " + " a.status = 0 and " + scorewhere
+ " ) b order by score desc ,materialname LIMIT " + pagenumber * pagesize + "," + pagesize;
param.put("pagenumber", pagenumber * pagesize).put("pagesize", pagesize);
logger.debug("商家搜索圖庫的SQL語句是{}", sql);
List<Map<String, Object>> list = namedParameterJdbcTemplate.queryForList(sql, param);
if (CommonUtils.isNotEmpty(list)) {
return list;
}
}
}
/**
* 計算分詞權重
* @author deng
* @date 2019年6月21日
* @param term
* @return
*/
public static int calculateWeight(Term term) {
// 漢字數
int num = countChinese(term.word);
// 大於3個漢字,權重增加
int value = num >= 3 ? 2 + (num - 3) / 2 : DWEIGHT;
// 專屬詞,如果有兩個字至少要最小分是2分
if (term.nature == Nature.nz && value <= DWEIGHT) {
value = DWEIGHT + 1;
}
return value;
}
總結一下,本文介紹的商品圖片推薦和自動匹配方法,可以看出來是相當簡單的,本質就是mysql的like%% 優化來的,依賴sql語句和hanlp分詞庫,做法簡單,但是能滿足專門商品的匹配,適合小圖庫。自然比不上大公司搞的搜索引擎來的效率高,僅供參考。