$this->apiKey(), 'Content-Type' => 'application/json', ])->timeout(10)->post("{$this->host()}/query", array_filter([ 'id' => $vectorId, 'topK' => $effectiveTopK + 1, // +1 to exclude self 'includeMetadata' => true, 'namespace' => $this->namespace() ?: null, 'filter' => [ 'is_active' => ['$eq' => true], ], ])); if (! $response->successful()) { Log::warning("[PineconeAdapter] Query failed: HTTP {$response->status()}"); return []; } $matches = $response->json('matches', []); $results = []; foreach ($matches as $match) { $matchId = $match['id'] ?? ''; // Extract artwork ID from "artwork:123" format if (! str_starts_with($matchId, 'artwork:')) { continue; } $matchArtworkId = (int) substr($matchId, 8); if ($matchArtworkId === $artworkId) { continue; // skip self } $results[] = [ 'artwork_id' => $matchArtworkId, 'score' => (float) ($match['score'] ?? 0.0), ]; } return $results; } catch (\Throwable $e) { Log::warning("[PineconeAdapter] Query exception: {$e->getMessage()}"); return []; } } public function upsertEmbedding(int $artworkId, array $embedding, array $metadata = []): void { $vectorId = "artwork:{$artworkId}"; // Spec §9B: metadata should include category_id, content_type, author_id, is_active, nsfw $pineconeMetadata = array_merge([ 'is_active' => true, 'category_id' => $metadata['category_id'] ?? null, 'content_type' => $metadata['content_type'] ?? null, 'author_id' => $metadata['author_id'] ?? null, 'nsfw' => $metadata['nsfw'] ?? false, ], array_diff_key($metadata, array_flip([ 'category_id', 'content_type', 'author_id', 'nsfw', 'is_active', ]))); // Remove null values (Pinecone doesn't accept nulls in metadata) $pineconeMetadata = array_filter($pineconeMetadata, fn ($v) => $v !== null); try { $response = Http::withHeaders([ 'Api-Key' => $this->apiKey(), 'Content-Type' => 'application/json', ])->timeout(10)->post("{$this->host()}/vectors/upsert", array_filter([ 'vectors' => [ [ 'id' => $vectorId, 'values' => array_map('floatval', $embedding), 'metadata' => $pineconeMetadata, ], ], 'namespace' => $this->namespace() ?: null, ])); if (! $response->successful()) { Log::warning("[PineconeAdapter] Upsert failed for artwork {$artworkId}: HTTP {$response->status()}"); } } catch (\Throwable $e) { Log::warning("[PineconeAdapter] Upsert exception for artwork {$artworkId}: {$e->getMessage()}"); } } public function deleteEmbedding(int $artworkId): void { $vectorId = "artwork:{$artworkId}"; try { Http::withHeaders([ 'Api-Key' => $this->apiKey(), 'Content-Type' => 'application/json', ])->timeout(10)->post("{$this->host()}/vectors/delete", array_filter([ 'ids' => [$vectorId], 'namespace' => $this->namespace() ?: null, ])); } catch (\Throwable $e) { Log::warning("[PineconeAdapter] Delete exception for artwork {$artworkId}: {$e->getMessage()}"); } } }