karawaci.kode

← Semua snippet

Python Menengah AI

Store embedding ke pgvector Postgres

Simpan embedding produk Tokopedia ke Postgres pgvector + query similar dengan cosine distance. Tanpa vector DB komersial.

Dipublikasikan 4 Juni 2026

Pinecone bagus, tapi $70/bulan untuk index kecil itu mahal. Kalau sudah pakai Postgres, pgvector cukup banget untuk sampai puluhan juta vector. Snippet ini setup table produk dengan embedding kolom + query top-K similar by cosine distance. Indonesia-ready.

Kode

import os
from dataclasses import dataclass
from typing import List

import psycopg
from openai import OpenAI

DATABASE_URL = os.environ["DATABASE_URL"]
openai_client = OpenAI()


@dataclass
class Produk:
    id: int
    nama: str
    deskripsi: str


def embed(text: str) -> list[float]:
    """Generate embedding 1536-dim dari OpenAI text-embedding-3-small."""
    resp = openai_client.embeddings.create(
        model="text-embedding-3-small",
        input=text,
    )
    return resp.data[0].embedding


def setup_schema(conn: psycopg.Connection) -> None:
    """Bikin extension pgvector + table produk."""
    with conn.cursor() as cur:
        cur.execute("CREATE EXTENSION IF NOT EXISTS vector")
        cur.execute(
            """
            CREATE TABLE IF NOT EXISTS produk_embedding (
                id BIGSERIAL PRIMARY KEY,
                nama TEXT NOT NULL,
                deskripsi TEXT NOT NULL,
                embedding vector(1536) NOT NULL,
                created_at TIMESTAMPTZ DEFAULT now()
            )
            """
        )
        # Index HNSW untuk fast approximate similarity
        cur.execute(
            """
            CREATE INDEX IF NOT EXISTS produk_embedding_hnsw_idx
            ON produk_embedding USING hnsw (embedding vector_cosine_ops)
            WITH (m = 16, ef_construction = 64)
            """
        )
    conn.commit()


def insert_produk(conn: psycopg.Connection, nama: str, deskripsi: str) -> int:
    """Generate embedding + insert ke DB. Return ID baru."""
    teks_lengkap = f"{nama}. {deskripsi}"
    vec = embed(teks_lengkap)

    with conn.cursor() as cur:
        cur.execute(
            "INSERT INTO produk_embedding (nama, deskripsi, embedding) "
            "VALUES (%s, %s, %s) RETURNING id",
            (nama, deskripsi, vec),
        )
        new_id = cur.fetchone()[0]
    conn.commit()
    return new_id


def cari_mirip(
    conn: psycopg.Connection, query: str, top_k: int = 5
) -> List[tuple[Produk, float]]:
    """Cari produk paling mirip berdasarkan query bebas. Return (produk, distance)."""
    query_vec = embed(query)

    with conn.cursor() as cur:
        cur.execute(
            """
            SELECT id, nama, deskripsi, embedding <=> %s::vector AS distance
            FROM produk_embedding
            ORDER BY embedding <=> %s::vector
            LIMIT %s
            """,
            (query_vec, query_vec, top_k),
        )
        rows = cur.fetchall()

    return [(Produk(id=r[0], nama=r[1], deskripsi=r[2]), float(r[3])) for r in rows]

Pemakaian

with psycopg.connect(DATABASE_URL) as conn:
    setup_schema(conn)

    # Insert beberapa produk Tokopedia
    insert_produk(conn, "Indomie Goreng Original", "Mie instan goreng rasa original, 85g per bungkus")
    insert_produk(conn, "Indomie Soto Lamongan", "Mie instan kuah rasa soto khas Lamongan")
    insert_produk(conn, "Aqua Galon 19L", "Air mineral kemasan galon 19 liter isi ulang")
    insert_produk(conn, "Lemonilo Mie Goreng", "Mie instan sehat tanpa MSG, varian goreng")
# Query semantic search
hasil = cari_mirip(conn, "mie instan rasa kuah nikmat", top_k=3)
for produk, dist in hasil:
    print(f"  [{dist:.3f}] {produk.nama}")
# Output:
#   [0.121] Indomie Soto Lamongan
#   [0.234] Indomie Goreng Original
#   [0.310] Lemonilo Mie Goreng

Kapan dipakai

  • Semantic search produk e-commerce (Tokopedia, Bukalapak style).
  • Deduplication artikel berita berdasarkan konten, bukan keyword.
  • Recommendation: “produk mirip dengan yang baru dilihat”.
  • RAG: chunk dokumen + retrieve relevant chunk untuk context LLM.

Catatan

  • HNSW vs IVFFlat — HNSW lebih akurat tapi slower build. Untuk dataset di bawah 1 juta, HNSW worth it. Di atas itu pertimbangkan IVFFlat dengan lists = sqrt(N).
  • Dimension matter — text-embedding-3-small = 1536, text-embedding-3-large = 3072. Storage 2x lebih besar, query latency 2x lebih lambat. Pilih yang sesuai kebutuhan.
  • Distance operator: <=> cosine, <-> L2, <#> inner product. Cosine paling umum untuk text similarity.
  • Hybrid search — kombinasikan dengan full-text search Postgres (tsvector) untuk dapet hasil terbaik. Vector miss kata-kata persis, BM25 miss makna.

Embedding deterministic-ish, tapi model upgrade (3-small → 3-large) bikin semua embedding lama harus di-regenerate. Track model version di kolom terpisah supaya tahu kapan harus reindex.

# tags

pgvectorembeddingpostgresaisimilarity

Ditulis oleh Asti Larasati · 4 Juni 2026