15b. modulAI Gateway és AI Search a gyakorlatban
Cloudflare for Devs · 15b. modul — mélymerülés

AI Gateway és AI Search a gyakorlatban

A két darab, ami a te SaaS-odban tényleg számít: a Gateway mint az AI-forgalom kontrollpontja (költséggel, kvótával, biztonsággal), és az AI Search mint dokumentum-kereső — konkrét, tenantonként szeparált példával, plusz őszinte összevetés a Postgres full-text search-csel.

15b.1AI Gateway: nem proxy, hanem kontrollpont

A 15. modulban „proxynak" neveztem, de ez alulértékeli. Pontosabb: az AI Gateway az a hely, ahol az összes AI-hívásod áthalad — és emiatt az egyetlen pont, ahol egységesen tudsz mérni, korlátozni, cache-elni és védeni. Ha egy multitenant SaaS-ban AI-funkciót adsz ki, három kérdés fog felmerülni az első hónapban: mennyibe kerül tenantonként? mi van, ha valaki visszaél vele? mi van, ha a szolgáltató elesik? — mindháromra ez a válasz.

Egységes API — a szolgáltatófüggetlenség gyakorlata

A Gateway ma egységes REST API-t ad az összes modellhez, szolgáltatótól függetlenül. Négy endpoint-alak közül választhatsz aszerint, hogy melyik SDK-t használod:

EndpointMire
POST /ai/rununiverzális: bármely modell, bármely modalitás
POST /ai/v1/chat/completionsOpenAI SDK-kompatibilis — a meglévő kódod base URL-cserével működik
POST /ai/v1/responsesOpenAI Responses API-kompatibilis
POST /ai/v1/messagesAnthropic SDK-kompatibilis
curl -X POST "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/ai/v1/chat/completions" \
  -H "Authorization: Bearer $CF_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "model": "openai/gpt-5.5",
        "messages": [{"role":"user","content":"Foglald össze ezt a ticketet…"}] }'

Figyeld meg, mi nincs ebben a hívásban: OpenAI API-kulcs. Két modell közül választhatsz:

A hat képesség — és mit adnak neked konkrétan

KépességMit csinálMire jó a SaaS-odban
Loggingminden kérés/válasz, modell, tokenszám, user agent; szűrhetőenhibakeresés, minőség-ellenőrzés, „miért ezt válaszolta?"
Cachingazonos kérésekre cache-elt válaszköltség és latencia: ismétlődő promptoknál (súgó-magyarázatok, sablon-generálás) azonnali nyereség
Rate limitingkérésszám-korlát a szolgáltató előttvisszaélés-védelem; a hívás el sem megy, tehát nem is kerül pénzbe
Spend limitsdollár-alapú költségkeret; a tokenhasználatból és a modellárból számolt valós költséget méri, és 429-cel blokkol a keret felettez a kulcs a tenant-szintű költségvédelemhez — lásd lent
Guardrails + DLPprompt/válasz szűrése nem biztonságos tartalomra; érzékeny adatok (személyes, pénzügyi) felismerésecompliance és felelősség: nem a te kódodban kell szűrőt írni
Fallback + retryhiba esetén másik modell/szolgáltatórendelkezésre állás: egy szolgáltatói kiesés nem visz le funkciót

A gyöngyszem: tenant-szintű költségkeret custom metadatával

Ez az a képesség, amiért érdemes végigolvasni ezt a szakaszt. A spend limit szabályai egyéni metaadat-dimenziók szerint is szűkíthetők — vagyis ha minden hívásba beteszed a tenant_id-t metaadatként, akkor tenantonként tudsz napi/havi dollárkeretet szabni, a Gateway szintjén, a te kódod nélkül:

server/utils/ai.ts — minden hívás megjelöli a tenantot
import OpenAI from "openai";

export function aiClient(env, tenantId: string, userId: string) {
  return new OpenAI({
    apiKey: env.CF_API_TOKEN,
    baseURL: `https://api.cloudflare.com/client/v4/accounts/${env.CF_ACCOUNT_ID}/ai`,
    defaultHeaders: {
      // egyéni metaadat: ezek lesznek a spend limit és a logok dimenziói
      "cf-aig-metadata": JSON.stringify({ tenant_id: tenantId, user_id: userId }),
      "cf-aig-gateway-id": "saas-prod",
    },
  });
}

