~/ / Yapay zeka makaleleri / VPS'te yerel LLM / Ollama embedding'lerini pgvector ile PostgreSQL'de saklama ve arama

Ollama embedding'lerini pgvector ile PostgreSQL'de saklama ve arama

Ollama'nın /api/embed uç noktasından gelen embedding'ler, arama için ancak bir yerde saklandıklarında işe yarar. pgvector, PostgreSQL'e vektör benzerliği araması ekleyen açık kaynaklı bir eklentidir; böylece vektörler, metinlerle ve diğer verilerinizle aynı veritabanında, aynı sunucuda durabilir. Bu makale; kurulumu, tablonun oluşturulmasını, uzaklığın ve indeksin seçilmesini ve sonuçları değiştiren ayarları, pgvector 0.8.7 sürümünün README'sine dayanarak anlatır. Ardından arama sonuçlarını, yanıt kaynaklarını belirtsin diye Ollama'nın /api/chat uç noktasına iletir ve bu atıfları denetler. SQL'i pgvector 0.8.7 ve PostgreSQL 16 üzerinde, Ollama'dan alınan gerçek vektörlerle çalıştırdık; sohbet isteklerini ise Ollama 0.40.11 üzerinde qwen2.5:7b ile çalıştırdık. Bu sayfadaki dört araç SQL'i ve isteği yazar, yanıtı da tarayıcınızda denetler.

Yayın: · 8 dk okuma · Bilgiler, pgvector'ün 0.8.7 sürümünün README'si ve kaynak koduyla, ayrıca Ollama'nın belgeleri ve kaynak koduyla (0.40.11) 2026-11-05 tarihinde karşılaştırıldı. Güvenmeden önce güncel belgeleri denetleyin.

pgvector PostgreSQL'e ne ekler

pgvector README'si onu Postgres için açık kaynaklı vektör benzerliği araması olarak tanımlar. Kesin ve yaklaşık en yakın komşu aramasını; tek duyarlıklı, yarım duyarlıklı, ikili ve seyrek vektörleri; ayrıca L2 uzaklığını, nokta çarpımını (inner product), kosinüs uzaklığını, L1 uzaklığını, Hamming uzaklığını ve Jaccard uzaklığını destekler. Vektörler sıradan PostgreSQL sütunları olduğu için ACID uyumluluğundan, belirli bir ana geri dönüşten (point-in-time recovery) ve JOIN'lerden de yararlanırlar.

Belgeleriniz üzerinde arama yapmak için her parça için metnini ve vektörünü içeren bir satır saklarsınız, soruyu aynı modelle embedding'e çevirirsiniz ve satırları sorunun vektörüne olan uzaklıklarına göre sıralarsınız.

Kurulum

pgvector, PostgreSQL 13 ve üstünü destekler. README, aralarında şunların da bulunduğu birkaç kurulum yolu sıralar:

YöntemREADME ne diyor
Kaynaktan derlemegit clone --branch v0.8.7, ardından make ve make install (sudo gerekebilir)
Dockerpgvector/pgvector imajı, Postgres imajına pgvector'ü ekler. pg18-trixie veya 0.8.7-pg16 gibi etiketler Postgres sürümünü seçer.
APT (Debian, Ubuntu)PostgreSQL APT deposundan sudo apt install postgresql-18-pgvector; 18 yerine Postgres sürümünüzü yazın
Homebrewbrew install pgvector; yalnızca postgresql@18 ve postgresql@17 formüllerine ekler

Ardından eklentiyi kullanmak istediğiniz her veritabanında bir kez CREATE EXTENSION vector; ile etkinleştirin. Bu makale için pgvector/pgvector:0.8.7-pg16 Docker imajını kullandık.

Tabloyu oluşturma

vector(n) türündeki bir sütun tam olarak n boyutlu vektörler tutar ve n, embedding modelinizin verdiği vektörlerin uzunluğu olmalıdır. Farklı uzunlukta bir vektör reddedilir: vector(3) sütununa 2 boyutlu bir vektör eklediğimizde PostgreSQL "expected 3 dimensions, not 2" hatasını verdi. all-minilm 384 boyut döndürür, bu yüzden sütunu vector(384) olur.

