Iurii RoguliaIurii Rogulia
AboutServicesPricingProjectsStackReviewsPhrasesBlog
Contact
Iurii ships.

Iurii Rogulia, IT partner for business & fractional CTO. Professionally building software since 2001.

Think of a number
PricingQuality checklistPrivacy PolicyCookie Policy

Business

TMI Iurii Rogulia
VAT ID: FI29845875
DUNS: 368664211
Lappeenranta, Finland 🇫🇮

[email protected]
Back to projects

Iurii Integrates: Pikkuna — i18n RAG AI System

December 15, 2025

RAG system on OpenAI and Upstash Vector with 30 language support. Includes streaming chatbot, hybrid search (semantic + keyword), AI ticket classifier, and incremental indexing with SHA-256 caching.

Live demo

Stack

Next.jsReactTypeScript

Libraries

Vercel AI SDKnext-intlassistant-uiZod

Databases

Upstash VectorRedis

Services

OpenAIUpstashVercel

Topics

RAGAI Chatboti18nE-commerceCTO

Key Results

  • Resolves 70%+ of support queries with no human involvement, day or night
  • Answers customers in any of 30 languages without extra support hires
  • Freed the support team from hours of daily copy-paste replies
  • Draft answers ready in under 4 seconds instead of 40+ seconds
Pikkuna — i18n RAG AI System

The Challenge

After the e-commerce platform launched across 32 countries, the support inbox became unmanageable. Customers from Germany, Estonia, Russia, the Netherlands were writing in their native languages every day, asking the same questions: delivery times to their country, sizing recommendations, installation on different wall types, which product was right for their situation.

The support team was spending three to four hours daily on copy-paste replies — reading a question in German, looking up the shipping table for Germany, translating the answer, drafting the email. Response times were slipping. The business was growing, more markets were being added, and the support load was only going to increase. Hiring more support staff to handle questions that were 70% identical was not a viable path.

The Solution

I built a RAG system that indexes the entire knowledge base — 1,614 documents covering products, delivery times, sizing guides, and installation FAQs — and serves answers through two interfaces: a streaming chat widget on the site and an AI classifier embedded in Zoho Desk.

The chatbot handles context correctly across languages. A customer asking "And to Germany?" as a follow-up gets the right answer without having to repeat the full question, because the query is enriched with prior conversation context before retrieval. The system does not filter by locale at the retrieval stage — cross-lingual vector search means a question asked in Dutch can match a document indexed in English or Finnish when the semantic content is the same.

The Zoho Desk classifier is a separate two-stage pipeline. It first extracts the core question from the email (stripping greetings, order numbers, and pleasantries), then runs RAG retrieval, then classifies the ticket into one of 18 categories and generates a draft reply with a confidence score. High-confidence tickets are handled automatically; anything below the threshold is routed to a human with the extracted question and relevant context already prepared.

Results

The support team went from spending half their day on repetitive emails to reviewing flagged exceptions. Seventy percent of queries are now resolved without human involvement, at any hour, in any of 30 languages.

MetricValue
Knowledge base1,614 documents
Languages30 (multilingual retrieval)
Vector dimensions3,072 (text-embedding-3-large)
RAG retrieval<500ms (P95)
Ingestion script1,843 lines
Incremental updatesSHA-256 cache (only changed docs)

The incremental indexing means the knowledge base stays current — when shipping rates or product details change, only the affected documents are re-embedded.

i18n Support

AspectImplementation
DocumentsIndexed for each of 30 locales separately
Retrieval (chat)No locale filter — cross-lingual search
Retrieval (search)With locale filter — results in UI language
Product detectioni18n dictionary (EN/RU/FI/NL/DE)
Fuzzy matchingNormalization: й→и, ё→е, ä→a, ö→o, ü→u, ß→ss
LLM responseAuto-switch to user's language
Brand namesLocalized: Pikkuna / Пиккуна / Πίκκουνα / 皮库娜

System Architecture

