Az Şey, Doğru Şey — Mnemo'nun Temel Mimari Kararları · Mnemo Serisi 2/5
Bir servisin gerçekten kaç hareketli parçaya ihtiyacı vardır? Mnemo'nun ilk haftası, bu soruya benim için çok şey öğretti. "İşte burada bir hafıza servisi yazıyorum" dediğim an, beynimin ilk yaptığı şey vector database'leri kıyaslamaya oturmaktı. Yapmadım. İyi ki yapmadım.
Serinin ikinci yazısı. Birincide "neden bir hafıza servisi" sorusunu açmıştık. Şimdi "nasıl başlayacağız" sorusuna geliyoruz — ama önce, nelerden vazgeçtiğimize.
En tehlikeli alışkanlık: özelliklere göre tool seçmek
Yeni bir servis yazmaya başlarken çoğumuzun düştüğü tuzak şu: önce özellikler listesi yapıyoruz, sonra her özellik için en iyi araç seçiyoruz. Mnemo'da vector search lazım — Pinecone bak. Event ingestion lazım — Kafka bak. Cache lazım — Redis bak. Search lazım — Elasticsearch bak.
Üç gün sonra elinde altı tane farklı state-ful sistem oluyor. Her birinin backup'ı, monitoring'i, healthcheck'i, restart prosedürü, network kuralı, sertifikası ayrı. Daha bir satır kod yazmamışsın, ama operasyon yorgunluğunu zaten satın aldın.
Mnemo'da ilk gün kendime şu cümleyi söyledim ve hâlâ tekrarlıyorum:
"Her yeni state-ful sistem, gece yarısı uyandıracak bir alarm demektir."
Bu cümle ile her aracı reddetmedim — sadece eklerken gerekçesini iki kez sordum. Cevap "çünkü güzel" ise reddettim. Cevap "çünkü mevcut sistem bu işi yapamıyor" ise düşündüm.
Vector database seçimi: Pinecone'dan geri kaçış
Klasik beklenti: AI projesi → vector database lazım → Pinecone aç → bir hesap kur.
Pinecone'un sayfasına gittim, fiyat tablosuna baktım, geri kapattım. Aylık ücret kötü değildi aslında. Ama sayfa bana şunu söylüyordu: "Bu sistemin tutucusu sen olmayacaksın, biz olacağız. Ücret bunun bedeli." Single-user bir kişisel hafıza servisi için vendor lock-in + aylık fatura mantıklı bir denklemde değildi.
İkinci dur: Qdrant. Self-hosted, açık kaynak, hızlı. İyi araç. Kurmak istedim, sonra durdum: "İki state-ful sistemim olacak — PostgreSQL ve Qdrant. İkisinin backup stratejisi, healthcheck'i, persist volume'ü ayrı. Bunlar arasında transaction yok — dosya bilgisini Postgres'e, embedding'i Qdrant'a yazmaya çalışırken biri başarılı diğeri başarısız olursa kim cleanup yapacak?"
Üçüncü dur: Weaviate, Milvus, Chroma. Aynı denklem, farklı renkler.
Sonra pgvector geldi aklıma. PostgreSQL'in bir extension'ı, vector column tipi ekliyor, HNSW index destekliyor. Aynı tabloda hem yapılandırılmış verim hem embedding'im olabiliyor. Üstüne pg_trgm da var — trigram tabanlı fuzzy keyword search. Yani hem vector hem keyword search'ü tek query'de yapabilirim, FULL OUTER JOIN ile.
Kazanç tablosu:
- Tek state-ful sistem.
- Tek backup.
- Tek transaction sınırı.
- Tek operasyonel zihinsel model.
- Sıfır vendor lock-in.
Kayıp: ölçek. pgvector milyonlarca embedding'e kadar gider, sonra IVF index'le devam eder, sonunda durur. Ama benim korpusum 100K'ya bile gelmeyecek — kişisel hafıza bu kadar büyümez. Pareto bana açık ara pgvector'u gösteriyordu.
Bu seçimin asıl öğretici tarafı şu: "en iyi vector DB hangisi" sorusu yanlış soruydu. Doğru soru "benim use case'imde sıfır operasyonel maliyetle hangisi yeterli" idi.
Tek tablo mu, kind başına tablo mı?
Mnemo'da bir hafıza dört türden biri olabiliyor — working, episodic, semantic, procedural. Bunlar biyolojik hafıza modelinden geliyor:
- Working → kısa süreli çalışma belleği (saatler).
- Episodic → belirli bir zamanda olmuş olay (haftalar/aylar).
- Semantic → zaman-bağımsız gerçek (yıllar).
- Procedural → nasıl-yapılır bilgisi (kalıcı).
İlk içgüdüm dört ayrı tablo yapmaktı. Daha "temiz", daha "domain-driven" hissi veriyordu. Ama bir saat sonra durdum ve şu soruyu sordum: "Bu dört türü ne sıklıkla ayrı sorgulayacağım, ne sıklıkla beraber?"
Cevap netti: birincil use case "hangi türden olduğu fark etmez, en alakalı 10 hafızayı bana getir" idi. Yani 4 ayrı tabloyu UNION ile birleştirmek zorunda kalacaktım. Üstüne 4 ayrı HNSW index, 4 ayrı backup row count'u, 4 ayrı migration acısı.
Tek tablo + kind enum kararı verdim. PostgreSQL'in partial index'leri sayesinde "soft-delete edilmemiş episodic memory'ler" gibi alt-küme sorgular yine de hızlı çalışıyor. Tek tablo, çok kullanım.
Buradan çıkardığım ders: şema tasarımında "ayrılık" kalitesini değil, "birlikte kullanım sıklığını" temel al.
"Henüz lazım değil ama gelecek için bırak"
Migration dosyamda Phase 1'de hiç kullanılmayacak alanlar vardı: consolidated_from, consolidated_into, expires_at, attachments. Bunlar Phase 3-5'te lazım olacaktı. Eklemek gerekliydi çünkü:
- Production'da tablo değiştirmek pahalı. Özellikle index'li, milyonlarca satırlı bir tabloda.
ALTER TABLE ADD COLUMNlock alır. Tek kullanıcılı projemde önemli değil ama prensip önemli.- Boş bir alan maliyet yaratmıyor — NULL ise neredeyse hiç yer kaplamıyor.
Ama burada ince bir denge var: "henüz lazım değil ama eklenmiş" şeyler ile "asla lazım olmayacak ama ben hayal etmiştim" şeyler arasındaki fark.
İkisinin filtresi şu olmalı: kullanım senaryosunu bugün tek bir cümleyle yazabiliyor musun?
expires_at→ "kullanıcıexpires_atset ederse, Phase 3'teki cleanup job'u o satırı hard-delete eder." Bir cümle.consolidated_from→ "Phase 3'te N episodic memory'i tek bir semantic memory'ye merge ettiğimde, hangilerinin merge edildiğini bilmek istiyorum." Bir cümle.- (Atılan örnek)
quantum_score→ "ileride belki vector quality'yi ölçeriz..." Bir cümle bile kuramıyorsun, eklemiyorsun.
Tek cümle testini geçen alan girer, geçmeyen girmez. Çok basit ama beni "her ihtimale karşı" şişirme tuzağından korudu.
"Composition root" hayatımı kurtardı
Go'da bağımlılıkları yönetmek için Wire, Fx, Dig gibi framework'ler var. Hepsi reflective. Çekici görünüyor — "bağımlılıkları otomatik wire eder!" Ama küçük servislerde bu maliyet/fayda çoğu zaman ters.
Mnemo'da tüm bağımlılık grafiği yaklaşık 10 obje. cmd/mnemo/main.go 250 satır altında ve her şey orada görünüyor. Bir constructor başka bir constructor'a el ile veriliyor. Sıralama gözle takip edilebiliyor. Bug çıktığında stack trace temiz. Yeni biri (veya gelecekteki ben) projeyi açtığında "burada ne kuruluyor" sorusunun cevabı tek dosyada.
DI framework'ler genelde aşağıdan yukarı doğru karmaşıklık artırır. Küçük projede karşılığını alamadan eklediğinde, basitliği kaybeder, debug zorluğu kazanırsın. Sadece servis cidden büyüdüğünde — 50+ obje, dinamik wiring, plugin sistemleri olduğunda — DI framework'ün ücretine değer.
Mnemo o boyutta olmadı, olmayacak. Manuel composition root olarak kalacak.
Vault ile paralel olmak: operasyonel CTRL+C/CTRL+V
Mnemo'yu yazmaya başlarken Vault zaten ayakta, çalışıyor, alışkanlıklarım oluşmuştu. "Vault nasıl deploy ediliyor" sorusunun cevabını kafamda gezdiriyordum: docker-compose, multi-stage Dockerfile, non-root user, nginx reverse proxy, Let's Encrypt, golang-migrate, pgxpool.
Mnemo için tek tek "en iyi pratik nedir" diye düşünmek yerine, Vault'un yaptığını kopyaladım. Bilinçli olarak.
Kazanç sadece zaman değil. Asıl kazanç operasyonel zihinsel yük: sunucuya SSH atınca her iki servis aynı yerde, aynı şekilde duruyor. Bir tanesini onarmayı bildiğimde diğerini de biliyorum. Backup, logging, deploy, rollback — hepsi aynı muscle memory'yi paylaşıyor.
Tek bilinçli ayrılık: Vault slog, Mnemo zerolog kullandı. Bunun tek sebebi Mnemo'nun brief'inde zerolog yazıyordu ve daha az allocation yapıyor. Önemli farklılaşma değil, kütüphane tercihi.
Buradan çıkardığım kural: sibling project'lerle bilinçli paralellik kur. Farklılık ancak gerçek bir teknik fark yaratıyorsa. Aksi takdirde her servis "biraz farklı şekilde" deploy edilen, "biraz farklı şekilde" loglanan, "biraz farklı şekilde" monitor edilen bir kar topu olur.
Karar verirken kullandığım filtre
Mnemo'nun ilk haftasında her teknik karar şu üç soruyu geçmek zorundaydı:
- Bu eklenti operasyonel yük getiriyor mu? (Yeni state-ful sistem? Yeni backup mı gerekli? Yeni monitoring metric'i mi?)
- Bu karar geri alınabilir mi? (Kütüphane değişimi 1 saat. DB değişimi 1 hafta. Önce küçük zorluklarla yaşamayı dene.)
- Bu eklenti tek cümlelik bir use case'e karşılık geliyor mu? (Hayır → ekleme. Evet → ekle.)
Üç soru çok az gibi görünebilir. Ama bu filtreden geçen kararlar geriye dönüp baktığımda hâlâ doğru görünüyor. Düşmeyen kararlar — Pinecone, Qdrant, Wire — eklenmiş olsaydı bugün hâlâ onların ücretini ödüyor olacaktım.
Sonraki yazıda
Üçüncü yazıda Mnemo'nun "akıllı" tarafına geleceğiz: LLM'in nasıl konuşabileceği bir API tasarımı (MCP), embedding katmanının nasıl bir interface ile yazıldığı (ve neden Voyage seçtiğim), hybrid search formülünün ardındaki sezgi, ve biyolojik decay metaforunun aslında pratik bir teknik karar olduğu.
Ama bu yazıyı bir cümleyle bitirmek istiyorum, çünkü Mnemo'nun ruhunu en iyi özetleyen şey bu: az şey, ama doğru şey. Vector DB tek. Backup tek. Migration tooling tek. Logging şekli tek. Aralarındaki zihinsel yük sıfıra yakın. Ölçeklediğinde göreceksin — basit bir sistemi karmaşık tutmak, karmaşık bir sistemi basitleştirmekten yüz kat zor.