README, her vektörün depolama boyutunu 4 × boyut + 8 bayt olarak verir; her öğe tek duyarlıklı bir kayan noktalı sayıdır ve bir sütunda 16,000 boyuta kadar izin verir. Bir indeks, aşağıda gösterildiği gibi daha az boyutu destekler. halfvec türü, yarım duyarlıklı değerleri 2 × boyut + 8 bayt içinde saklar.

Boyutvectorhalfvec
3841,544 bayt776 bayt
7683,080 bayt1,544 bayt
10244,104 bayt2,056 bayt

Test tablomuzda pg_column_size, vector(384) için 1,544 bayt ve halfvec(384) için 776 bayt verdi; bu, formülle uyuşuyor. Bu yalnızca vektörün boyutudur; metin sütunu ve indeksler ek yer kaplar.

Vektörleri Ollama'dan yükleme

pgvector bir vektörü köşeli parantezli metin olarak okur, örneğin '[1,2,3]'; README tüm öğelerin sonlu olması gerektiğini söyler (NaN, Infinity veya -Infinity olmaz). Bir /api/embed yanıtının embeddings alanındaki her dizi zaten bu biçimdedir ve yanıt girdinin sırasını korur; yani ilk metin ilk vektörle eşleşir.

README, birkaç satır için INSERT'i, upsert için INSERT … ON CONFLICT'i ve toplu yükleme için COPY'yi gösterir. Aşağıdaki dönüştürücü metinlerinizi yanıtla eşleştirir ve tırnakları kaçışlanmış INSERT ifadeleri yazar. Çıktısını tırnak, ters eğik çizgi, satır sonu ve emoji içeren metinlerle çalıştırdık ve metinler değişmeden geri geldi.

Deneyin: Embeddings to pgvector INSERT

Örnek, okuyabilmeniz için uydurma, birim uzunlukta, 4 boyutlu vektörler kullanır. Gerçek embedding'lerin yüzlerce ya da binlerce boyutu vardır.

4 boyutlu 3 satır. Metinler /api/embed istek gövdesi olarak, vektörler bir Ollama /api/embed yanıtı olarak okundu.

INSERT ifadeleri

psql veya başka bir istemcide çalıştırın. Satırlar tek bir işlemde (transaction) eklenir.

BEGIN;

INSERT INTO documents (content, embedding) VALUES
  ('Embeddings turn text into numeric vectors.', '[0.620186,0.410123,0.550165,0.380114]'),
  ('pgvector stores vectors in PostgreSQL, next to the rest of your data.', '[0.578815,0.469039,0.518937,0.419142]'),
  ('Ollama''s /api/embed returns one vector per input, in the same order.', '[0.119982,0.809879,-0.329951,0.46993]');

COMMIT;

Vektör biçimi pgvector README'sinden alındı, pgvector 0.8.7 ile 2026-11-04 tarihinde denetlendi. pgvector README

Her şey tarayıcınızda yapılır. Araç SQL yazar ve hiçbir yere bağlanmaz.

Uzaklığı seçme

Ollama'nın belgeleri, anlamsal aramanın çoğu kullanım senaryosu için kosinüs benzerliğini önerir ve /api/embed'in uzunluğu 1 olan vektörler döndürdüğünü söyler. Bu tür vektörlerde kosinüs uzaklığı, L2 uzaklığı ve nokta çarpımı satırları aynı sıraya koyar; çünkü L2 uzaklığının karesi, 2 eksi 2 çarpı kosinüs benzerliğidir. all-minilm ile yaptığımız testte ilk üç satır üçünde de aynıydı ve 0.931 olan L2 uzaklığı, 0.566 olan kosinüs benzerliğiyle uyuşuyordu.

Her uzaklığın kendi operatörü vardır. Bir arama operatöre göre sıralar ve LIMIT ile ilk satırları alır; bir indeks de tek bir operatör sınıfı için oluşturulur.