┌─────────────────────────────────────────────────────────────────────┐
│                    DATA INGESTION (Offline)                         │
│                                                                     │
│  Sources:                      Pipeline:                            │
│  ├─ src/locales/*.json        ┌─────────────┐    ┌──────────────┐   │
│  │  (30 langs)         ──────►│ Extract &   │───►│ SHA-256 Hash │   │
│  ├─ docs/**/*.md              │ Chunk       │    │ Cache Check  │   │
│  └─ next-intl.config.js       └─────────────┘    └──────┬───────┘   │
│                                                  Changed only       │
│                                ┌─────────────┐   ┌──────▼────────┐  │
│                                │ OpenAI      │◄──│ Generate      │  │
│                                │ Embedding   │   │ Embeddings    │  │
│                                │ 3-large     │   └───────────────┘  │
│                                └──────┬──────┘                      │
│                                ┌──────▼──────┐                      │
│                                │  Upstash    │                      │
│                                │  Vector     │                      │
│                                │  (1614 docs)│                      │
│                                └─────────────┘                      │
└─────────────────────────────────────────────────────────────────────┘
                                      │
                                      ▼
┌─────────────────────────────────────────────────────────────────────┐
│                    RUNTIME (Online, per request)                    │
│                                                                     │
│  ┌───────────┐   ┌───────────┐   ┌─────────────────────────┐        │
│  │  ChatBot  │   │  Search   │   │  Ticket Classifier      │        │
│  │  (Stream) │   │  (Hybrid) │   │  (Zoho Desk webhook)    │        │
│  └─────┬─────┘   └─────┬─────┘   └───────────┬─────────────┘        │
│        └───────────────┼─────────────────────┘                      │
│                        ▼                                            │
│              ┌─────────────────────┐                                │
│              │  retrieveContext()  │                                │
│              │  1. Embed query     │                                │
│              │  2. Detect product  │                                │
│              │  3. Vector search   │                                │
│              │  4. Re-rank         │                                │
│              └──────────┬──────────┘                                │
│                         ▼                                           │
│              ┌─────────────────────┐                                │
│              │  OpenAI LLM         │                                │
│              │  OpenAI model (chat)│                                │
│              │  OpenAI model (cls) │                                │
│              └─────────────────────┘                                │
└─────────────────────────────────────────────────────────────────────┘

Under the Hood

For the technically curious, here is how the core pieces are built.

RAG Pipeline Architecture

A single retrieveContext function used in both the chatbot and the ticket classifier:

// src/lib/rag-chat.ts
export const retrieveContext = async (
  query: string,
  locale: string = "en"
): Promise<RetrievalResult[]> => {
  // 1. Detect product by keywords (multilingual dictionary)
  const detectedProducts = detectProductFromQuery(query);
  // → ['pikkuna'] | ['pikkuroof'] | ['general']
 
  // 2. Generate query embedding
  const { embedding } = await embed({
    model: openai.embedding("text-embedding-3-large"),
    value: query,
  });
 
  // 3. Vector search (no locale filter — all languages available)
  const results = await vectorIndex.query({
    vector: embedding,
    topK: 15, // candidates for filtering
    includeMetadata: true,
  });
 
  // 4. Re-ranking with product boost
  const reranked = results
    .filter((r) => r.score >= 0.7)
    .map((r) => {
      let score = r.score;
      const productTag = r.metadata?.productTag;
 
      // +30% boost if product matches query
      if (productTag && detectedProducts.includes(productTag)) {
        score = Math.min(1.0, score * 1.3);
      }
      // -40% penalty if product doesn't match (and not general)
      else if (productTag && productTag !== "general") {
        score *= 0.6;
      }
 
      return { ...r, score };
    })
    .sort((a, b) => b.score - a.score)
    .slice(0, 5);
 
  return reranked;
};
 
// i18n dictionary for product detection
const PRODUCT_KEYWORDS = {
  pikkuna: [
    "side curtain",
    "vertical",
    "pole",
    "post", // EN
    "боковые",
    "вертикальн",
    "столб", // RU
    "sivuverho",
    "pylväs",
    "tolppa", // FI
    "zijgordijn",
    "verticaal",
    "paal", // NL
    "seitenvorhang",
    "vertikal",
    "stütze", // DE
  ],
  pikkuroof: [
    "roof",
    "pergola",
    "horizontal",
    "canopy",
    "крыша",
    "перголы",
    "горизонтальн",
    "навес",
    "katto",
    "pergola",
    "vaakasuora",
  ],
};

Streaming Chat with Vercel AI SDK

Integration with @assistant-ui/react for a ready-to-use UI with conversation persistence:

