LocalAI ile Metin Gömme Sunucusu Kurulumu: Semantic Search ve RAG Pipeline

Semantic search ve RAG pipeline kurmak isteyip de nereden başlayacağını bilmeyenler için bu yazı biraz uzun ama sonunda her şey yerli yerine oturmuş olacak. LocalAI, OpenAI API’siyle uyumlu çalışan açık kaynaklı bir sunucu ve biz bugün bunu metin gömme (embedding) servisi olarak kullanacağız. Kendi verinizin üzerinde arama yapmak, doküman tabanlı soru-cevap sistemi kurmak ya da sadece “bunu nasıl yapıyorlar” merakını gidermek için okumaya devam edin.

LocalAI Nedir, Neden Embedding için Kullanıyoruz?

LocalAI, Go ile yazılmış, OpenAI API formatını taklit eden bir çıkarım sunucusu. LLaMA, Whisper, Stable Diffusion gibi modelleri çalıştırabildiği gibi embedding modelleri için de gayet iyi iş çıkarıyor. Bizim için en önemli avantajı şu: Veri gizliliği kritikse, kurumsal dökümanları dışarı göndermek istemiyorsanız ya da sadece aylık API faturasından kurtulmak istiyorsanız, LocalAI tam anlamıyla ihtiyacınıza cevap veriyor.

Embedding, metni vektör uzayında bir noktaya dönüştürme işlemi. “Kedi” ile “köpek” kelimeleri anlam olarak yakın olduğu için vektör uzayında birbirine yakın konumlanır. Bunu semantic search ve RAG (Retrieval-Augmented Generation) için kullanıyoruz. RAG’ı kısaca anlatmak gerekirse: LLM’e soru sorduğunuzda, model önce ilgili dökümanları bulup sonra bu bağlamla cevap üretiyor. Embedding bu “ilgili dökümanları bul” kısmının omurgasını oluşturuyor.

Sunucu Gereksinimleri

GPU olması güzel ama zorunlu değil. CPU ile de embedding çalıştırabilirsiniz, inference hızı biraz düşük olur ama production’da makul düzeyde kalır.

Önerilen minimum donanım:

  • RAM: 8 GB (model için 4 GB, sistem için 4 GB)
  • CPU: 4 çekirdek, AVX2 desteği olan modern bir işlemci
  • Disk: 10 GB boş alan (model dosyaları için)
  • İşletim Sistemi: Ubuntu 22.04 LTS veya RHEL/Rocky Linux 9

GPU kullanacaksanız CUDA 11.8+ kurulu NVIDIA kartı gerekiyor. Embedding modelleri LLM’lere göre çok daha hafif olduğu için 6-8 GB VRAM yeterli.

LocalAI Kurulumu

Docker ile kurmak en temiz yol. Önce Docker ve Docker Compose olmadığı durumlar için binary kurulumu göstereyim, sonra Docker’a geçelim.

Binary ile Kurulum (Ubuntu 22.04)

# Gerekli bağımlılıkları yükle
sudo apt update
sudo apt install -y build-essential cmake git wget curl

# LocalAI son sürümünü indir
LOCALAI_VERSION=$(curl -s https://api.github.com/repos/go-skynet/LocalAI/releases/latest | grep tag_name | cut -d'"' -f4)
wget https://github.com/go-skynet/LocalAI/releases/download/${LOCALAI_VERSION}/local-ai-Linux-x86_64 -O local-ai
chmod +x local-ai
sudo mv local-ai /usr/local/bin/

# Dizin yapısını oluştur
sudo mkdir -p /opt/localai/{models,config}
sudo chown -R $USER:$USER /opt/localai

Docker Compose ile Kurulum

Pratikte Docker ile çalışmak çok daha rahat, özellikle model yönetimi ve restart politikaları açısından.

# /opt/localai/docker-compose.yml
version: '3.8'

services:
  localai:
    image: quay.io/go-skynet/local-ai:latest-aio-cpu
    container_name: localai
    ports:
      - "8080:8080"
    volumes:
      - ./models:/build/models
      - ./config:/build/config
    environment:
      - THREADS=4
      - CONTEXT_SIZE=4096
      - DEBUG=false
      - GALLERIES=github:go-skynet/model-gallery/index.yaml
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/readyz"]
      interval: 30s
      timeout: 10s
      retries: 5

GPU desteği istiyorsanız image’ı değiştirip deploy kısmını ekleyin:

# GPU destekli versiyon için ek konfigürasyon
services:
  localai:
    image: quay.io/go-skynet/local-ai:latest-aio-gpu-nvidia-cuda-12
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]
    environment:
      - CUDA_VISIBLE_DEVICES=0