Ezután a dashboardon felveszel néhány szabályt — kódmódosítás nélkül:

SzabályMit véd
tenantonként max $20/napegy elszabadult ügyfél nem viszi el a havi AI-keretedet
userenként max $2/napegy tenanton belüli visszaélés (vagy hibás integráció)
gateway-szinten max $500/napa globális vészfék — bármi is történjen
a drága modellre $50/napa frontier-modell használatának plafonja
Gondold végig, mit jelentene ez saját fejlesztésben: tokenszámlálás minden hívásnál, modellenkénti árazás karbantartása, aggregálás tenantonként, atomikus számláló (7. modul: Durable Object!), majd döntés minden kérés előtt. Ez több napnyi munka, és utána karban is kell tartani, ahogy az árak változnak. Itt konfiguráció. Egy dolgot tudj: a spend limit eventually consistent — párhuzamos kérésekből egy rövid burst átcsúszhat a limit felett, mielőtt az érvényesítés utoléri. Kemény, azonnali kvótához (pl. „ingyenes csomagban napi 10 kérdés") továbbra is a saját DO-limitered kell (7. modul); a spend limit a költségvédelem, nem a termék-kvóta.

15b.2AI Search: mi az, amit elrejt előled?

Ahhoz, hogy értsd, mit spórolsz vele, előbb nézzük meg, mi a teljes RAG-pipeline, amit egyébként neked kellene megépítened és üzemeltetned:

INDEXELÉS (feltöltéskor) dokumentum PDF, docx, md… parse szöveg-kinyerés chunk darabolás embed vektorizálás index vektor + kulcsszó LEKÉRDEZÉS (kérdéskor) kérdés természetes nyelv query embed + átfogalmazás hibrid keresés vektor + kulcsszó rangsorolás boosting válasz találatok vagy szöveg ezt a két sort adja készen az AI Search — neked az instance és a fájl marad
15b/1. ábra — A teljes RAG-pipeline. Vectorize-zal a kék dobozokat te építed; AI Search-csel csak feltöltesz és kérdezel.

Egy instance = beépített tár + vektorindex + a fenti pipeline. Nem kell hozzá R2-bucket, embedding-modell-választás, chunkolási stratégia vagy külön index-karbantartás. Amit még kapsz:

Egy nem nyilvánvaló csapda: ha az AI Search modellhívásaihoz saját AI Gatewayt kötsz be, ne kapcsold be azon a gatewayen a cache-t és a rate limitet. Az embedding-hívásoknál a cache hibás vektorokat adhat vissza, ami csendben rontja a keresési minőséget; a rate limit pedig megakasztja az indexelést (ami sok embedding-hívás). A találatok cache-elésére az AI Search saját similarity cache-e való.

15b.3A konkrét példa: dokumentum-szeparáció shared-DB SaaS-ban

A helyzeted: közös adatbázis tenant_id oszlopokkal (5b. modul), és most jön egy funkció, hogy a felhasználók feltölthetik a saját dokumentumaikat, és kérdezhetnek belőlük. A kérdés: hogyan szeparálod?

Az architektúra-döntés

A hivatalos — és szerintem is helyes — ajánlás: tenantonként egy instance. Az üzleti adat marad a közös DB-ben, a dokumentumok viszont fizikailag külön indexbe kerülnek. A kettő nem mond ellent egymásnak: a shared DB-d marad, csak a kereshető tartalom szeparálódik.

wrangler.jsonc
{
  "ai_search_namespaces": [
    { "binding": "AI_SEARCH", "namespace": "default", "remote": true }
  ]
}

A remote: true itt kötelező a lokális fejlesztéshez (11. modul): az AI Search nem fut lokálisan, a wrangler dev a deployolt instance-okhoz proxyzik.

1) Tenant-onboarding: instance létrehozása

Illeszd be a meglévő onboarding-folyamatodba (7. modul Workflow-jába, ha ott van):

server/utils/docs.ts
export const tenantIndexId = (tenantId: string) => `tenant-${tenantId}`;

