# 04 — RAG Sistemi (Bilgi Tabanı)

## Ne İş Yapar?

Tenant'ın ürünlerini, SSS'lerini, politikalarını ve web sitesini vektör olarak saklar. Ziyaretçi soru sorduğunda semantik olarak en yakın içeriği bulur ve LLM'e context olarak verir. Halüsinasyonu önlemenin temel mekanizmasıdır.

---

## Veri Modeli

```
flovy_knowledge_sources
├── id (UUID)
├── tenant_id
├── source_type (product / faq / url / pdf / manual)
├── title
├── url                  ← scrape kaynağı (varsa)
├── content              ← ham içerik (varsa — PDF/FAQ için)
├── status (pending / indexing / indexed / failed)
├── chunk_count (INT)
├── last_indexed_at
├── error_message
└── created_at

flovy_knowledge_chunks
├── id (UUID)
├── tenant_id
├── source_id (FK → flovy_knowledge_sources)
├── source_type          ← tekrar tutuluyor (sorgu performansı için)
├── content (TEXT)       ← chunk metni
├── embedding (JSON)     ← float array [0.123, -0.456, ...]
├── embedding_model      ← "text-embedding-004"
├── meta (JSON)          ← { "product_id": ..., "url": ..., "page": ... }
└── created_at
```

---

## Kaynak Tipleri

| Tip | Açıklama | Otomatik mi? |
|---|---|---|
| `product` | Ürün title + description + bullet_points | Evet — ürün eklenince |
| `faq` | Manuel SSS girdisi | Hayır — panel'den girilir |
| `url` | Site sayfası scrape | Yarı otomatik — URL girilir, Celery scraper |
| `pdf` | Katalog PDF yükleme | Hayır — dosya yüklenir, Celery parse |
| `manual` | Serbest metin bloğu | Hayır — panel'den girilir |

---

## Embedding Pipeline

```python
class KnowledgeIndexer:
    async def index_product(self, product: Product):
        text = f"{product.title}\n{product.description}\n"
        if product.bullet_points:
            text += "\n".join(product.bullet_points)

        chunks = self.chunk_text(text, max_tokens=500, overlap=50)
        source = await self.get_or_create_source(product.tenant_id, "product", product.id)

        for chunk_text in chunks:
            vec = await self.embedding_client.embed(chunk_text, task="RETRIEVAL_DOCUMENT")
            await db.execute(insert(KnowledgeChunk).values(
                tenant_id=product.tenant_id,
                source_id=source.id,
                source_type="product",
                content=chunk_text,
                embedding=vec,
                meta={"product_id": str(product.id)},
            ))

    def chunk_text(self, text: str, max_tokens: int, overlap: int) -> list[str]:
        # Paragraf bazlı chunking → kelime bazlı fallback
        ...
```

---

## Retriever

```python
class KnowledgeRetriever:
    QUERY_EMBED_CACHE_TTL = 86400  # 24 saat

    async def top_k(
        self,
        tenant_id: str,
        query: str,
        k: int = 5,
        source_type: str = None,
    ) -> list[dict]:
        # 1. Query embedding (Redis cache)
        cache_key = f"flovy:qemb:{tenant_id}:{md5(query.lower().strip())}"
        query_vec = await redis.get(cache_key)
        if not query_vec:
            query_vec = await self.embedding_client.embed(query, task="RETRIEVAL_QUERY")
            await redis.setex(cache_key, self.QUERY_EMBED_CACHE_TTL, json.dumps(query_vec))

        # 2. Tenant'ın chunk'larını çek (max 2000 — pgvector'a geçince DB seviyesinde)
        q = select(KnowledgeChunk).where(
            KnowledgeChunk.tenant_id == tenant_id,
            KnowledgeChunk.embedding.isnot(None),
        )
        if source_type:
            q = q.where(KnowledgeChunk.source_type == source_type)
        chunks = await db.scalars(q.limit(2000))

        # 3. Cosine similarity (numpy)
        query_arr = np.array(query_vec)
        scored = []
        for chunk in chunks:
            chunk_arr = np.array(chunk.embedding)
            score = np.dot(query_arr, chunk_arr) / (
                np.linalg.norm(query_arr) * np.linalg.norm(chunk_arr) + 1e-10
            )
            scored.append({"chunk": chunk, "score": float(score)})

        scored.sort(key=lambda x: x["score"], reverse=True)
        return scored[:k]
```

**Not:** Numpy cosine, PHP cosine'den çok daha hızlı. 2000 chunk için ~5ms. Yine de ilerleyen fazda pgvector/MySQL vec extension ile DB seviyesine taşınacak.

---

## Gemini Embedding Client

```python
class EmbeddingClient:
    async def embed(self, text: str, task: str = "RETRIEVAL_DOCUMENT") -> list[float]:
        cache_key = f"flovy:emb:{md5(text[:200])}"
        cached = await redis.get(cache_key)
        if cached:
            return json.loads(cached)

        async with httpx.AsyncClient() as client:
            resp = await client.post(
                f"{settings.GEMMA_ENDPOINT}/models/text-embedding-004:embedContent",
                params={"key": settings.GEMINI_API_KEY},
                json={
                    "model": "models/text-embedding-004",
                    "content": {"parts": [{"text": text}]},
                    "taskType": task,
                },
                timeout=10.0,
            )
        vec = resp.json()["embedding"]["values"]
        await redis.setex(cache_key, 3600, json.dumps(vec))
        return vec
```

---

## Anchor Warming (Semantic Router için)

`flovy:warm-anchors` Celery görevi günde bir kez çalışır:
- SemanticRouter'daki intent anchor cümlelerini embed eder
- Redis'e 7 gün TTL ile kaydeder
- Bu sayede semantic routing'in ilk isteği yavaş değildir

---

## API Endpoint'leri

```
GET  /api/knowledge/sources              ← Kaynak listesi
POST /api/knowledge/sources/faq         ← Manuel SSS ekle
POST /api/knowledge/sources/url         ← URL ekle (Celery scrape başlar)
POST /api/knowledge/sources/pdf         ← PDF yükle
DELETE /api/knowledge/sources/{id}      ← Kaynak + chunk'ları sil
POST /api/knowledge/sources/{id}/reindex ← Yeniden indexle

GET  /api/knowledge/search?q=           ← Test araması (panel debug için)
```

---

## Gotcha'lar

- Embedding API rate limit: Gemini text-embedding-004 dakikada 15 istek (free tier). Celery worker'ı `rate_limit=10/m` ile sınırla.
- Chunk boyutu 500 token'ı geçmemeli — Gemini input limiti 2048.
- Tenant'ın chunk sayısı 2000'i geçmeye başlarsa yavaşlama olur. pgvector geçişi bu noktada tetiklenir.
- Silinmiş ürünün chunk'ları da silinmeli — cascade delete veya `on_product_delete` event.
