where('artwork_id', $artworkId) ->select('embedding_json') ->first(); if (! $ref || ! $ref->embedding_json) { return []; } $embedding = json_decode($ref->embedding_json, true); if (! is_array($embedding) || $embedding === []) { return []; } // pgvector cosine distance operator: <=> // Score = 1 - distance (higher = more similar) $vecLiteral = '[' . implode(',', array_map('floatval', $embedding)) . ']'; try { $rows = DB::select( "SELECT artwork_id, 1 - (embedding_json::vector <=> ?::vector) AS score FROM artwork_embeddings WHERE artwork_id != ? ORDER BY embedding_json::vector <=> ?::vector LIMIT ?", [$vecLiteral, $artworkId, $vecLiteral, $topK] ); } catch (\Throwable $e) { Log::warning("[PgvectorAdapter] Query failed: {$e->getMessage()}"); return []; } return array_map(fn ($row) => [ 'artwork_id' => (int) $row->artwork_id, 'score' => (float) $row->score, ], $rows); } public function upsertEmbedding(int $artworkId, array $embedding, array $metadata = []): void { $json = json_encode($embedding); DB::table('artwork_embeddings')->updateOrInsert( ['artwork_id' => $artworkId], [ 'embedding_json' => $json, 'model' => $metadata['model'] ?? 'clip', 'model_version' => $metadata['model_version'] ?? 'v1', 'dim' => count($embedding), 'is_normalized' => $metadata['is_normalized'] ?? true, 'generated_at' => now(), ], ); } public function deleteEmbedding(int $artworkId): void { DB::table('artwork_embeddings') ->where('artwork_id', $artworkId) ->delete(); } }