spark ml 推荐源码笔记二

本文深入探讨Spark MLlib中ALS算法的实现细节,重点关注`partitionRatings`方法,它将原始评分数据划分为块。通过`aggregateByKey`与`flatMap`组合优化内存使用,避免过度对象创建。此外,还介绍了`UncompressedInBlock`和`InBlock`数据结构在构建内链块过程中的作用,以及如何通过排序和压缩提高性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

上次我们讲到als对象train方法的

val solver = if (nonnegative) new NNLSSolver else new CholeskySolver

接下来是

    val blockRatings = partitionRatings(ratings, userPart, itemPart)//有个方法partitionRatings,传入数据源,user的HashPartitioner,item的HashPartitioner,并缓存
      .persist(intermediateRDDStorageLevel)

我们来看下partitionRatings

   /**
   * Partitions raw ratings into blocks.//把原始ratings分块
   *
   * @param ratings raw ratings//原始ratings
   * @param srcPart partitioner for src IDs//源HashPartitioner
   * @param dstPart partitioner for dst IDs//目的HashPartitioner
   *
   * @return an RDD of rating blocks in the form of ((srcBlockId, dstBlockId), ratingBlock)//以((srcBlockId, dstBlockId), ratingBlock)形式返回rating
   */
  private def partitionRatings[ID: ClassTag](
      ratings: RDD[Rating[ID]],
      srcPart: Partitioner,
      dstPart: Partitioner): RDD[((Int, Int), RatingBlock[ID])] = {


     /* The implementation produces the same result as the following but generates less objects.//下面的实现结果一样但是产生更少的对象


     ratings.map { r =>
       ((srcPart.getPartition(r.user), dstPart.getPartition(r.item)), r)//ratings的每条记录转成(user的HashPartitioner索引,item的HashPartitioner索引,rating)的形式
     }.aggregateByKey(new RatingBlockBuilder)(//然后用aggregateByKey聚合,(srcPart.getPartition(r.user), dstPart.getPartition(r.item))是k,,r是value,RatingBlockBuilder是初始值,我们先进入RatingBlockBuilder

  /**
   * Builder for [[RatingBlock]]. [[mutable.ArrayBuilder]] is used to avoid boxing/unboxing.//构建[RatingBlock],使用mutable.ArrayBuilder避免装箱/拆箱
   */
  private[recommendation] class RatingBlockBuilder[@specialized(Int, Long) ID: ClassTag]//@specialized(Int, Long) ID: ClassTag代表运行时确定推断类型
    extends Serializable {


    private val srcIds = mutable.ArrayBuilder.make[ID]//创建一个空的可变数组srcIds ,这种创建方法适合只声明元素类型的方式
    private val dstIds = mutable.ArrayBuilder.make[ID]//创建一个空的可变数组dstIds 
    private val ratings = mutable.ArrayBuilder.make[Float]//创建一个空的可变数组ratings
    var size = 0


    /** Adds a rating. */
    def add(r: Rating[ID]): this.type = {//加一个Rating[ID],数组长度size自增,srcIds 可变数组加元素r.user,dstIds 可变数组加元素r.item,ratings 可变数组加元素 r.rating,返回当前对象,这样当前对象就有了加的这个对象的信息
      size += 1
      srcIds += r.user
      dstIds += r.item
      ratings += r.rating
      this
    }


    /** Merges another [[RatingBlockBuilder]]. */
    def merge(other: RatingBlock[ID]): this.type = {//合并另一个RatingBlockBuilder,数组长度增加,两个srcIds数组合并,两个dstIds 数组合并,两个ratings 数组合并,返回当前对象
      size += other.srcIds.length
      srcIds ++= other.srcIds
      dstIds ++= other.dstIds
      ratings ++= other.ratings
      this
    }


    /** Builds a [[RatingBlock]]. */
    def build(): RatingBlock[ID] = {//构建RatingBlock[ID] 对象,传入参数,.result()方法把不可变数组变为可变数组
      RatingBlock[ID](srcIds.result(), dstIds.result(), ratings.result())
    }
  }

回到aggregateByKey,下面就是

}.aggregateByKey(new RatingBlockBuilder)(//(srcPart.getPartition(r.user), dstPart.getPartition(r.item))是k,,r是value,RatingBlockBuilder是初始值

         seqOp = (b, r) => b.add(r),//这里面b是RatingBlockBuilder,r是Rating[ID],把r的user,item,rating加到RatingBlockBuilder的数组中,这里每个value都会产生一个RatingBlockBuilder,印证了注释,所以没有采用这种方法
         combOp = (b0, b1) => b0.merge(b1.build()))//相同key的RatingBlockBuilder合并,.build()是RatingBlockBuilder的方法
       .mapValues(_.build())aggregateByKey的最终结果也是k-value类型,通过mapValues取value,通过_.build()转成RatingBlock[ID] ,RatingBlock成员分别是srcIds,dstIds,ratings数组
     */