Servisi başlatın:

cd /opt/localai
docker compose up -d

# Logları kontrol et
docker compose logs -f localai

# Sağlık kontrolü
curl http://localhost:8080/readyz

Embedding Modeli İndirme ve Yapılandırma

LocalAI, bert.cpp ve llama.cpp backend’lerini destekliyor. Embedding için en çok kullanılan modeller all-MiniLM-L6-v2 ve nomic-embed-text. Türkçe metin için multilingual-e5-large veya paraphrase-multilingual-mpnet-base-v2 daha iyi sonuç veriyor.

Model Gallery ile Otomatik İndirme

# Mevcut embedding modellerini listele
curl http://localhost:8080/models/available | python3 -m json.tool | grep -A2 "embed"

# Model gallery üzerinden yükle
curl http://localhost:8080/models/apply 
  -H "Content-Type: application/json" 
  -d '{"id": "bert-embeddings"}'

# İndirme durumunu takip et
curl http://localhost:8080/models/jobs

Manuel Model Yapılandırması

Gallery’de bulamadığınız modeller için ya da Hugging Face’den kendi modelinizi kullanmak istiyorsanız manuel yapılandırma gerekiyor.

# Multilingual model için dizin oluştur
mkdir -p /opt/localai/models/multilingual-e5

# Hugging Face'den GGUF formatında model indir
# intfloat/multilingual-e5-large modelinin quantize edilmiş versiyonu
wget -P /opt/localai/models/ 
  "https://huggingface.co/ChristianAzinn/multilingual-e5-large-GGUF/resolve/main/multilingual-e5-large-q4_k_m.gguf"

Model konfigürasyon dosyasını oluşturun:

# /opt/localai/config/multilingual-e5.yaml
name: multilingual-e5
backend: llama-cpp
embeddings: true
parameters:
  model: multilingual-e5-large-q4_k_m.gguf
context_size: 512
f16: true
threads: 4

Konfigürasyonu yüklemek için servisi yeniden başlatın veya API üzerinden tetikleyin:

# Config dosyasını yükle (restart gerekmez)
curl http://localhost:8080/v1/models

Embedding API’sini Test Etme

Her şey ayakta mı kontrol edelim. LocalAI, OpenAI’nin /v1/embeddings endpoint’ini taklit ediyor, bu yüzden mevcut OpenAI client’ları doğrudan çalışıyor.

# Basit embedding testi
curl http://localhost:8080/v1/embeddings 
  -H "Content-Type: application/json" 
  -d '{
    "input": "LocalAI ile embedding sunucusu kuruyoruz",
    "model": "multilingual-e5"
  }' | python3 -c "
import json, sys
data = json.load(sys.stdin)
vec = data['data'][0]['embedding']
print(f'Vektör boyutu: {len(vec)}')
print(f'İlk 5 değer: {vec[:5]}')
print(f'Token kullanımı: {data["usage"]}')
"

Birden fazla metin için batch işlem:

curl http://localhost:8080/v1/embeddings 
  -H "Content-Type: application/json" 
  -d '{
    "input": [
      "Türkiye büyük bir ülkedir",
      "Ankara başkentimizdir",
      "Python programlama dilidir"
    ],
    "model": "multilingual-e5"
  }' | python3 -c "
import json, sys
data = json.load(sys.stdin)
for i, item in enumerate(data['data']):
    print(f'Metin {i}: {len(item["embedding"])} boyutlu vektör')
"

Semantic Search Pipeline Kurma

Şimdi asıl işe gelelim. Basit bir semantic search pipeline kuracağız. Vektör veritabanı olarak Qdrant kullanacağız, çünkü Docker ile çok kolay kurulabiliyor ve production’da sağlam durumda.

Qdrant Kurulumu