// src/app/api/chat/route.ts
export async function POST(req: Request) {
  const { messages, locale } = await req.json();
 
  // Handle follow-up questions (context enrichment)
  const query = buildContextualQuery(messages);
  // "And to Germany?" → "how much is shipping... And to Germany?"
 
  // RAG retrieval
  const relevantDocs = await retrieveContext(query, locale);
 
  // Build context for prompt
  const contextMessage = relevantDocs
    .map((doc, i) => `[${i + 1}] ${doc.metadata?.category}:\n${doc.text}`)
    .join('\n\n');
 
  // Streaming response
  const result = streamText({
    model: openai(process.env.OPENAI_CHAT_MODEL!),
    system: SYSTEM_PROMPT + `\n\nKnowledge base context:\n${contextMessage}`,
    messages,
    temperature: 0.7,
  });
 
  // Format compatible with @assistant-ui/react
  return result.toUIMessageStreamResponse();
}
 
// src/components/ChatBot/ChatBot.tsx
const chat = useChat({
  api: '/api/chat',
  body: { locale },
  initialMessages: loadFromLocalStorage(),  // persistence
});
 
const runtime = useAISDKRuntime(chat);
 
return (
  <AssistantRuntimeProvider runtime={runtime}>
    <AssistantModal />
  </AssistantRuntimeProvider>
);

Hybrid Search (Semantic + Keyword)

Search endpoint combines vector similarity with fuzzy keyword matching:

// src/app/api/search/route.ts
export async function GET(req: Request) {
  const { query, locale, type } = parseParams(req);
 
  // 1. Query embedding
  const { embedding } = await embed({
    model: openai.embedding("text-embedding-3-large"),
    value: query,
  });
 
  // 2. Vector search WITH locale filter (unlike chat)
  const results = await vectorIndex.query({
    vector: embedding,
    topK: 50,
    filter: `locale = "${locale}"${type ? ` AND type = "${type}"` : ""}`,
    includeMetadata: true,
  });
 
  // 3. Fuzzy keyword matching
  const queryWords = normalizeForSearch(query).split(" ");
  // normalizeForSearch: й→и, ё→е, ä→a, ö→o, ü→u, ß→ss
 
  // 4. Hybrid scoring
  const scored = results.map((r) => {
    const titleMatches = countMatches(r.metadata?.question, queryWords);
    const contentMatches = countMatches(r.metadata?.text, queryWords);
 
    // Hybrid score = vector + keyword bonus
    const hybridScore = r.score + Math.max(titleMatches * 0.25, contentMatches * 0.08);
 
    return { ...r, hybridScore, hasKeywordMatch: titleMatches > 0 };
  });
 
  // 5. Smart filtering
  // With keyword matches → threshold 0.62
  // Without keyword matches → threshold 0.70 (stricter)
  const filtered = scored.filter((r) =>
    r.hasKeywordMatch ? r.hybridScore >= 0.62 : r.score >= 0.7
  );
 
  return Response.json({
    hits: filtered.slice(0, 5),
    totalHits: filtered.length,
  });
}

AI Ticket Classifier with Two-Stage Pipeline

Auto-categorization of Zoho Desk emails with RAG enrichment:

// src/app/api/zoho/ticket-classifier/route.ts
export async function POST(req: Request) {
  const { subject, description } = await req.json();
  const fullMessage = `${subject}\n\n${description}`;
 
  // === STAGE 1: Extract core question ===
  const extraction = await generateText({
    model: openai(process.env.OPENAI_MODEL!),
    temperature: 0.1, // strict for extraction
    system: EXTRACTION_PROMPT,
    prompt: fullMessage,
  });
  // "Hello! When will my order #12345 arrive? Thanks, John"
  // → "When will my order arrive?"
 
  // === STAGE 2: RAG retrieval ===
  const context = await retrieveContext(extraction.text, "en");
 
  // === STAGE 3: Classification + Reply Generation ===
  const classification = await generateText({
    model: openai(process.env.OPENAI_MODEL!),
    temperature: 0.3, // strict for JSON output
    system: CLASSIFIER_PROMPT, // 18 categories + confidence rules
    prompt: `
FULL MESSAGE: ${fullMessage}
CORE QUESTION: ${extraction.text}
KNOWLEDGE BASE: ${context.map((c) => c.text).join("\n\n")}
    `,
  });
 
  const result = JSON.parse(classification.text);
  // {
  //   language: "en",
  //   classification: "Order status inquiry",
  //   confidence: 0.85,
  //   reply_en: "Orders typically ship within 2-3 business days...",
  // }
 
  // If confidence <= 0.3 → reply empty (requires manager)
  return Response.json(result);
}
 
// 18 classification categories
const CATEGORIES = [
  "Order status inquiry",
  "Quote request",
  "Custom drawing",
  "Reclamation::Film defect",
  "Reclamation::Incorrect size",
  "Reclamation::Delivery Issue",
  "Partnership",
  "Spam",
  // ... 10 more
];

