易企秀基于elasticsearch快速構(gòu)建圖片搜索引擎(一)

內(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ì)好很多。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容