# Docker Compose dosyasına Qdrant ekle
cat >> /opt/localai/docker-compose.yml << 'EOF'

  qdrant:
    image: qdrant/qdrant:latest
    container_name: qdrant
    ports:
      - "6333:6333"
      - "6334:6334"
    volumes:
      - ./qdrant_storage:/qdrant/storage
    restart: unless-stopped
EOF

docker compose up -d qdrant

Python ile End-to-End Pipeline

Gerekli kütüphaneleri yükleyin:

pip install openai qdrant-client python-dotenv tqdm

Şimdi gerçek dünya senaryosu: Bir şirketin iç wiki dökümanlarını indeksleyip semantic arama yapabilir hale getireceğiz.

#!/usr/bin/env python3
"""
LocalAI + Qdrant ile Semantic Search Pipeline
Kullanım: python3 semantic_search.py
"""

import os
import json
import uuid
from pathlib import Path
from typing import List, Dict
from openai import OpenAI
from qdrant_client import QdrantClient
from qdrant_client.models import (
    Distance, VectorParams, PointStruct, Filter, FieldCondition, MatchValue
)

# LocalAI istemcisini yapılandır (OpenAI uyumlu)
ai_client = OpenAI(
    base_url="http://localhost:8080/v1",
    api_key="localai-dummy-key"  # LocalAI API key gerektirmiyor
)

# Qdrant istemcisi
qdrant = QdrantClient(host="localhost", port=6333)

COLLECTION_NAME = "wiki_docs"
EMBEDDING_MODEL = "multilingual-e5"
VECTOR_SIZE = 1024  # multilingual-e5-large için


def get_embedding(text: str) -> List[float]:
    """Tek metin için embedding al"""
    response = ai_client.embeddings.create(
        input=text,
        model=EMBEDDING_MODEL
    )
    return response.data[0].embedding


def get_embeddings_batch(texts: List[str]) -> List[List[float]]:
    """Batch embedding - daha verimli"""
    response = ai_client.embeddings.create(
        input=texts,
        model=EMBEDDING_MODEL
    )
    return [item.embedding for item in response.data]


def setup_collection():
    """Qdrant collection oluştur"""
    existing = [c.name for c in qdrant.get_collections().collections]
    if COLLECTION_NAME not in existing:
        qdrant.create_collection(
            collection_name=COLLECTION_NAME,
            vectors_config=VectorParams(
                size=VECTOR_SIZE,
                distance=Distance.COSINE
            )
        )
        print(f"Collection oluşturuldu: {COLLECTION_NAME}")
    else:
        print(f"Collection zaten mevcut: {COLLECTION_NAME}")


def index_documents(documents: List[Dict]):
    """Dökümanları vektör veritabanına ekle"""
    setup_collection()
    
    # Batch olarak işle (15'erli gruplar)
    batch_size = 15
    for i in range(0, len(documents), batch_size):
        batch = documents[i:i + batch_size]
        texts = [doc["content"] for doc in batch]
        
        print(f"Embedding alınıyor: {i+1}-{min(i+batch_size, len(documents))}/{len(documents)}")
        embeddings = get_embeddings_batch(texts)
        
        points = [
            PointStruct(
                id=str(uuid.uuid4()),
                vector=embedding,
                payload={
                    "content": doc["content"],
                    "title": doc.get("title", ""),
                    "category": doc.get("category", "genel"),
                    "source": doc.get("source", "")
                }
            )
            for doc, embedding in zip(batch, embeddings)
        ]
        
        qdrant.upsert(collection_name=COLLECTION_NAME, points=points)
    
    print(f"Toplam {len(documents)} döküman indekslendi.")


def semantic_search(query: str, top_k: int = 5, category: str = None) -> List[Dict]:
    """Semantic arama yap"""
    query_embedding = get_embedding(query)
    
    # Opsiyonel kategori filtresi
    search_filter = None
    if category:
        search_filter = Filter(
            must=[FieldCondition(key="category", match=MatchValue(value=category))]
        )
    
    results = qdrant.search(
        collection_name=COLLECTION_NAME,
        query_vector=query_embedding,
        limit=top_k,
        query_filter=search_filter,
        with_payload=True
    )
    
    return [
        {
            "score": result.score,
            "title": result.payload.get("title", ""),
            "content": result.payload.get("content", ""),
            "category": result.payload.get("category", ""),
            "source": result.payload.get("source", "")
        }
        for result in results
    ]