OperatörDöndürdüğüİndeks operatör sınıfı
<=>Kosinüs uzaklığı. Kosinüs benzerliği 1 - (a <=> b) olur.vector_cosine_ops
<->L2 (Öklid) uzaklığıvector_l2_ops
<#>Negatif nokta çarpımı. Nokta çarpımını elde etmek için -1 ile çarpın.vector_ip_ops
<+>L1 uzaklığıvector_l1_ops

README, <#> operatörünün negatif nokta çarpımını döndürmesinin nedenini, Postgres'in operatörlerde yalnızca ASC sıralı indeks taramalarını desteklemesi olarak açıklar. halfvec için operatör sınıfları halfvec_cosine_ops, halfvec_l2_ops ve benzeridir.

Kesin arama mı, indeks mi

İndeks olmadan pgvector kesin en yakın komşu araması yapar; README bunun mükemmel recall sağladığını söyler. Yaklaşık bir indeks, biraz recall'u hız karşılığında verir ve README, bir indeks ekledikten sonra sorguların farklı sonuçlar döndürebileceği konusunda uyarır.

Bir indeks yalnızca sorguda ORDER BY ve LIMIT varsa ve ORDER BY artan sırada uzaklık operatörünün kendisiyse kullanılır. ORDER BY embedding <=> $1 LIMIT 5 indeksi kullanabilir; ORDER BY 1 - (embedding <=> $1) DESC kullanamaz. README ayrıca tablo küçükken tablo taramasının daha hızlı olabileceğini belirtir.

NoktaHNSWIVFFlat
Nasıl çalışırÇok katmanlı bir çizgeVektörleri listelere böler ve sorguya en yakın listeleri arar
Diğeriyle karşılaştırmaDaha iyi hız-recall dengesi, ancak oluşturması daha yavaştır ve daha fazla bellek kullanırOluşturması daha hızlıdır ve daha az bellek kullanır, ancak hız-recall dengesi daha düşüktür
Ne zaman oluşturulurBoş bir tabloda oluşturulabilir (eğitim adımı yok)Tabloda biraz veri olduktan sonra
Oluşturma seçeneklerim (varsayılan 16) ve ef_construction (varsayılan 64)lists: 1M satıra kadar rows / 1000 ile başlayın, bunun üzerinde sqrt(rows) ile
Arama seçeneğihnsw.ef_search, varsayılan 40ivfflat.probes, varsayılan 1; sqrt(lists) ile başlayın
Boyutvector için 2,000'e, halfvec için 4,000'e kadarvector için 2,000'e, halfvec için 4,000'e kadar

pgvector 0.8.7 üzerinde ayrıca vector(2001) sütununun HNSW indeksi alamadığını, IVFFlat'in L1 uzaklığı için bir operatör sınıfı olmadığını ve ef_construction değerinin en az 2 × m olması gerektiğini doğruladık. Daha fazla boyut için README'nin SSS bölümü halfvec'i, ikili nicemlemeyi ve diğer seçenekleri sıralar.

Deneyin: pgvector SQL Builder

Her vektör 1,544 bayt kaplar; bu yüzden 10,000 satır yalnızca vektörler için yaklaşık 15.4 MB yer kaplar (indeksler ve diğer sütunlar dahil değil).

1. Eklenti ve tablo

CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE documents (
  id bigserial PRIMARY KEY,
  content text NOT NULL,
  embedding vector(384) NOT NULL
);

2. İndeks

CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);

3. Arama

PREPARE, sorunun /api/embed'den gelen vektörünü $1 olarak alır. Onu metin olarak geçirin, örneğin '[0.01,-0.02,…]'.

PREPARE search_documents(vector(384)) AS
  SELECT id, content, 1 - (embedding <=> $1) AS cosine_similarity
  FROM documents
  ORDER BY embedding <=> $1
  LIMIT 5;

-- EXECUTE search_documents('[0.0123, -0.0456, ...]');

Sınırlar ve varsayılanlar pgvector 0.8.7 README'sinden ve kaynak kodundan alındı, 2026-11-04 tarihinde denetlendi. pgvector README

Her şey tarayıcınızda yapılır. Araç SQL yazar ve hiçbir yere bağlanmaz.

Bir arama neden LIMIT'ten az satır döndürebilir

