onQueue($queue); } } public function handle(): void { $modelVersion = (string) config('recommendations.similarity.model_version', 'sim_v1'); $resultLimit = (int) config('recommendations.similarity.result_limit', 30); $maxPerAuthor = (int) config('recommendations.similarity.max_per_author', 2); $query = Artwork::query()->public()->published()->select('id', 'user_id'); if ($this->artworkId !== null) { $query->where('id', $this->artworkId); } $query->chunkById($this->batchSize, function ($artworks) use ($modelVersion, $resultLimit, $maxPerAuthor) { foreach ($artworks as $artwork) { $this->processArtwork($artwork, $modelVersion, $resultLimit, $maxPerAuthor); } }); } private function processArtwork( Artwork $artwork, string $modelVersion, int $resultLimit, int $maxPerAuthor, ): void { // Fetch top co-occurring artworks (bi-directional) $candidates = DB::table('rec_item_pairs') ->where('a_artwork_id', $artwork->id) ->select(DB::raw('b_artwork_id AS related_id'), 'weight') ->union( DB::table('rec_item_pairs') ->where('b_artwork_id', $artwork->id) ->select(DB::raw('a_artwork_id AS related_id'), 'weight') ) ->orderByDesc('weight') ->limit($resultLimit * 3) ->get(); if ($candidates->isEmpty()) { return; } $relatedIds = $candidates->pluck('related_id')->map(fn ($id) => (int) $id)->all(); // Fetch author info for diversity filtering $authorMap = DB::table('artworks') ->whereIn('id', $relatedIds) ->where('is_public', true) ->where('is_approved', true) ->whereNotNull('published_at') ->where('published_at', '<=', now()) ->whereNull('deleted_at') ->pluck('user_id', 'id') ->all(); // Apply diversity cap $authorCounts = []; $final = []; foreach ($candidates as $cand) { $relatedId = (int) $cand->related_id; if (! isset($authorMap[$relatedId])) { continue; // not public/published } $authorId = (int) $authorMap[$relatedId]; $authorCounts[$authorId] = ($authorCounts[$authorId] ?? 0) + 1; if ($authorCounts[$authorId] > $maxPerAuthor) { continue; } $final[] = $relatedId; if (count($final) >= $resultLimit) { break; } } if ($final === []) { return; } RecArtworkRec::query()->updateOrCreate( [ 'artwork_id' => $artwork->id, 'rec_type' => 'similar_behavior', 'model_version' => $modelVersion, ], [ 'recs' => $final, 'computed_at' => now(), ], ); } }