Files
SkinbaseNova/app/Services/Recommendations/VectorSimilarity/PgvectorAdapter.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();
}
}