HNSW'de sonuçlar, varsayılanı 40 olan hnsw.ef_search ile sınırlıdır. HNSW indeksli 200 satırlık bir test tablosunda LIMIT 100 yalnızca 40 satır döndürdü; SET hnsw.ef_search = 100 sonrasında 100 satır döndürdü. hnsw.ef_search değerini en az ihtiyaç duyduğunuz satır sayısı kadar yapın. Bir işlem (transaction) içindeki SET LOCAL, değeri yalnızca tek bir sorgu için değiştirir.

Filtreleme de sonuçları azaltır. Yaklaşık indekslerde README'ye göre WHERE koşulu, indeks tarandıktan sonra uygulanır: bir koşul satırların %10'una uyuyorsa, hnsw.ef_search varsayılan değeri 40 olan HNSW ortalama olarak yalnızca 4 satır döndürür. 0.8.0 sürümünden beri yinelemeli indeks taramaları (SET hnsw.iterative_scan = strict_order veya relaxed_order), yeterli satır bulunana kadar indeksin daha fazlasını tarar; üst sınır hnsw.max_scan_tuples'tır (varsayılan 20,000).

README, NULL vektörlerin indekslenmediğini, kosinüs uzaklığında sıfır vektörlerin de indekslenmediğini ekler.

Sonuçları numaralı kaynaklarla /api/chat'e iletin

Arama en yakın satırları döndürdükten sonra modelin bunlara isteminde ihtiyacı vardır. Ollama'nın /api/chat uç noktası, her biri bir rol (system, user, assistant ya da tool) ve içerik taşıyan bir mesaj listesi alır. Basit bir düzen, kuralları içeren bir sistem mesajı ile satırları, her birine bir numara vererek, örneğin [1] ve [2], ardından da soruyu sıralayan bir kullanıcı mesajıdır. Numaralar sayesinde yanıt hangi satırları kullandığını söyleyebilir ve her numarayı satırına geri eşleyebilirsiniz.

Atıfları sabit bir biçimde almak için format alanına, yanıtı ve her biri kaynak numarası ile kısa bir alıntı içeren bir atıf listesini barındıran bir JSON Schema verin. Ollama'nın yapılandırılmış çıktılar sayfası, şemayı istemde de yazmayı ve daha deterministik çıktı için sıcaklığı (temperature) örneğin 0'a düşürmeyi önerir. psql'de json_agg işlevi bir sorgunun satırlarını bir JSON dizisinde toplar; aşağıdaki araç bunu doğrudan okur.

Deneyin: RAG Prompt Builder

Örnek kaynaklar, json_agg'in döndürdüğü biçimde elle yazılmıştır.

3 kaynak, nesnelerden oluşan bir JSON dizisi olarak okundu.

curl komutu

İsteği bu makinedeki Ollama'ya gönderir (localhost:11434).

curl http://localhost:11434/api/chat -d '{
  "model": "qwen2.5:7b",
  "messages": [
    {
      "role": "system",
      "content": "Answer the question using only the numbered sources in the user message. Cite the number of each source you use. If the sources do not contain the answer, say that you do not know and cite nothing. Answer in the same language as the question. For each citation, give the source number and copy a short quote from that source exactly as it is written. Return JSON that matches this schema: {\"type\":\"object\",\"properties\":{\"answer\":{\"type\":\"string\"},\"citations\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"source\":{\"type\":\"integer\"},\"quote\":{\"type\":\"string\"}},\"required\":[\"source\",\"quote\"]}}},\"required\":[\"answer\",\"citations\"]}"
    },
    {
      "role": "user",
      "content": "Sources:\n\n[1] (id: 12)\nWith HNSW, hnsw.ef_search is 40 by default, so a query returns at most 40 rows unless you raise it.\n\n[2] (id: 7)\nSET LOCAL inside a transaction changes hnsw.ef_search for a single query.\n\n[3] (id: 3)\npgvector supports PostgreSQL 13 and later.\n\nQuestion: Why does my HNSW search return only 40 rows with LIMIT 100?"
    }
  ],
  "stream": false,
  "format": {
    "type": "object",
    "properties": {
      "answer": {
        "type": "string"
      },
      "citations": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "source": {
              "type": "integer"
            },
            "quote": {
              "type": "string"
            }
          },
          "required": [
            "source",
            "quote"
          ]
        }
      }
    },
    "required": [
      "answer",
      "citations"
    ]
  },
  "options": {
    "temperature": 0
  },
  "truncate": false
}'