Incremental Indexing with SHA-256 Cache

Only changed documents get re-indexed:

// scripts/ingest-locales-to-upstash.ts
async function ingestDocuments(options: { force?: boolean }) {
  // 1. Load hash cache
  const cache = await loadCache(); // .cache/rag-hashes.json
 
  // 2. Extract documents from all sources
  const documents = [
    ...extractFAQs(locales), // pages.support.content
    ...extractProducts(locales), // pages.products.*
    ...extractDeliveryTimes(config), // next-intl.config.js
    ...extractShippingCosts(config), // shipping_rates + VAT
    ...extractMarkdownDocs(docsDir), // docs/**/*.md
  ];
 
  // 3. Compute SHA-256 hashes
  const currentHashes = documents.reduce((acc, doc) => {
    acc[doc.id] = crypto.createHash("sha256").update(doc.text).digest("hex").slice(0, 16);
    return acc;
  }, {});
 
  // 4. Compare with cache
  const { added, changed, deleted } = compareWithCache(currentHashes, cache);
 
  if (!options.force && added.length === 0 && changed.length === 0) {
    console.log("No changes detected, skipping ingestion");
    return;
  }
 
  // 5. Delete stale vectors
  if (deleted.length > 0) {
    await vectorIndex.delete(deleted);
  }
 
  // 6. Generate embeddings (batch of 10, rate-limit friendly)
  const toUpdate = [...added, ...changed];
  const embeddings = await generateEmbeddingsBatch(toUpdate, 10);
 
  // 7. Upsert to Upstash (batch of 100)
  await upsertVectors(embeddings, 100);
 
  // 8. Save new cache
  await saveCache({ hashes: currentHashes, version: "2.0" });
}
 
// npm run ingest-rag          → incremental
// npm run ingest-rag --force  → full rebuild
Iurii RoguliaAvailable

Need something similar?

I build custom solutions — from APIs to full products. Let's talk about your project.

View all projects

Related projects

Pikkuna — E-commerce for Vinyl Curtains & PVC Products
Pikkuna — E-commerce for Vinyl Curtains & PVC Products
October 12, 2024
Pikkuna — E-commerce for Vinyl Curtains & PVC Products

International e-commerce platform with 30 locales, product configurators, AI chatbot, and fully automated order flow: Stripe → Zoho CRM → Airtable → Mailgun →

Stack

Next.jsReactTypeScript

Libraries

next-intlZodpdf-libPuppeteerStripe.js

Databases

Redis

Services

StripeZohoMailgunPostNordNetvisorVercelVercel BlobGoogle AnalyticsMeta CAPIAirtableUpstash

Topics

E-commercePaymentsShippingPDFPWACTOArchitectureSEOSchema.orgi18nPerformance
pi-pi.ee — B2B E-commerce for Waterless Urinal Systems
pi-pi.ee — B2B E-commerce for Waterless Urinal Systems
January 17, 2026
pi-pi.ee — B2B E-commerce for Waterless Urinal Systems

i18n B2B e-commerce platform for waterless urinal products across 32 European countries with automated VAT handling, PDF invoicing, and CRM integration.

Stack

Next.jsReactTypeScriptTailwind CSS

Libraries

next-intlDaisyUIStripe SDKreact-pdf

Services

StripeVercelResendNotionGoogle Analytics

Topics

E-commerceB2Bi18nPDFSSRArchitectureSEOSchema.org

Related posts

i18n RAG Chatbot Architecture (25 Languages, Production)
October 27, 2025· 9 min
i18n RAG Chatbot Architecture (25 Languages, Production)

RAG chatbot for e-commerce: resolve 70% of support queries across 25 languages — ingestion pipeline, hybrid search, confidence thresholds, and streaming UI.

Stack

Next.jsTypeScript

Libraries

Vercel AI SDK

Databases

Upstash Vector

Services

OpenAIUpstashZoho

Topics

RAGAI ChatbotE-commercei18n
How to Add AI to an Existing Product Without Rewriting It
May 4, 2026· 11 min
How to Add AI to an Existing Product Without Rewriting It

Add AI to an existing product without a rebuild. Three integration patterns, how to pick the right one, and what production-ready AI actually demands.

Stack

Next.jsTypeScript

Libraries

Vercel AI SDK

Databases

PostgreSQLRedisUpstash Vector

Services

OpenAI

Topics

RAGArchitectureSaaSE-commerce