我们看看更优的方法

    val numPartitions = srcPart.numPartitions * dstPart.numPartitions//分块数乘积
    ratings.mapPartitions { iter =>//分块计算
      val builders = Array.fill(numPartitions)(new RatingBlockBuilder[ID])每个分块生成一个RatingBlockBuilder数组,长度是分块乘积数
      iter.flatMap { r =>//拿出每个rating,由于是flatMap,最终结果是一个数组
        val srcBlockId = srcPart.getPartition(r.user)//确定rating中user的srcBlockId 
        val dstBlockId = dstPart.getPartition(r.item)//确定rating中item的dstBlockId 
        val idx = srcBlockId + srcPart.numPartitions * dstBlockId//组成便宜索引,看来还是数组好使
        val builder = builders(idx)//取出对应的RatingBlockBuilder
        builder.add(r)//和并单个Rating[ID]
        if (builder.size >= 2048) { // 2048 * (3 * 4) = 24k//如果RatingBlockBuilder数组的size超过2048,2048*3(3个数组)*4(数组每个元素4个字节)=24k
          builders(idx) = new RatingBlockBuilder//超过就把数组对应位置的RatingBlockBuilder弄成新的,又有新的空间,旧的弄成RatingBlock[ID]返回成为结果数组的一个元素,如果没超过相当于无返回结果,这个感觉好牛逼,一下控制了RatingBlockBuilder对象的数量
          Iterator.single(((srcBlockId, dstBlockId), builder.build()))
        } else {
          Iterator.empty
        }
      } ++ {//这里++的原因是flatMap 里只包含超过2048的builder,最终不满2048的还没有处理,这里就在处理不满2048的builder
        builders.view.zipWithIndex.filter(_._1.size > 0).map { case (block, idx) =>//取出size>0的builder
          val srcBlockId = idx % srcPart.numPartitions//获取要处理的builder对应的srcBlockId ,dstBlockId 
          val dstBlockId = idx / srcPart.numPartitions
          ((srcBlockId, dstBlockId), block.build())//和flatMap 元素同样的数据结构,最终合并,我再加个牛逼,以前确实没见过这种写法
        }
      }
    }.groupByKey().mapValues { blocks =>//聚合只操作builder
      val builder = new RatingBlockBuilder[ID]//由于blocks是iterator,所以聚合成一个builder,最终的结果就是RatingBlock[ID](srcIds.result(), dstIds.result(), ratings.result())构成的rdd,这里可以感受到一个套路,mapPartitions后groupByKey再把每个 iterator合并是一种高效的方法
      blocks.foreach(builder.merge)
      builder.build()
    }.setName("ratingBlocks")
  }