İstek gövdesi (JSON)

{
  "model": "qwen2.5:7b",
  "messages": [
    {
      "role": "system",
      "content": "Answer the question using only the numbered sources in the user message. Cite the number of each source you use. If the sources do not contain the answer, say that you do not know and cite nothing. Answer in the same language as the question. For each citation, give the source number and copy a short quote from that source exactly as it is written. Return JSON that matches this schema: {\"type\":\"object\",\"properties\":{\"answer\":{\"type\":\"string\"},\"citations\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"source\":{\"type\":\"integer\"},\"quote\":{\"type\":\"string\"}},\"required\":[\"source\",\"quote\"]}}},\"required\":[\"answer\",\"citations\"]}"
    },
    {
      "role": "user",
      "content": "Sources:\n\n[1] (id: 12)\nWith HNSW, hnsw.ef_search is 40 by default, so a query returns at most 40 rows unless you raise it.\n\n[2] (id: 7)\nSET LOCAL inside a transaction changes hnsw.ef_search for a single query.\n\n[3] (id: 3)\npgvector supports PostgreSQL 13 and later.\n\nQuestion: Why does my HNSW search return only 40 rows with LIMIT 100?"
    }
  ],
  "stream": false,
  "format": {
    "type": "object",
    "properties": {
      "answer": {
        "type": "string"
      },
      "citations": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "source": {
              "type": "integer"
            },
            "quote": {
              "type": "string"
            }
          },
          "required": [
            "source",
            "quote"
          ]
        }
      }
    },
    "required": [
      "answer",
      "citations"
    ]
  },
  "options": {
    "temperature": 0
  },
  "truncate": false
}

Yanıtta cevap message.content içindedir. prompt_eval_count beklediğinizden çok düşükse istem kesilmiş olabilir.

İstek alanları Ollama'nın API başvurusundan ve kaynak kodundan alınmıştır; Ollama 0.40.11 üzerinde 2026-11-05 tarihinde denetlendi. Ollama API: /api/chat

Her şey tarayıcınızda yapılır. Araç isteği yazar ve hiçbir yere bağlanmaz.

Kaynaklar sığmadığında Ollama istemin başını keser

Kurallar, satırlar ve soru bağlam uzunluğuna sığmak zorundadır. Ollama'nın bağlam uzunluğu sayfası, 24 GiB'tan az VRAM olduğunda varsayılan olarak 4k token verir, SSS bölümü ise 4096 token verir; test makinemizde ollama ps 4096 gösterdi. options içindeki num_ctx ya da OLLAMA_CONTEXT_LENGTH ortam değişkeni bunu değiştirir.

İstem daha uzun olduğunda /api/chat varsayılan olarak hata döndürmez. Ollama'nın kaynak kodunda (0.40.11) sunucu önce daha önceki mesajları bütünüyle atar; sistem mesajlarını ve son mesajı korur. Kalan istem hâlâ çok uzunsa ilk birkaç token'ı (num_keep, varsayılan 4) ve istemin sonunu korur, aradaki kısmı atar. qwen2.5:7b ve num_ctx 1024 ile yaptığımız testte 2,068 token'lık bir istem 514'e kesildi. Sistem mesajı ve ilk kaynaklar kayboldu: yanıt [1] numaralı kaynaktayken model hiçbir kaynakta olmayan bir kod sözcüğüyle yanıt verdi; yanıt son kaynaktayken ise doğru yanıt verdi, ama sistem mesajının istediği kaynak numarası olmadan. Yanıtta hata yoktu; sunucu günlüğünde "truncating input prompt" uyarısı vardı.

Ollama'nın kaynak kodunda sohbet istekleri için /api/chat sayfasında listelenmeyen bir truncate alanı da vardır. "truncate": false ile aynı istek, "request (2068 tokens) exceeds the available context size (1024 tokens), try increasing it" hatasıyla HTTP 400 döndürdü. Bir yanıttaki prompt_eval_count alanı istemdeki token sayısıdır; kesilen testte 2,068 değil, 514 idi.

