內(nèi)容較多、請(qǐng)先馬后看;借助es分布式計(jì)算的能力,使得早期易企秀APP端圖片搜索功能就具備了高可用、可擴(kuò)展的能力
1、背景
易企秀商場(chǎng)為我們提供了大量免付費(fèi)的模板,這些模板多以固定的圖片及樣式組合而成,用戶在這個(gè)基礎(chǔ)上稍加修改便可以快速實(shí)現(xiàn)自己的H5場(chǎng)景,為了滿足小白用戶能夠快速制作H5場(chǎng)景的需求,方便用戶能夠從海量商城作品中快速找到符合自己使用的風(fēng)格模板,為此產(chǎn)品上提供了通過文本搜索快速獲取樣例商品的途徑,也提供了基于圖片搜索樣例商品的功能,做圖片搜索的目的是為了拓展用戶獲取商品的途徑,同時(shí)也滿足了用戶基于圖片風(fēng)格樣式獲取商品的訴求。
以下內(nèi)容進(jìn)入實(shí)戰(zhàn),項(xiàng)目來自易企秀一線工程師操刀實(shí)踐,干貨滿滿
2、流程介紹
業(yè)務(wù)處理流程相對(duì)比較簡(jiǎn)單,這里就不放架構(gòu)圖了,整個(gè)項(xiàng)目中用到了sqoop、hive、spark、elasticsearch等大數(shù)據(jù)組件,步驟如下:
1、商品模板主要來自設(shè)計(jì)師、秀客以及運(yùn)營(yíng)精選,每個(gè)小時(shí)都有大量新增商品入庫(kù),我們通過sqoop實(shí)現(xiàn)商品數(shù)據(jù)增量同步到數(shù)據(jù)倉(cāng)庫(kù)(hive),主要包括商品庫(kù)中的商品封面圖、標(biāo)題、描述、Id等信息
2、借助spark分布式計(jì)算的能力快速清洗并抽取圖片特征
3、將抽取后的特征與商品模板建立對(duì)應(yīng)關(guān)系,并存儲(chǔ)到es
4、編寫查詢script腳本,用于計(jì)算用戶輸入圖片與候選集的相似度。
3、具體操作
- ETL
通過sqoop實(shí)現(xiàn)增量數(shù)據(jù)同步非常簡(jiǎn)單,需要指定一個(gè)用于監(jiān)控增量變化的字段:
sqoop job --create jobname -- import --connect jdbc:mysql://host:3306/mall --username 'bigdata' --password pwd
--table mysqlablename --hive-import --hive-table hivetablename
--incremental lastmodified --check-column create_time --last-value '2019-04-22 13:00:00'
以下幾點(diǎn)需要注意:
1、不能在sqoop job中指定-m參數(shù),指定了-m參數(shù)會(huì)在數(shù)據(jù)遷移過程中產(chǎn)生臨時(shí)數(shù)據(jù)文件,下次導(dǎo)入時(shí)會(huì)報(bào)數(shù)據(jù)目錄已存在的錯(cuò)誤;
2、因?yàn)槲覀儓?zhí)行的是增量操作,所以需要提前在hive中創(chuàng)建hivetablename對(duì)應(yīng)的數(shù)據(jù)表;
3、增量同步需將incremental配置為lastmodified,并在第一次導(dǎo)入數(shù)據(jù)時(shí)設(shè)置--last-value為數(shù)據(jù)下屆,每次sqoop會(huì)同步大于該下屆的數(shù)據(jù)并自動(dòng)更新該下屆值;
- 特征提取
圖片特征提取是本項(xiàng)目的核心模塊之一,由于圖片特征提取方式較多,通過調(diào)研這里我們先對(duì)幾種常用的傳統(tǒng)特征提取算法做簡(jiǎn)要說明:
| 算法 | 描述 | 應(yīng)用場(chǎng)景 |
|---|---|---|
| 顏色直方圖 | 提取圖片中各種顏色的分布數(shù)據(jù),對(duì)圖片翻轉(zhuǎn)、縮放、模糊處理后的特征影響比較小 | 自然環(huán)境、色彩風(fēng)格 |
| 顏色向量 | 在顏色直方圖基礎(chǔ)上增加了色彩空間分布特征的提取 | - |
| 文理特征 | 提取圖片中顏色漸變與物體紋理數(shù)據(jù)特征 | 物體分類、圖像搜索 |
| 形狀特征 | 提取圖片中物體輪廓特征與區(qū)域形狀特征 | 物體分類 |
| SIFT | 通過復(fù)雜的數(shù)據(jù)公式實(shí)現(xiàn)物體局部特征提取,具有平移、旋轉(zhuǎn)、光照不變性 | 物體識(shí)別、圖像檢測(cè) |
| SURF | 采用了SIFT相近的實(shí)現(xiàn)原理,但計(jì)算復(fù)雜度降低很多 | - |
在實(shí)際操作后我們選用了顏色灰度直方圖算法,以下是相關(guān)代碼,原生jdk代碼實(shí)現(xiàn),沒有第三方依賴,直接拷貝可運(yùn)行(需要全部工程代碼的請(qǐng)留下你的郵箱):
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.Base64;
import javax.imageio.ImageIO;
public class Hog extends FeatureSelect {
private static int GRAYBIT = 2; //GRAYBIT=4;用12位的int表示灰度值,前4位表示red,中間4們表示green,后面4位表示blue
/**
* 求三維的灰度直方圖
* @throws IOException
* @throws MalformedURLException
*/
public static void main(String[] args) {
/*double[] data5 = getHistgram2("http://pic15.nipic.com/20110713/2328079_172740212177_2.jpg");
ImageVector.print(data5);
double[] data1 = getHistgram2("http://imgup01.sj88.com/2018-07/04/09/15306691026479_3.jpg");
ImageVector.print(data1);*/
double[] data2 = getHistgram2("http://res.eqh5.com/o_1cjacked6nsv1m4du77esr1mr4u.jpg");
print(data2);
// double[] data3 = getHistgram2("http://res.eqh5.com/o_1cgqee47bfb966fmf8j472559.jpg");
// ImageVector.print(data3);
// double[] data4 = getHistgram2("http://res.eqh5.com/o_1ci40kmlv1c7b16ob1imfk961kjae.png");
// print(data4);
// double[] data6 = getHistgram2("http://res.eqh5.com/o_1ci40kmlv1c7b16ob1imfk961kjae.png");
// print(data6);
}
public static void print(double[] data){
StringBuffer sb = new StringBuffer();
StringBuffer sb2 = new StringBuffer();
for(int i=0; i<data.length; i++){
sb.append(i+"|"+data[i]+" ");
sb2.append( Double.valueOf(data[i])+",");
}
// System.out.println(sb);
System.out.println(sb2);
System.out.println( convertArrayToBase64(data));
}
public static final String convertArrayToBase64(double[] array) {
final int capacity = 8 * array.length;
final ByteBuffer bb = ByteBuffer.allocate(capacity);
for (int i = 0; i < array.length; i++) {
bb.putDouble(array[i]);
}
bb.rewind();
final ByteBuffer encodedBB = Base64.getEncoder().encode(bb);
return new String(encodedBB.array());
}
private static BufferedImage readImg(String url) {
try {
return ImageIO.read(new URL(url).openStream());
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public static double[][] getHistgram(String srcPath) {
BufferedImage img = readImg(srcPath);
return getHistogram(img);
}
/**
* hist[0][]red的直方圖,hist[1][]green的直方圖,hist[2][]blue的直方圖
* @param img 要獲取直方圖的圖像
* @return 返回r,g,b的三維直方圖
*/
public static double[][] getHistogram(BufferedImage img) {
int w = img.getWidth();
int h = img.getHeight();
double[][] hist = new double[3][256];
int r, g, b;
int pix[] = new int[w*h];
pix = img.getRGB(0, 0, w, h, pix, 0, w);
for(int i=0; i<w*h; i++) {
r = pix[i]>>16 & 0xff;
g = pix[i]>>8 & 0xff;
b = pix[i] & 0xff;
/*hr[r] ++;
hg[g] ++;
hb[b] ++;*/
hist[0][r] ++;
hist[1][g] ++;
hist[2][b] ++;
}
for(int j=0; j<256; j++) {
for(int i=0; i<3; i++) {
hist[i][j] = hist[i][j]/(w*h);
//System.out.println(hist[i][j] + " ");
}
}
return hist;
}
/**
* 求一維的灰度直方圖
* @param srcPath
* @return
*/
public static double[] getHistgram2(String srcPath) {
BufferedImage img = readImg(srcPath);
return getHistogram2(img);
}
/**
* 求一維的灰度直方圖
* @param img
* @return
*/
public static double[] getHistogram2(BufferedImage img) {
int w = img.getWidth();
int h = img.getHeight();
int series = (int) Math.pow(2, GRAYBIT); //GRAYBIT=4;用12位的int表示灰度值,前4位表示red,中間4們表示green,后面4位表示blue
int greyScope = 256/series;
double[] hist = new double[series*series*series];
int r, g, b, index;
int pix[] = new int[w*h];
pix = img.getRGB(0, 0, w, h, pix, 0, w);
for(int i=0; i<w*h; i++) {
r = pix[i]>>16 & 0xff;
r = r/greyScope;
g = pix[i]>>8 & 0xff;
g = g/greyScope;
b = pix[i] & 0xff;
b = b/greyScope;
index = r<<(2*GRAYBIT) | g<<GRAYBIT | b;
hist[index] ++;
}
for(int i=0; i<hist.length; i++) {
hist[i] = hist[i]/(w*h);
//System.out.println(hist[i] + " ");
}
return hist;
}
}
- 特征存儲(chǔ)
首先在mapping中定義存儲(chǔ)特征field
"features": {
"type": "binary",
"doc_values": true
}
其次借助spark的并行計(jì)算能力,每小時(shí)增量讀取hive表中新增商品的數(shù)據(jù),對(duì)封面圖進(jìn)行特征提取,并將提取后的特征字段連同其它屬性值一并存入ES,由于features存儲(chǔ)的是binary類型,數(shù)據(jù)需要轉(zhuǎn)化為base64字符串進(jìn)行存儲(chǔ),所以spark中主要代碼是:
String b64 = Hog.convertArrayToBase64(Hog.getHistgram2( imgUrl ));
- 圖片檢索
和構(gòu)建索引庫(kù)的方式一樣,我們?cè)跈z索前也需要對(duì)圖片進(jìn)行特征提取,但這次提取后的特征不需要進(jìn)行base64轉(zhuǎn)化,以下是query的核心語句:
{
"query": {
"function_score": {
"boost_mode": "replace",
"script_score": {
"script": {
"inline": "binary_vector_score",
"lang": "knn",
"params": {
"cosine": true,
"field": "features",
"vector": [
-0.09217305481433868, 0.010635560378432274, -0.02878434956073761, 0.06988169997930527, 0.1273992955684662, -0.023723633959889412, 0.05490724742412567, -0.12124507874250412, -0.023694118484854698
}
}
}
}
}
如果你覺得上述查詢返回的結(jié)果相關(guān)度不高或者響應(yīng)很慢,也可以重寫query增加過濾條件,以限制參與計(jì)算的數(shù)據(jù)范圍。
需要注意的是es5.6中并不原生支持cosine等計(jì)算相似度的函數(shù),開始執(zhí)行上述query之前,我們要先安裝一個(gè)script腳本,在這里下載
4、小結(jié)
上述工程雖然實(shí)現(xiàn)了圖片與文本相結(jié)合搜索功能,但檢索效果和性能并不是很出色,可優(yōu)化的空間還有很多,比如特征提取部分可以嘗試使用深度學(xué)習(xí)模型,通過卷積神經(jīng)網(wǎng)絡(luò)提取的特征可能效果會(huì)更好,另外新版ES7.0支持了vector數(shù)據(jù)類型(圖片數(shù)據(jù)存儲(chǔ)為該類型更合適),并且內(nèi)部實(shí)現(xiàn)了基于vector的余弦相似度計(jì)算,切換到新版本實(shí)現(xiàn)性能應(yīng)該也會(huì)好很多。