# Test verisi
sample_docs = [
    {
        "title": "VPN Kurulum Rehberi",
        "content": "Şirket VPN'ine bağlanmak için OpenVPN client kurmanız gerekmektedir. IT departmanından config dosyasını alın.",
        "category": "it-destek",
        "source": "wiki/vpn"
    },
    {
        "title": "Uzaktan Çalışma Politikası",
        "content": "Uzaktan çalışma talepleri İK departmanına 3 iş günü öncesinden bildirilmelidir.",
        "category": "hr",
        "source": "wiki/hr-policy"
    },
    {
        "title": "Sunucu Yedekleme Prosedürü",
        "content": "Kritik sunucular her gece 02:00'de otomatik yedeklenir. Yedekler 30 gün boyunca saklanır.",
        "category": "it-ops",
        "source": "wiki/backup"
    }
]

if __name__ == "__main__":
    # Dökümanları indeksle
    index_documents(sample_docs)
    
    # Arama yap
    query = "evden çalışma izni nasıl alırım"
    results = semantic_search(query, top_k=3)
    
    print(f"nSorgu: '{query}'")
    print("-" * 50)
    for i, r in enumerate(results, 1):
        print(f"{i}. [{r['score']:.3f}] {r['title']}")
        print(f"   {r['content'][:100]}...")

RAG Pipeline: LLM ile Entegrasyon

Semantic search’ü bulduk, şimdi bunu bir LLM ile birleştirip gerçek bir RAG sistemi kuralım. LocalAI üzerinde hem embedding hem de LLM çalıştırabilirsiniz.

#!/usr/bin/env python3
"""
Basit RAG Pipeline - LocalAI + Qdrant
"""

from openai import OpenAI

# Aynı LocalAI sunucusu hem embedding hem LLM için
ai_client = OpenAI(
    base_url="http://localhost:8080/v1",
    api_key="dummy"
)

LLM_MODEL = "llama-3.2-3b"  # LocalAI'da yüklü olan modeliniz


def rag_query(user_question: str, top_k: int = 3) -> str:
    """
    RAG sorgusu:
    1. Soruyu embedding'e çevir
    2. İlgili dökümanları bul
    3. LLM'e bağlam ile sor
    """
    # Adım 1 & 2: Semantic search (önceki fonksiyonu kullanıyoruz)
    relevant_docs = semantic_search(user_question, top_k=top_k)
    
    if not relevant_docs:
        return "İlgili bilgi bulunamadı."
    
    # Adım 3: Bağlamı hazırla
    context_parts = []
    for i, doc in enumerate(relevant_docs, 1):
        context_parts.append(f"[Kaynak {i}] {doc['title']}n{doc['content']}")
    
    context = "nn".join(context_parts)
    
    system_prompt = """Sen bir şirket asistanısın. Sana verilen şirket wiki belgelerine dayanarak soruları yanıtla.
Eğer belgeler soruyu yanıtlamak için yeterli bilgi içermiyorsa, bunu açıkça belirt.
Yanıtını Türkçe ver."""
    
    user_prompt = f"""Aşağıdaki şirket belgelerini kullanarak soruyu yanıtla:

{context}

Soru: {user_question}"""
    
    response = ai_client.chat.completions.create(
        model=LLM_MODEL,
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_prompt}
        ],
        temperature=0.3,
        max_tokens=500
    )
    
    return response.choices[0].message.content


# Kullanım
if __name__ == "__main__":
    sorular = [
        "VPN'e nasıl bağlanabilirim?",
        "Yedeklemeler ne zaman yapılıyor?",
        "Uzaktan çalışmak istiyorum, ne yapmalıyım?"
    ]
    
    for soru in sorular:
        print(f"nSoru: {soru}")
        print(f"Cevap: {rag_query(soru)}")
        print("-" * 60)

Performans İzleme ve Optimizasyon

Production’a almadan önce birkaç şeyi ayarlamak lazım.

LocalAI Prometheus Metrikleri

LocalAI, /metrics endpoint’i üzerinden Prometheus metriklerini açıyor. Bu metrikler arasında inference latency, token throughput ve model load süreleri bulunuyor. Bunu Grafana ile görselleştirmek standart bir yapıya dönüştürebilir.