İstemin sonu korunduğundan soruyu kaynaklardan sonra koyun. Kaynakların ne zaman sığmadığını öğrenmek için "truncate": false gönderin, ardından num_ctx değerini artırın ya da daha az satır gönderin. Daha büyük bir num_ctx daha fazla bellek gerektirir.

Atıfları denetleyin

Bir atıf mekanik olarak denetlenebilir: numara gönderilmiş bir kaynağı göstermelidir ve alıntı o kaynakta geçmelidir. Dört kaynakla ve qwen2.5:7b ile yaptığımız testte JSON yanıtlarındaki alıntılar kaynaklarla kelimesi kelimesine eşleşti. Bu, yanıtın doğru olduğunu kanıtlamaz: 384 boyutlu bir vektörün boyutu sorulduğunda model doğru kuralı (boyut sayısının 4 katı artı 8 bayt) alıntıladı ve yine de 1,544 yerine 1552 bayt yanıtladı.

Atıfsız yanıtlara da bakın. Kaynaklarda yanıt yokken model bir keresinde kendi bilgisinden atıfsız yanıt verdi, bir keresinde de bilmediğini söyledi ama yine de ilgisiz bir kaynağa atıf yaptı. Yanıttaki done_reason "length" ise yanıt num_predict yüzünden kesilmiştir ve atıfları eksik olabilir.

Aşağıdaki denetleyici yalnızca metni karşılaştırır. Bir kaynağın bir cümleyi gerçekten destekleyip desteklemediğine yine bir insanın karar vermesi gerekir.

Deneyin: RAG Citation Checker

Örnek yanıt, kendi kaynağında olmayan bir alıntıyı göstermek için elle yazılmıştır.

4 kaynak (string'lerden oluşan bir JSON dizisi). Yanıt, bir Ollama /api/chat yanıtı olarak okundu; biçimi: atıflı JSON.

İstemdeki token sayısı (prompt_eval_count): 306

  • #2. atıfın alıntısı [4] numaralı kaynakta yok.

Atıflar

KaynakAlıntıSonuç
[2]hnsw.ef_search is 40 by defaultYazıldığı gibi bulundu
[4]4 bytes per dimension plus 8Bu kaynakta yok

Bu yalnızca numaraların ve alıntıların kaynaklarla eşleştiğini denetler. Yanıtın doğru olup olmadığını ya da bir kaynağın bir cümleyi gerçekten destekleyip desteklemediğini söyleyemez.

Yanıt biçimi Ollama'nın API başvurusundan ve kaynak kodundan alınmıştır; Ollama 0.40.11 üzerinde 2026-11-05 tarihinde denetlendi. Ollama API: /api/chat

Her şey tarayıcınızda yapılır. Araç hiçbir yere bağlanmaz.

Küçük bir sunucuda bellek

README, çizge maintenance_work_mem içine sığdığında bir HNSW indeksinin belirgin biçimde daha hızlı oluştuğunu, artık sığmadığında bir bildirim gösterdiğini ve maintenance_work_mem değerini sunucunun belleğini tüketecek kadar yüksek ayarlamamanız gerektiğini söyler. Diğer indekslerde olduğu gibi, indeksi ilk veriler yüklendikten sonra oluşturmak daha hızlıdır. İndekslerin belleğe sığması gerekmez, ancak sığarlarsa büyük olasılıkla daha iyi performans görürsünüz; SELECT pg_size_pretty(pg_relation_size('index_name')) bir indeksin boyutunu gösterir.

Daha küçük bir çalışma kümesi ve daha küçük indeksler için README halfvec'i, yarım duyarlıklı indekslemeyi veya ikili nicemlemeyi önerir. Ollama aynı sunucuda çalışıyorsa PostgreSQL ve modeller aynı belleği paylaşır; Ollama'nın belleğini izleme üzerine yazdığımız makale, modellerin ne kadar bellek kullanacağını neyin belirlediğini açıklıyor.

Kaynaklar

Sırada ne okunmalı