export async function provisionTenantIndex(env, tenantId: string) {
  await env.AI_SEARCH.create({ id: tenantIndexId(tenantId) });
}

export async function deprovisionTenantIndex(env, tenantId: string) {
  await env.AI_SEARCH.delete(tenantIndexId(tenantId));   // GDPR-törléskor: egy hívás
}

2) Feltöltés: a fájl két helyre kerül

server/api/docs/upload.post.ts
export default defineEventHandler(async (event) => {
  const { env } = event.context.cloudflare;
  const { tenantId, userId } = await requireSession(event);   // a saját auth-od
  const form = await readMultipartFormData(event);
  const file = form.find((f) => f.name === "file");

  const docId = crypto.randomUUID();
  const key = `${tenantId}/${docId}/${file.filename}`;

  // (a) az eredeti fájl R2-be — ez marad az igazság (6. modul)
  await env.UPLOADS.put(key, file.data, {
    httpMetadata: { contentType: file.type },
    customMetadata: { tenantId, userId, docId },
  });

  // (b) a kereshető másolat a TENANT SAJÁT indexébe
  const index = env.AI_SEARCH.get(tenantIndexId(tenantId));
  await index.items.uploadAndPoll(`${docId}__${file.filename}`, file.data);

  // (c) a metaadat a közös DB-be — itt él a jogosultság és a listázás
  await env.DB.prepare(
    `INSERT INTO documents (id, tenant_id, owner_user_id, filename, r2_key, visibility)
     VALUES (?, ?, ?, ?, ?, ?)`
  ).bind(docId, tenantId, userId, file.filename, key, "team").run();

  return { ok: true, docId };
});

A hármas felosztás szándékos, és a kurzus korábbi elveit követi: R2 az eredeti fájl (olcsó, nulla egress), AI Search a kereshető index (ez „csak" derivált adat — újraépíthető), D1/Postgres a metaadat és a jogosultság (ez a tranzakcionális igazság, 6. modul hármas felosztása).

3) Keresés: közös tudásbázis + tenant dokumentumai egyetlen hívásban

Itt jön a rész, ami elegánsabb, mint elsőre gondolnád. A namespace-szintű kereséssel több instance-t kérdezhetsz egy hívásban, és minden találat magával hozza, melyik indexből jött:

server/api/docs/ask.post.ts
export default defineEventHandler(async (event) => {
  const { env } = event.context.cloudflare;
  const { tenantId } = await requireSession(event);
  const { question } = await readBody(event);

  const results = await env.AI_SEARCH.search({
    messages: [{ role: "user", content: question }],
    ai_search_options: {
      // a közös termék-dokumentáció ÉS a tenant saját anyagai — egy rangsorban
      instance_ids: ["product-docs", tenantIndexId(tenantId)],
    },
  });

  // forrás szerint csoportosítva adjuk vissza a UI-nak
  return {
    answer: results.response,
    sources: results.data.map((chunk) => ({
      text: chunk.content,
      from: chunk.instance_id === "product-docs" ? "Súgó" : "Saját dokumentumok",
    })),
  };
});

Ez az a pont, ahol a szeparáció szerkezeti, nem fegyelmi kérdés: a lekérdezés csak azokat az indexeket látja, amiket felsorolsz — és a tenant azonosítója a session-ből jön, nem a kliens kéréséből. Egy másik tenant adata nem attól nem jelenik meg, mert jól írtad meg a WHERE-t, hanem mert fizikailag nincs abban az indexben, amit megkérdeztél.

4) Ha a tenanton belül user-szintű szeparáció is kell

Gyakori igény: egy tenanton belül a HR-es dokumentumait ne lássa a fejlesztő. Három szintet érdemes ismerni:

SzintMegoldásMikor
Tenantinstance tenantonkéntmindig — ez az alap
Csoport/projektinstance csoportonként (tenant-acme-hr), és a keresésnél csak a jogosult instance-okat sorolod felha kevés, jól körülhatárolt csoport van, és erős izoláció kell
Felhasználó / finomhangolt jogosultságutószűrés a saját DB-dből: a találatok docId-jait visszaellenőrzöd a documents táblán a user jogosultságávalha sok, dinamikusan változó jogosultság van (ez az általános eset)
a jogosultsági utószűrés mintája
// a találatokból kinyerjük a dokumentum-azonosítót (a fájlnév-prefixből)
const docIds = [...new Set(results.data.map(c => c.filename.split("__")[0]))];