# Metriklere bak
curl http://localhost:8080/metrics | grep -E "localai|embedding|latency"

# Systemd service olarak çalıştırıyorsanız log rotasyonu
cat > /etc/logrotate.d/localai << 'EOF'
/var/log/localai/*.log {
    daily
    rotate 7
    compress
    delaycompress
    missingok
    notifempty
}
EOF

Embedding Cache ile Hızlanma

Aynı metinleri defalarca embed etmek gereksiz. Basit bir Redis cache ekleyelim:

import hashlib
import json
import redis

cache = redis.Redis(host='localhost', port=6379, db=0)
CACHE_TTL = 86400  # 24 saat

def get_embedding_cached(text: str) -> list:
    """Redis cache'li embedding"""
    cache_key = f"emb:{hashlib.md5(text.encode()).hexdigest()}"
    
    cached = cache.get(cache_key)
    if cached:
        return json.loads(cached)
    
    # Cache'de yoksa LocalAI'dan al
    embedding = get_embedding(text)
    cache.setex(cache_key, CACHE_TTL, json.dumps(embedding))
    return embedding

Nginx ile Reverse Proxy ve Rate Limiting

# /etc/nginx/sites-available/localai
upstream localai_backend {
    server 127.0.0.1:8080;
    keepalive 32;
}

server {
    listen 443 ssl http2;
    server_name ai.sirketiniz.com;

    ssl_certificate /etc/letsencrypt/live/ai.sirketiniz.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ai.sirketiniz.com/privkey.pem;

    # Embedding endpoint için rate limiting
    limit_req_zone $binary_remote_addr zone=embedding:10m rate=30r/m;

    location /v1/embeddings {
        limit_req zone=embedding burst=10 nodelay;
        proxy_pass http://localai_backend;
        proxy_set_header Host $host;
        proxy_read_timeout 120s;
    }

    location / {
        proxy_pass http://localai_backend;
        proxy_set_header Host $host;
    }
}

Yaygın Sorunlar ve Çözümleri

  • Model yüklenmiyor: Config YAML’daki backend değerinin doğru olduğundan emin olun. GGUF modelleri için llama-cpp, BERT modelleri için bert yazılmalı.
  • Vektör boyutu uyuşmazlığı: Qdrant collection’ı oluştururken size değeri modelinizin gerçek çıktısıyla eşleşmeli. Test ederek boyutu doğrulayın.
  • Yavaş embedding: threads değerini sunucunuzdaki fiziksel çekirdek sayısının yarısına ayarlayın, HyperThreading’i hesaba katmayın.
  • OOM hatası: Aynı anda birden fazla model yüklüyse bellek tükenebilir. PRELOAD_MODELS ortam değişkeni ile sadece ihtiyacınız olanı önyükleyin.
  • Docker network sorunu: LocalAI ve Qdrant ayrı container’lardaysa Qdrant’a localhost yerine container adıyla bağlanın (qdrant:6333).

Sonuç

Bu yazıda LocalAI’yi bir embedding sunucusu olarak sıfırdan kurup Qdrant ile entegre ettik, semantic search ve RAG pipeline inşa ettik. Tüm bu altyapı tamamen kendi sunucunuzda, dışarıya bir byte bile göndermeden çalışıyor.

Gerçek dünyada bu kurulumu genişletmek için birkaç öneri: Qdrant’ın koleksiyon bazlı yetkilendirmesini mutlaka açın, embedding modelinizi düzenli aralıklarla daha yeni versiyonlarla değiştirip performansı karşılaştırın ve büyük döküman kümeleri için indeksleme işlemini Celery ya da basit bir job queue ile asenkron hale getirin. Türkçe içerik ağırlıklı çalışıyorsanız multilingual-e5-large modeli gerçekten iyi sonuçlar veriyor, all-MiniLM gibi İngilizce ağırlıklı modellere göre belirgin fark var.

LocalAI ekibi aktif geliştirme yapıyor, özellikle v2.x serisi ile model yönetimi çok daha olgunlaştı. GitHub sayfasını takip etmek ve release note’ları okumak iyi bir alışkanlık.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir