易企秀基於 elasticsearch 快速構建圖片搜尋引擎(一)
內容較多、請先馬後看;藉助es分散式計算的能力,使得早期易企秀APP端圖片搜尋功能就具備了高可用、可擴充套件的能力
1、背景
易企秀商場為我們提供了大量免付費的模板,這些模板多以固定的圖片及樣式組合而成,使用者在這個基礎上稍加修改便可以快速實現自己的H5場景,為了滿足小白使用者能夠快速製作H5場景的需求,方便使用者能夠從海量商城作品中快速找到符合自己使用的風格模板,為此產品上提供了通過文字搜尋快速獲取樣例商品的途徑,也提供了基於圖片搜尋樣例商品的功能,做圖片搜尋的目的是為了拓展使用者獲取商品的途徑,同時也滿足了使用者基於圖片風格樣式獲取商品的訴求。
以下內容進入實戰,專案來自易企秀一線工程師操刀實踐,乾貨滿滿
2、流程介紹
業務處理流程相對比較簡單,這裡就不放架構圖了,整個專案中用到了sqoop、hive、spark、elasticsearch等大資料元件,步驟如下:
1、商品模板主要來自設計師、秀客以及運營精選,每個小時都有大量新增商品入庫,我們通過sqoop實現商品資料增量同步到資料倉庫(hive),主要包括商品庫中的商品封面圖、標題、描述、Id等資訊
2、藉助spark分散式計算的能力快速清洗並抽取圖片特徵
3、將抽取後的特徵與商品模板建立對應關係,並存儲到es
4、編寫查詢script指令碼,用於計算使用者輸入圖片與候選集的相似度。
3、具體操作
- ETL
通過sqoop實現增量資料同步非常簡單,需要指定一個用於監控增量變化的欄位:
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'
以下幾點需要注意:
1、不能在sqoop job中指定-m引數,指定了-m引數會在資料遷移過程中產生臨時資料檔案,下次匯入時會報資料目錄已存在的錯誤;
2、因為我們執行的是增量操作,所以需要提前在hive中建立hivetablename對應的資料表;
3、增量同步需將incremental配置為lastmodified,並在第一次匯入資料時設定--last-value為資料下屆,每次sqoop會同步大於該下屆的資料並自動更新該下屆值;
- 特徵提取
圖片特徵提取是本專案的核心模組之一,由於圖片特徵提取方式較多,通過調研這裡我們先對幾種常用的傳統特徵提取演算法做簡要說明:
演算法 | 描述 | 應用場景 |
---|---|---|
顏色直方圖 | 提取圖片中各種顏色的分佈資料,對圖片翻轉、縮放、模糊處理後的特徵影響比較小 | 自然環境、色彩風格 |
顏色向量 | 在顏色直方圖基礎上增加了色彩空間分佈特徵的提取 | - |
文理特徵 | 提取圖片中顏色漸變與物體紋理資料特徵 | 物體分類、影象搜尋 |
形狀特徵 | 提取圖片中物體輪廓特徵與區域形狀特徵 | 物體分類 |
SIFT | 通過複雜的資料公式實現物體區域性特徵提取,具有平移、旋轉、光照不變性 | 物體識別、影象檢測 |
SURF | 採用了SIFT相近的實現原理,但計算複雜度降低很多 | - |
在實際操作後我們選用了顏色灰度直方圖演算法,以下是相關程式碼,原生jdk程式碼實現,沒有第三方依賴,直接拷貝可執行(需要全部工程程式碼的請留下你的郵箱):
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 BufferedImagereadImg(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; } }
- 特徵儲存
首先在mapping中定義儲存特徵field
"features": { "type": "binary", "doc_values": true }
其次藉助spark的平行計算能力,每小時增量讀取hive表中新增商品的資料,對封面圖進行特徵提取,並將提取後的特徵欄位連同其它屬性值一併存入ES,由於features儲存的是binary型別,資料需要轉化為base64字串進行儲存,所以spark中主要程式碼是:
String b64 = Hog.convertArrayToBase64(Hog.getHistgram2( imgUrl ));
- 圖片檢索
和構建索引庫的方式一樣,我們在檢索前也需要對圖片進行特徵提取,但這次提取後的特徵不需要進行base64轉化,以下是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 } } } } }
如果你覺得上述查詢返回的結果相關度不高或者響應很慢,也可以重寫query增加過濾條件,以限制參與計算的資料範圍。
需要注意的是es5.6中並不原生支援cosine等計算相似度的函式,開始執行上述query之前,我們要先安裝一個script指令碼,在這裡下載
4、小結
上述工程雖然實現了圖片與文字相結合搜尋功能,但檢索效果和效能並不是很出色,可優化的空間還有很多,比如特徵提取部分可以嘗試使用深度學習模型,通過卷積神經網路提取的特徵可能效果會更好,另外新版ES7.0支援了vector資料型別(圖片資料儲存為該型別更合適),並且內部實現了基於vector的餘弦相似度計算,切換到新版本實現效能應該也會好很多。