美文网首页大数据实战spark||flink||scala
易企秀基于elasticsearch快速构建图片搜索引擎(一)

易企秀基于elasticsearch快速构建图片搜索引擎(一)

作者: 郭彦超 | 来源:发表于2019-04-21 00:01 被阅读775次

    内容较多、请先马后看;借助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 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;
    
        }
    
        
    }
    
    
    
    • 特征存储

    首先在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的余弦相似度计算,切换到新版本实现性能应该也会好很多。

    相关文章

      网友评论

        本文标题:易企秀基于elasticsearch快速构建图片搜索引擎(一)

        本文链接:https://www.haomeiwen.com/subject/lhdkgqtx.html