// és a KÖZÖS DB dönt arról, mit láthat ez a user
const allowed = await env.DB.prepare(
  `SELECT id FROM documents
   WHERE tenant_id = ? AND id IN (${docIds.map(() => "?").join(",")})
     AND (visibility = 'team' OR owner_user_id = ?)`
).bind(tenantId, ...docIds, userId).all();

const allowedSet = new Set(allowed.results.map(r => r.id));
const visible = results.data.filter(c => allowedSet.has(c.filename.split("__")[0]));
Fontos figyelmeztetés a generált válaszra: ha a generált szöveges választ kéred (nem a nyers találatokat), akkor a modell már látta az összes visszakapott chunkot, mielőtt te szűrnél. Finomhangolt jogosultságnál ezért vagy a nyers találatokat kérd le, szűrj, és csak utána generálj a szűrt kontextusból — vagy eleve olyan indexeket kérdezz, amiket a user teljes egészében láthat. Ez ugyanaz a hiba-osztály, mint a „SELECT után szűrünk az appban": itt viszont a modell a szivárgás csatornája.

5) Újraépítés és karbantartás

Mivel az index derivált adat, a helyreállítás egyszerű: az R2-ben ott az összes eredeti fájl, a DB-ben a lista — egy Workflow (7. modul) végigmegy rajtuk és újraindexel. Ezt érdemes megírni még az indulás előtt: ez a te „mi van, ha elromlik az index" válaszod.

15b.4Miért (és miben nem) jobb ez a Postgres full-text search-nél?

Fontos, hogy ezt tisztán lásd, mert a kettő nem ugyanazt a problémát oldja meg, és a „jobb" kérdésre az őszinte válasz: attól függ, mit keresel.

Mit csinál valójában a Postgres FTS?

A tsvector/tsquery lexikai keresés: szótövezés, stopszavak, súlyozás, rangsorolás (ts_rank), GIN-index. Kiváló, ha a felhasználó tudja a szavakat, amiket keres. Amit viszont nem tud: nem érti a jelentést. A „hogyan mondom le az előfizetést" kérdés nem fogja megtalálni a „számlázási ciklus megszüntetése" című szakaszt, mert nincs közös szótő. És nem tud PDF-et olvasni, nem darabol, nem generál választ.

Az összevetés

SzempontPostgres FTSAI Search
Keresés jellegekulcsszó/lexikai; a pontos egyezés erősségehibrid: jelentés + kulcsszó
Természetes nyelvű kérdésgyengeerre való
Fájlformátumok (PDF, docx)neked kell kinyerned a szövegetbeépített feldolgozás
Chunkolás, embedding, indexnincs (pgvectorral: te építed)kezelt pipeline
Generált válasz forrásokkalnincsvan
Konzisztencia az adattaltranzakcionális: a commit után azonnal kereshetőaszinkron: az indexelés késik, külön rendszer
SQL-lel kombinálható (join, filter, sorrend)természeteskülön rendszer; utószűrés kell
Pontos szűrők (dátum, státusz, összeg)erőskorlátozottabb
Multitenant izolációWHERE tenant_id = ?egy kifelejtett feltétel = szivárgáskülön instance — szerkezetileg nem szivároghat
Üzemeltetésnulla új rendszerúj függőség, új limitek, új költségsor
Költséggyakorlatilag ingyen (a DB-den fut)tárolás + lekérdezés + modellhívás
Törlés / GDPRDELETEinstance törlése egy hívással

A verdikt — három szabály

  1. Az alkalmazásod saját rekordjaira (számla, ügyfél, ticket cím szerint) maradj a Postgresnél. Ott a tranzakcionális konzisztencia, a joinok és a pontos szűrők számítanak — és az AI Search ezekben gyengébb, nem erősebb.
  2. Feltöltött dokumentumokra és természetes nyelvű kérdésekre az AI Search a jobb. Nem azért, mert „AI", hanem mert a teljes pipeline-t (PDF-feldolgozás, chunkolás, embedding, hibrid keresés, rangsorolás) kapod készen — ezt pgvectorral hetekig építenéd, és utána karbantartanád.
  3. Multitenant szempontból pedig van egy strukturális érv: a Postgres FTS-nél az izoláció fegyelmi kérdés (minden lekérdezésben ott a WHERE tenant_id), az AI Search-nél szerkezeti (nem is létezik a másik tenant adata abban az indexben). Egy hibás query az egyiknél adatszivárgás, a másiknál üres találatlista.
