85 lines
2.6 KiB
PHP
85 lines
2.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\Recommendations\VectorSimilarity;
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
/**
|
|
* pgvector adapter — uses the artwork_embeddings table with cosine similarity.
|
|
*
|
|
* Requires PostgreSQL with the pgvector extension installed.
|
|
* Schema: artwork_embeddings (artwork_id PK, model, dims, embedding vector(N), ...)
|
|
*
|
|
* Spec §9 Option A.
|
|
*/
|
|
final class PgvectorAdapter implements VectorAdapterInterface
|
|
{
|
|
public function querySimilar(int $artworkId, int $topK = 100): array
|
|
{
|
|
// Fetch reference embedding
|
|
$ref = DB::table('artwork_embeddings')
|
|
->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();
|
|
}
|
|
}
|