回到train方法,

    val (userInBlocks, userOutBlocks) =
      makeBlocks("user", blockRatings, userPart, itemPart, intermediateRDDStorageLevel)//这里又有一个makeBlocks方法,传入"user"字符串,blockRatings上一步得到的RDD[((Int, Int), ALS.RatingBlock[ID])] ,userPart用户HashPartitioner,itemPart项目itemPart,缓存级别,我们先看下makeBlocks方法

  /**
   * Creates in-blocks and out-blocks from rating blocks.//从rating blocks创建in-blocks和out-blocks,具体in-blocks和out-blocks是什么我也不知道,过完这个方法就清楚了
   * @param prefix prefix for in/out-block names//前缀
   * @param ratingBlocks rating blocks//传入的ratingBlocks 
   * @param srcPart partitioner for src IDs//源HashPartitioner
   * @param dstPart partitioner for dst IDs//目的HashPartitioner
   * @return (in-blocks, out-blocks)//返回 (in-blocks, out-blocks)元组
   */
  private def makeBlocks[ID: ClassTag](
      prefix: String,
      ratingBlocks: RDD[((Int, Int), RatingBlock[ID])],
      srcPart: Partitioner,
      dstPart: Partitioner,
      storageLevel: StorageLevel)(
      implicit srcOrd: Ordering[ID]): (RDD[(Int, InBlock[ID])], RDD[(Int, OutBlock)]) = {//有个排序的隐式参数
    val inBlocks = ratingBlocks.map {
      case ((srcBlockId, dstBlockId), RatingBlock(srcIds, dstIds, ratings)) =>//展开ratingBlocks,源HashPartitioner的id,目的HashPartitioner的id,RatingBlock成员用户id数组,项目id数组,评分数组
        // The implementation is a faster version of//下面的实现是val dstIdToLocalIndex = dstIds.toSet.toSeq.sorted.zipWithIndex.toMap的更高效版本,我就喜欢看到这句话
        // val dstIdToLocalIndex = dstIds.toSet.toSeq.sorted.zipWithIndex.toMap//把dstIds按升序排列并构成dstIds和排序索引的map,我们看下更高效的实现
        val start = System.nanoTime()//记录时间
        val dstIdSet = new OpenHashSet[ID](1 << 20)//弄一个OpenHashSet,大小是1向左移动20位
        dstIds.foreach(dstIdSet.add)//如果插入操作超过集合大小,默认是OpenHashSet的元素个数0.7时,集合大小*2,重新hash所有元素,这里面蛮复杂的,因为高手用的都是位操作和数组操作,这句的作用是把用户id数组弄到OpenHashSet中
        val sortedDstIds = new Array[ID](dstIdSet.size)//根据OpenHashSet元素数生成数组
        var i = 0
        var pos = dstIdSet.nextPos(0)//返回OpenHashSet中下一个元素的位置
        while (pos != -1) {//如果有值,获取这个值放入sortedDstIds 数组,如此循环
          sortedDstIds(i) = dstIdSet.getValue(pos)
          pos = dstIdSet.nextPos(pos + 1)
          i += 1
        }
        assert(i == dstIdSet.size)
        Sorting.quickSort(sortedDstIds)//升序排列sortedDstIds 
        val dstIdToLocalIndex = new OpenHashMap[ID, Int](sortedDstIds.length)//根据sortedDstIds长度弄了一个OpenHashMap
        i = 0
        while (i < sortedDstIds.length) {
          dstIdToLocalIndex.update(sortedDstIds(i), i)//把sortedDstIds数组弄到dstIdToLocalIndex这个OpenHashMap
          i += 1
        }
        logDebug(
          "Converting to local indices took " + (System.nanoTime() - start) / 1e9 + " seconds.")//统计耗时
        val dstLocalIndices = dstIds.map(dstIdToLocalIndex.apply)//取出与dstIds对应的索引,搞这么半天就是为了保证dstIds不变,求出dstIds的升序索引
        (srcBlockId, (dstBlockId, srcIds, dstLocalIndices, ratings))//和开始一样,只是多了一个dstLocalIndices,其实这里优化主要是用了OpenHashSet,OpenHashMap,还有quickSort

    }.groupByKey(new ALSPartitioner(srcPart.numPartitions))//这里又做了一个groupByKey,和平时用的不太一样传入了指定分区数的partitions,我们梳理一下结果,结果是相同srcBlockId的value都放到一个Iterable里,最终key是srcBlockId,代表用户分块索引,value是可迭代元组,元素第一个成员是对应产品分块索引,第二个成员是分块内用户id数组,第三个是对应产品id按次数降序的索引,最后一个是和分块内用户数组顺序对应的评分数组,由于groupByKey性能较低,这里可以说是算法性能的瓶颈,下面又有一个新类UncompressedInBlockBuilder,我们先看这是干什么的

  /**
   * Builder for uncompressed in-blocks of (srcId, dstEncodedIndex, rating) tuples.//构建元组的非压缩内链
   * @param encoder encoder for dst indices//参数是目标索引的编码器
   */
  private[recommendation] class UncompressedInBlockBuilder[@specialized(Int, Long) ID: ClassTag](
      encoder: LocalIndexEncoder)(
      implicit ord: Ordering[ID]) {


    private val srcIds = mutable.ArrayBuilder.make[ID]//构建源id数组
    private val dstEncodedIndices = mutable.ArrayBuilder.make[Int]//构建目标编码索引数组
    private val ratings = mutable.ArrayBuilder.make[Float]//构建评分数组


    /**
     * Adds a dst block of (srcId, dstLocalIndex, rating) tuples.//第一个方式是add,添加(srcId, dstLocalIndex, rating)元组
     *
     * @param dstBlockId dst block ID//目标分块索引
     * @param srcIds original src IDs//源id数组
     * @param dstLocalIndices dst local indices//目标本地索引数组
     * @param ratings ratings//评分数组
     */
    def add(
        dstBlockId: Int,
        srcIds: Array[ID],
        dstLocalIndices: Array[Int],
        ratings: Array[Float]): this.type = {
      val sz = srcIds.length//源id数组长度,要和目标本地索引数组,评分数组长度一致
      require(dstLocalIndices.length == sz)
      require(ratings.length == sz)
      this.srcIds ++= srcIds//把当前对象的源id数组合并
      this.ratings ++= ratings//把当前对象的评分数组合并
      var j = 0
      while (j < sz) {//遍历
        this.dstEncodedIndices += encoder.encode(dstBlockId, dstLocalIndices(j))//这个encoder就是LocalIndexEncoder,我们在笔记一详细讲过,传入目的分块索引和目的本地索引数组元素放到声明的dstEncodedIndices数组
        j += 1
      }
      this
    }


    /** Builds a [[UncompressedInBlock]]. *///第二个方法build,构建UncompressedInBlock
    def build(): UncompressedInBlock[ID] = {
      new UncompressedInBlock(srcIds.result(), dstEncodedIndices.result(), ratings.result())
    }
  }

我们看下UncompressedInBlock这个类

  /**
   * A block of (srcId, dstEncodedIndex, rating) tuples stored in primitive arrays.//存储元组(srcId, dstEncodedIndex, rating)的数组
   */
  private[recommendation] class UncompressedInBlock[@specialized(Int, Long) ID: ClassTag](
      val srcIds: Array[ID],
      val dstEncodedIndices: Array[Int],
      val ratings: Array[Float])(
      implicit ord: Ordering[ID]) {


    /** Size the of block. */
    def length: Int = srcIds.length//srcId数组长度


    /**
     * Compresses the block into an [[InBlock]]. The algorithm is the same as converting a//压缩block,类似把坐标数组形式的稀疏矩阵转化成压缩稀疏列形式
     * sparse matrix from coordinate list (COO) format into compressed sparse column (CSC) format.//使用spark内置时间排序避免产生过多对象
     * Sorting is done using Spark's built-in Timsort to avoid generating too many objects.
     */
    def compress(): InBlock[ID] = {
      val sz = length
      assert(sz > 0, "Empty in-link block should not exist.")
      sort()//先排序,调用后面的方法,搞完以后UncompressedInBlock成员数组都是按用户id升序排列的且其他数组保持与用户数组对应关系
      val uniqueSrcIdsBuilder = mutable.ArrayBuilder.make[ID]//搞两个可变数组
      val dstCountsBuilder = mutable.ArrayBuilder.make[Int]
      var preSrcId = srcIds(0)
      uniqueSrcIdsBuilder += preSrcId//把第一个用户id放入uniqueSrcIdsBuilder 
      var curCount = 1
      var i = 1
      var j = 0
      while (i < sz) {//遍历srcIds,不重复的放入uniqueSrcIdsBuilder,重复的把重复次数放入dstCountsBuilder,这样就压缩了
        val srcId = srcIds(i)
        if (srcId != preSrcId) {
          uniqueSrcIdsBuilder += srcId
          dstCountsBuilder += curCount
          preSrcId = srcId
          j += 1//这里j明显没用啊,这个不知道是不是失误?!
          curCount = 0
        }
        curCount += 1
        i += 1
      }
      dstCountsBuilder += curCount//把最后一个curCount放入dstCountsBuilder 
      val uniqueSrcIds = uniqueSrcIdsBuilder.result()//转成不可变数组
      val numUniqueSrdIds = uniqueSrcIds.length//唯一用户数组的长度
      val dstCounts = dstCountsBuilder.result()//转成不可变数组
      val dstPtrs = new Array[Int](numUniqueSrdIds + 1)//声明一个长度+1的数组
      var sum = 0
      i = 0
      while (i < numUniqueSrdIds) {//每个元素放累计数
        sum += dstCounts(i)
        i += 1
        dstPtrs(i) = sum
      }
      InBlock(uniqueSrcIds, dstPtrs, dstEncodedIndices, ratings)//压缩后返回InBlock,InBlock是一个case class,成员是唯一用户id数组,和唯一用户id数据对应的累计条数数组,后两个参数没变
    }


    private def sort(): Unit = {
      val sz = length
      // Since there might be interleaved log messages, we insert a unique id for easy pairing.
      val sortId = Utils.random.nextInt()//随机数,日志里会用到
      logDebug(s"Start sorting an uncompressed in-block of size $sz. (sortId = $sortId)")
      val start = System.nanoTime()

      val sorter = new Sorter(new UncompressedInBlockSort[ID])//先看下UncompressedInBlockSort,这里Sorter用的是java的timSort,

timSort排序的输入的单位不是一个个单独的数字了,而一个个的分区。其中每一个分区我们叫一个“run“。针对这个 run 序列,每次我们拿一个 run 出来进行归并。每次归并会将两个 runs 合并成一个 run。归并的结果保存到 "run_stack" 上。如果我们觉得有必要归并了,那么进行归并,直到消耗掉所有的 runs。这时将 run_stack 上剩余的 runs 归并到只剩一个 run 为止。这时这个仅剩的 run 即为我们需要的排好序的结果
分区:为了在已经按升序排好序的输入面前减少回溯,我们把输入当中已经有序的这些段分组,使得它们成为一个基本单元,这样我们就不必在这个基本单元内部浪费时间进行回溯了。比如[1, 2, 3, 2] 进行分区后就变成了 [[1, 2, 3], [2]]
减少回溯:为了在已经按降序排好序的输入面前避免归并排序倒退成 O(n^2),我们把输入当中降序的部分翻转成升序,也作为一个单元。比如 [3, 2, 1, 3] 进行分区后就变成了 [[1, 2, 3], [3]]

  /**
   * [[SortDataFormat]] of [[UncompressedInBlock]] used by [[Sorter]].//使用Sorter排序的UncompressedInBlock的数据形
   */
  private class UncompressedInBlockSort[@specialized(Int, Long) ID: ClassTag](
      implicit ord: Ordering[ID])
    extends SortDataFormat[KeyWrapper[ID], UncompressedInBlock[ID]] {//继承SortDataFormat,SortDataFormat有两个参数,第一个是KeyWrapper,KeyWrapper只是主键的封装类,第二个是UncompressedInBlock,之前提到过


    override def newKey(): KeyWrapper[ID] = new KeyWrapper()//复写newKey


    override def getKey(//复写getKey,参数是UncompressedInBlock,位置索引,KeyWrapper
        data: UncompressedInBlock[ID],
        pos: Int,
        reuse: KeyWrapper[ID]): KeyWrapper[ID] = {
      if (reuse == null) {//如果没有KeyWrapper,声明KeyWrapper,把对应位置的用户id设为key,有KeyWrapper就放入传入的KeyWrapper
        new KeyWrapper().setKey(data.srcIds(pos))
      } else {
        reuse.setKey(data.srcIds(pos))
      }
    }


    override def getKey(//复写getKey的另外一种形式
        data: UncompressedInBlock[ID],
        pos: Int): KeyWrapper[ID] = {
      getKey(data, pos, null)
    }


    private def swapElements[@specialized(Int, Float) T](//数组元素交换
        data: Array[T],
        pos0: Int,
        pos1: Int): Unit = {
      val tmp = data(pos0)
      data(pos0) = data(pos1)
      data(pos1) = tmp
    }


    override def swap(data: UncompressedInBlock[ID], pos0: Int, pos1: Int): Unit = {//UncompressedInBlock成员数组元素交换,注意是同时交换,对应关系保持不变
      swapElements(data.srcIds, pos0, pos1)
      swapElements(data.dstEncodedIndices, pos0, pos1)
      swapElements(data.ratings, pos0, pos1)
    }


    override def copyRange(//复写拷贝方法,都是把第一个参数数组从第二个参数位置,拷贝到第三个参数数组的第四个参数位置,第五个参数是拷贝长度
        src: UncompressedInBlock[ID],
        srcPos: Int,
        dst: UncompressedInBlock[ID],
        dstPos: Int,
        length: Int): Unit = {
      System.arraycopy(src.srcIds, srcPos, dst.srcIds, dstPos, length)
      System.arraycopy(src.dstEncodedIndices, srcPos, dst.dstEncodedIndices, dstPos, length)
      System.arraycopy(src.ratings, srcPos, dst.ratings, dstPos, length)
    }


    override def allocate(length: Int): UncompressedInBlock[ID] = {//分配长度,生成UncompressedInBlock,成员数组长度都为参数长度
      new UncompressedInBlock(
        new Array[ID](length), new Array[Int](length), new Array[Float](length))
    }


    override def copyElement(//拷贝元素,把源UncompressedInBlock第srcPos个位置的元素拷贝到目的UncompressedInBlock的第dstPos个位置
        src: UncompressedInBlock[ID],
        srcPos: Int,
        dst: UncompressedInBlock[ID],
        dstPos: Int): Unit = {
      dst.srcIds(dstPos) = src.srcIds(srcPos)
      dst.dstEncodedIndices(dstPos) = src.dstEncodedIndices(srcPos)
      dst.ratings(dstPos) = src.ratings(srcPos)
    }
  }

回到UncompressedInBlock类的sort方法

      sorter.sort(this, 0, length, Ordering[KeyWrapper[ID]])//排序当前对象,比较器是Ordering[KeyWrapper[ID]]
      val duration = (System.nanoTime() - start) / 1e9
      logDebug(s"Sorting took $duration seconds. (sortId = $sortId)")
    }
  }

介绍完了UncompressedInBlock,我们回到UncompressedInBlockBuilder,UncompressedInBlockBuilder最后一个方法buile返回的就是UncompressedInBlock,UncompressedInBlockBuilder也就介绍完了,我们回到makeBlocks方法

      .mapValues { iter => //mapValues的数据结构是RDD[(Int, Iterable[(Int, Array[ID], Array[Int], Array[Float])])],这里对每个iterator操作
        val builder =
          new UncompressedInBlockBuilder[ID](new LocalIndexEncoder(dstPart.numPartitions))//构建UncompressedInBlockBuilder,传入LocalIndexEncoder,LocalIndexEncoder的参数是10
        iter.foreach { case (dstBlockId, srcIds, dstLocalIndices, ratings) =>//对于每个iterator,把元素加到builder中
          builder.add(dstBlockId, srcIds, dstLocalIndices, ratings)
        }
        builder.build().compress()//把UncompressedInBlock压缩成InBlock,这些类刚才都介绍了
      }.setName(prefix + "InBlocks")//给RDD弄个名字 userInBlocks并缓存,最终赋值给inBlocks,inBlock里面的
      .persist(storageLevel)
    val outBlocks = inBlocks.mapValues { case InBlock(srcIds, dstPtrs, dstEncodedIndices, _) =>//由inBlocks生成outBlocks,
      val encoder = new LocalIndexEncoder(dstPart.numPartitions)//搞一个LocalIndexEncoder
      val activeIds = Array.fill(dstPart.numPartitions)(mutable.ArrayBuilder.make[Int])//生成一个数组,注意每个元素是一个可变数组
      var i = 0
      val seen = new Array[Boolean](dstPart.numPartitions)//生成一个布尔数组
      while (i < srcIds.length) {//遍历用户id数组
        var j = dstPtrs(i)//取用户id对应项目累计数组的第一个位置
        ju.Arrays.fill(seen, false)//初始化seen每个元素为false
        while (j < dstPtrs(i + 1)) {//取用户id对应项目累计数组的第二个位置,也就是在第二个位置前都是同一个用户对应的同一个项目
          val dstBlockId = encoder.blockId(dstEncodedIndices(j))//根据项目排序索引计算BlockId
          if (!seen(dstBlockId)) {//seen的作用是标记某个用户是否与某个产品block相连,true就是相连,false不相连
            activeIds(dstBlockId) += i // add the local index in this out-block//把相连的用户id索引赋值给activeIds(dstBlockId),dstBlockId是产品BlockId
            seen(dstBlockId) = true
          }
          j += 1
        }
        i += 1
      }
      activeIds.map { x =>//最终返回二维矩阵,行是dstBlockId,列是和这个dstBlockId有关联的用户索引数组
        x.result()
      }
    }.setName(prefix + "OutBlocks")//这个RDD起名userOutBlocks,确实恰如其分
      .persist(storageLevel)
    (inBlocks, outBlocks)//返回inBlocks,outBlocks

  }

我们梳理一下,inBlocks和outBlocks都是key-value形式,inBlocks的key是srcBlockId,value是InBlock(uniqueSrcIds, dstPtrs, dstEncodedIndices, ratings),uniqueSrcIds是升序去重的用户id数组,dstPtrs是和用户id相关的产品编码索引区间的数组,dstEncodedIndices是产品编码数组由dstPtrs可以找到dstEncodedIndices的索引ratings是评分数组,outBlocks的key也是srcBlockId,value是矩阵,行是dstBlockId,列是和这个dstBlockId有关联的用户索引数组,理清这个结构还是有些困难的

这篇我们就到这,下篇我们完结这个算法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值