És ha PlanetScale Postgresen ülsz? Az nem érv sem mellette, sem ellene: a kérdés nem a szolgáltató, hanem a keresés fajtája. A józan felállás nálad: a strukturált keresés (ügyfélnév, számlaszám, státusz) marad a Postgresen — akár Hyperdrive mögött (4. modul) —, és mellé jön az AI Search a dokumentumokra. Ne cseréld le az egyiket a másikra; a kettő nem versenyez, hanem kiegészíti egymást. Ha pedig csak egy egyszerű „keresés a súgóban" funkció kell és nincs feltöltött dokumentumod, a Postgres FTS bőven elég — ne vezess be új rendszert egy megoldott problémára (16. modul zárógondolata).

15b.5Költség és limitek — amit előre tudni kell

15b.6Ellenőrizd magad

  1. Hogyan szabsz tenantonként napi dollárkeretet az AI-funkciódra, kódmódosítás nélkül?
    Válasz

    Minden hívásba egyéni metaadatot teszel (cf-aig-metadata: tenant_id, user_id), majd a Gatewayen spend limit szabályt veszel fel erre a dimenzióra. A Gateway a tokenhasználatból és a modellárból számolt valós költséget méri, és 429-cel blokkol a keret felett. Kemény termék-kvótához viszont továbbra is saját DO-limiter kell, mert a spend limit eventually consistent.

  2. Mit ad az AI Search egy pgvector-alapú saját megoldáshoz képest?
    Válasz

    A teljes pipeline-t: fájlfeldolgozás (PDF, docx), chunkolás, embedding, index-karbantartás, hibrid keresés (vektor + kulcsszó), rangsorolás/boosting, similarity cache és generált válasz forrásokkal. pgvectorral ezt mind te építed és tartod karban — az adatbázis csak a vektorokat tárolja.

  3. Miért „szerkezeti" az izoláció instance-onként, szemben a WHERE tenant_id mintával?
    Válasz

    Mert a lekérdezés csak a felsorolt instance-okat látja, és a tenant azonosítója a session-ből jön: a másik tenant adata fizikailag nincs abban az indexben. Egy hibás lekérdezés így üres találatot ad, nem idegen adatot — a shared táblánál viszont egy kifelejtett feltétel azonnal szivárgás.

  4. Egy tenanton belül user-szintű jogosultság kell. Mi a javasolt minta, és mi a csapda a generált válasznál?
    Válasz

    Utószűrés a saját DB-dből: a találatok dokumentum-azonosítóit visszaellenőrzöd a documents táblán a user jogosultságával. A csapda: ha generált választ kérsz, a modell már látta az összes chunkot a szűrés előtt — ilyenkor előbb nyers találatokat kérj, szűrj, és csak a szűrt kontextusból generálj.

  5. Miért ne kapcsold be az AI Gateway cache-ét azon a gatewayen, amit az AI Search használ?
    Válasz

    Mert az embedding-hívásokra is hatna: cache-elt vektorok kerülhetnek az indexbe vagy a lekérdezésbe, ami csendben rontja a találati minőséget. A rate limit hasonlóan megakaszthatja az indexelést. Találat-cache-re az AI Search saját similarity cache-e való.

  6. Mikor maradj mégis a Postgres full-text search-nél?
    Válasz

    Ha az alkalmazásod saját rekordjaiban keresel (ügyfélnév, számlaszám, ticket-cím), ahol a tranzakcionális konzisztencia, a joinok és a pontos szűrők számítanak; vagy ha egyszerű kulcsszavas keresés is elég és nincsenek feltöltött dokumentumok. Ne vezess be új rendszert egy már megoldott problémára.

Előző15. modul — AI a Cloudflare platformon Következő 16. modul — A platform teljes szolgáltatás-térképe