Arkitektura e Kërkimit Hibrid Semantik me PostgreSQL pgvector dhe Next.js 16: Nga SQL LIKE te Kërkimi Inteligjent për E-Commerce në Ballkan
Pse kërkimi tradicional me SQL LIKE dështon përballë dialekteve dhe gabimeve drejtshkrimore në Kosovë dhe si të ndërtoni kërkim hibrid (BM25 Full-Text + pgvector) në PostgreSQL me Next.js 16 pa kosto të fryra të SaaS.

Në tregun e sotëm digjital në Kosovë, Shqipëri dhe Maqedoninë e Veriut, kërkimi brenda faqes (in-site search) është një ndër pikat më kritike të konversionit për çdo platformë tregtie elektronike, katalog B2B apo portal shërbimesh. Statistikat e industrisë tregojnë vazhdimisht se vizitorët që përdorin shiritin e kërkimit kanë deri në 3 herë më shumë gjasa të blejnë krahasuar me ata që thjesht shfletojnë kategoritë pasivisht.
Megjithatë, një përqindje dërrmuese e uebfaqeve vendore vazhdojnë të mbështeten mbi zgjidhje të vjetruara relacionale: pyetje primitive WHERE title ILIKE '%kerkimi%' ose, në rastin më të mirë, kërkime të thjeshta me fjalë kyçe (full-text search) që dështojnë plotësisht përballë realitetit gjuhësor të rajonit tonë.
Në këtë udhëzues arkitekturor dhe inxhinierik, Ekipi i Postieri XYZ L.L.C. analizon arsyet pse modelet tradicionale dështojnë, pse kalimi në SaaS të shtrenjta si Algolia apo Pinecone është shpesh një shpenzim i panevojshëm dhe si mund të ndërtoni një sistem Kërkimi Hibrid Semantik (BM25 + pgvector) me performancë nën 40ms, me kosto shtesë software $0, të integruar drejtpërdrejt në Next.js 16 dhe PostgreSQL.
1. Problemi Real në Tregun Rajonal: Dialektet, Huazimet dhe Gabimet Drejtshkrimore
Kërkimi në Ballkan paraqet sfida të veçanta gjuhësore dhe teknike që rrallëherë trajtohen nga libraritë standarde të zhvilluara për tregun anglishtfolës:
- Variacionet Dialektore dhe Sinonimet: Një konsumator në Prishtinë ose Gjakovë mund të kërkojë "patika për vrap", një në Tiranë "atlete për vrap", ndërsa një tjetër "sneakers". Nëse titulli i produktit në bazën e të dhënave është "Atlete Sportive Nike Pegasus", kërkimi tradicional me SQL
LIKE '%patika%'do të kthejë 0 rezultate. Konsumatori mendon se produkti nuk ekziston dhe largohet nga faqja. - Shkronjat me Diakritikë (ë, ç): Shumica e përdoruesve në pajisje mobile nuk përdorin tastierën shqipe. Ata shkruajnë "pantallona me vija" në vend të "pantallona me vija", ose "cmimi" në vend të "çmimi", "kemishe" në vend të "këmishë".
- Kërkimet e Bazuara në Qëllim (Intent-Based Search): Konsumatorët e sotëm, të mësuar me modelet e inteligjencës artificiale, nuk kërkojnë më vetëm me kode produktesh. Ata kërkojnë fraza konceptuale si: "fustan elegant për dasmë verore", "telefon me bateri që mban gjatë për punë në terren", ose "pjesë këmbimi për pompë uji të lavatriçes". Një bazë të dhënash relacionale nuk ka asnjë koncept semantik të fjalës "dasmë verore" nëse këto fjalë nuk gjenden fizikisht në titullin e fustanit.
- Kostoja dhe Vonesa e SaaS të Jashtme: Për të zgjidhur këtë problem, shumë agjenci rekomandojnë integrimin e shërbimeve të jashtme cloud (si Algolia, Elastic Cloud apo Pinecone). Përveç kostove mujore që shkojnë nga €300 deri në mbi €2,000 në muaj me rritjen e trafikut, kjo krijon dy probleme madhore:
- Vonesa e Rrjetit Ndërkufitar: Çdo kërkesë duhet të udhëtojë nga serveri në rajon drejt qendrave të të dhënave në Frankfurt ose Dublin dhe të kthehet mbrapsht.
- Data Drift dhe Sinkronizimi i Dyfishtë: Sa herë që ndryshon stoku, çmimi apo përshkrimi i një produkti në PostgreSQL, duhet të sinkronizohet menjëherë indeksi i jashtëm. Nëse sinkronizimi dështon për shkak të ndonjë gabimi të përkohshëm rrjeti, klienti mund të shohë në kërkim një produkt që nuk ka stok.
2. Pse Kërkimi Hibrid? (Leksikor + Semantik me RRF)
Për të arritur saktësi maksimale, nuk mjafton as vetëm kërkimi me fjalë kyçe dhe as vetëm kërkimi me vektorë semantikë:
- Kërkimi Leksikor (Lexical / BM25 / Trigram): Shkëlqen në gjetjen e kodeve ekzakte, SKU-ve, modeleve teknike (p.sh. "iPhone 16 Pro Max 256GB", "RTX 4080", "Bosch 0281002"). Në këto raste, kërkimi semantik mund të ngatërrohet me produkte me karakteristika të përafërta.
- Kërkimi Semantik (Vector Embeddings): Shkëlqen në kapjen e kuptimit, kontekstit, sinonimeve dhe qëllimit abstrakt ("veshje për mbrëmje gala", "kufje që izolojnë zhurmën").
- Kërkimi Hibrid (Hybrid Search): Ekzekuton të dy kërkimet paralelisht brenda të njëjtit motor dhe i bashkon rezultatet duke përdorur algoritmin e njohur inxhinierik Reciprocal Rank Fusion (RRF).
┌────────────────────────────────────────────────────────────────────────┐
│ Kërkesa e Përdoruesit │
│ "pantolla komode për zyrë pa hekurosje" │
└───────────────────────────────────┬────────────────────────────────────┘
│
┌────────────────────────┴────────────────────────┐
│ │
▼ ▼
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ Dega Leksikore (BM25) │ │ Dega Semantike (Dense) │
│ - tsvector (Fjalët kyçe) │ │ - Gjenerimi i Embedding │
│ - pg_trgm (Tolerancë typo) │ │ - HNSW pgvector Kërkim │
│ - unaccent (ë->e, ç->c) │ │ - Cosine Distance (<=>) │
└──────────────┬───────────────┘ └──────────────┬───────────────┘
│ │
│ Lista A (Top 50) │ Lista B (Top 50)
│ │
└────────────────────┬────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Reciprocal Rank Fusion (RRF) │
│ Score = Σ 1 / (60 + Rangu_Leksikor) │
│ + Σ 1 / (60 + Rangu_Semantik) │
│ + Filtrimi i Stokut & Çmimit (ACID) │
└────────────────────┬────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Rezultatet Përfundimtare të Renditura │
│ (Latenca totale: < 35 milisekonda) │
└─────────────────────────────────────────┘
3. Topologjia e Bazës së të Dhënave: Skema, Indekset HNSW dhe RRF në PostgreSQL
Falë ekstensionit të fuqishëm pgvector dhe veglave të integruara të PostgreSQL (pg_trgm, unaccent), e gjithë kjo logjikë ekzekutohet brenda bazës suaj ekzistuese të të dhënave, duke ruajtur të gjitha garancitë e transaksioneve (ACID) dhe duke lejuar filtrimin në kohë reale të çmimeve dhe stokut.
Hapi 1: Aktivizimi i Ekstensioneve
sql-- Aktivizimi i ekstensioneve thelbësore në PostgreSQL CREATE EXTENSION IF NOT EXISTS vector; CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE EXTENSION IF NOT EXISTS unaccent;
Hapi 2: Skema e Tabelës së Produkteve
Përdorim një vektor me 768 ose 1536 dimensione (në varësi të modelit të përdorur, p.sh. text-embedding-3-small ose modele të hapura si bge-base-multilingual):
sqlCREATE TABLE public.products ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), title VARCHAR(255) NOT NULL, slug VARCHAR(255) UNIQUE NOT NULL, description TEXT NOT NULL, category VARCHAR(100) NOT NULL, price NUMERIC(10, 2) NOT NULL, stock INT NOT NULL DEFAULT 0, is_active BOOLEAN NOT NULL DEFAULT true, -- Vektori i ngulitjes semantike (dense embedding) embedding vector(768), -- Vektori i kërkimit tekstual (lexical tsvector) tsv tsvector GENERATED ALWAYS AS ( setweight(to_tsvector('simple', unaccent(coalesce(title, ''))), 'A') || setweight(to_tsvector('simple', unaccent(coalesce(category, ''))), 'B') || setweight(to_tsvector('simple', unaccent(coalesce(description, ''))), 'C') ) STORED, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() );
Hapi 3: Krijimi i Indekseve me Performancë të Lartë
Për të garantuar që kërkimi të ekzekutohet në disa milisekonda edhe mbi qindra mijëra produkte, ndërtojmë indekse të specializuara:
sql-- 1. Indeksi HNSW për kërkim semantik me shpejtësi ekstreme -- HNSW (Hierarchical Navigable Small World) është dukshëm më i shpejtë se IVFFlat në prodhim CREATE INDEX idx_products_embedding_hnsw ON public.products USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64); -- 2. Indeksi GIN mbi tsvector për kërkim leksikor me pesha CREATE INDEX idx_products_tsv ON public.products USING gin (tsv); -- 3. Indeksi Trigram për të mbuluar gabimet drejtshkrimore (typo tolerance) në titull CREATE INDEX idx_products_title_trgm ON public.products USING gin (title gin_trgm_ops); -- 4. Indeks i përbërë për filtrim të shpejtë relacional CREATE INDEX idx_products_active_stock ON public.products (is_active, stock, price);
Hapi 4: Funksioni i Kërkimit Hibrid me Reciprocal Rank Fusion (SQL Function)
Ekzekutimi i shkrirjes së rangjeve (RRF) direkt në nivel SQL eliminon ngarkesën e rrjetit dhe shmang përpunimin e dhjetëra objekteve në Node.js:
sqlCREATE OR REPLACE FUNCTION hybrid_product_search( query_text TEXT, query_embedding vector(768), match_limit INT DEFAULT 20, rrf_k INT DEFAULT 60, min_price NUMERIC DEFAULT NULL, max_price NUMERIC DEFAULT NULL, filter_category TEXT DEFAULT NULL ) RETURNS TABLE ( id UUID, title VARCHAR, slug VARCHAR, category VARCHAR, price NUMERIC, stock INT, combined_score FLOAT ) LANGUAGE sql STABLE AS $$ WITH -- Dega Leksikore: Gjetja e top rezultateve me fjalë kyçe dhe trigrams lexical_results AS ( SELECT p.id, ROW_NUMBER() OVER ( ORDER BY ts_rank_cd(p.tsv, plainto_tsquery('simple', unaccent(query_text))) DESC, similarity(p.title, query_text) DESC ) AS rank_lexical FROM public.products p WHERE p.is_active = true AND p.stock > 0 AND (min_price IS NULL OR p.price >= min_price) AND (max_price IS NULL OR p.price <= max_price) AND (filter_category IS NULL OR p.category = filter_category) AND ( p.tsv @@ plainto_tsquery('simple', unaccent(query_text)) OR similarity(p.title, query_text) > 0.2 ) LIMIT 50 ), -- Dega Semantike: Gjetja e top rezultateve me distancë kosinusi semantic_results AS ( SELECT p.id, ROW_NUMBER() OVER (ORDER BY p.embedding <=> query_embedding) AS rank_semantic FROM public.products p WHERE p.is_active = true AND p.stock > 0 AND (min_price IS NULL OR p.price >= min_price) AND (max_price IS NULL OR p.price <= max_price) AND (filter_category IS NULL OR p.category = filter_category) AND p.embedding IS NOT NULL ORDER BY p.embedding <=> query_embedding LIMIT 50 ) -- Bashkimi me Reciprocal Rank Fusion (RRF) SELECT p.id, p.title, p.slug, p.category, p.price, p.stock, ( COALESCE(1.0 / (rrf_k + l.rank_lexical), 0.0) + COALESCE(1.0 / (rrf_k + s.rank_semantic), 0.0) )::FLOAT AS combined_score FROM lexical_results l FULL OUTER JOIN semantic_results s ON l.id = s.id JOIN public.products p ON p.id = COALESCE(l.id, s.id) ORDER BY combined_score DESC LIMIT match_limit; $$;
4. Implementimi në Next.js 16: Shërbimi i Embeddings dhe Server Actions
Në arkitekturën e Next.js 16, kombinojmë Server Actions të tipizuara, React 19 Streaming dhe një shtresë minimale caching për të mos shpenzuar thirrje të përsëritura për embeddings.
Shërbimi i Krijimit të Embeddings me Caching
typescript// lib/embeddings.ts import { createHash } from "node:crypto"; import { getDbPool } from "@/lib/db"; // Cache e shpejtë në memorie për query-t më të zakonshme const embeddingMemoryCache = new Map<string, number[]>(); export async function getQueryEmbedding(text: string): Promise<number[]> { const normalizedText = text.trim().toLowerCase(); const hashKey = createHash("sha256").update(normalizedText).digest("hex"); // 1. Kontrollo memorien lokale if (embeddingMemoryCache.has(hashKey)) { return embeddingMemoryCache.get(hashKey)!; } // 2. Gjenerimi i vektorit përmes shërbimit AI të optimizuar const response = await fetch("https://api.openai.com/v1/embeddings", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, }, body: JSON.stringify({ model: "text-embedding-3-small", // 768 ose 1536 dimensione input: normalizedText, dimensions: 768, }), }); if (!response.ok) { throw new Error(`Embedding API error: ${response.statusText}`); } const json = await response.json(); const vector: number[] = json.data[0].embedding; // Ruaj në cache në memorie (limituar në 1000 kërkesa) if (embeddingMemoryCache.size > 1000) { const firstKey = embeddingMemoryCache.keys().next().value; if (firstKey) embeddingMemoryCache.delete(firstKey); } embeddingMemoryCache.set(hashKey, vector); return vector; }
Server Action për Kërkimin në Next.js 16
typescript// app/actions/search-actions.ts "use server"; import { getDbPool } from "@/lib/db"; import { getQueryEmbedding } from "@/lib/embeddings"; import { z } from "zod"; const searchInputSchema = z.object({ query: z.string().min(1).max(150), category: z.string().optional(), minPrice: z.number().optional(), maxPrice: z.number().optional(), limit: z.number().min(1).max(50).default(20), }); export interface SearchResultItem { id: string; title: string; slug: string; category: string; price: number; stock: number; combinedScore: number; } export async function searchCatalogAction(input: z.infer<typeof searchInputSchema>): Promise<{ success: boolean; data: SearchResultItem[]; executionTimeMs: number; error?: string; }> { const start = performance.now(); const validation = searchInputSchema.safeParse(input); if (!validation.success) { return { success: false, data: [], executionTimeMs: 0, error: "Parametra të pavlefshëm kërkimi.", }; } const { query, category, minPrice, maxPrice, limit } = validation.data; const pool = getDbPool(); try { // Gjenerojmë vektorin semantik për pyetjen const queryVector = await getQueryEmbedding(query); const vectorSqlFormat = `[${queryVector.join(",")}]`; // Ekzekutojmë thirrjen e funksionit SQL const querySql = ` SELECT id, title, slug, category, price, stock, combined_score AS "combinedScore" FROM hybrid_product_search( $1::TEXT, $2::vector, $3::INT, 60, $4, $5, $6 ); `; const result = await pool.query(querySql, [ query, vectorSqlFormat, limit, minPrice || null, maxPrice || null, category || null, ]); const duration = Math.round(performance.now() - start); return { success: true, data: result.rows, executionTimeMs: duration, }; } catch (error) { console.error("[Hybrid Search Error]:", error); return { success: false, data: [], executionTimeMs: Math.round(performance.now() - start), error: "Ndodhi një gabim gjatë përpunimit të kërkimit.", }; } }
5. Ndërfaqja e Përdoruesit: UI Reaktive me React 19 dhe Next.js 16
Për të ofruar një përvojë të pandërprerë, përdorim hook-un modern useTransition nga React 19. Kjo parandalon ngrirjen e fushës së tekstit (input lag) dhe shfaq rezultatet menjëherë sapo Server Action të kthejë të dhënat.
tsx// components/search/InstantCatalogSearch.tsx "use client"; import { useState, useTransition, useId } from "react"; import { searchCatalogAction, type SearchResultItem } from "@/app/actions/search-actions"; export function InstantCatalogSearch() { const inputId = useId(); const [query, setQuery] = useState(""); const [results, setResults] = useState<SearchResultItem[]>([]); const [isPending, startTransition] = useTransition(); const [stats, setStats] = useState<{ count: number; timeMs: number } | null>(null); const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => { const val = e.target.value; setQuery(val); if (val.trim().length < 2) { setResults([]); setStats(null); return; } startTransition(async () => { const res = await searchCatalogAction({ query: val, limit: 12 }); if (res.success) { setResults(res.data); setStats({ count: res.data.length, timeMs: res.executionTimeMs }); } }); }; return ( <div className="w-full max-w-3xl mx-auto space-y-4"> <div className="relative"> <label htmlFor={inputId} className="sr-only">Kërko produkte</label> <input id={inputId} type="text" value={query} onChange={handleSearch} placeholder="Kërkoni me fjalë kyçe, kode ose qëllim (p.sh. 'fustan mbrëmjeje', 'patika nike')..." className="w-full px-5 py-3.5 rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 text-neutral-900 dark:text-neutral-100 placeholder:text-neutral-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 shadow-sm transition" /> {isPending && ( <div className="absolute right-4 top-3.5 text-xs text-emerald-600 dark:text-emerald-400 font-medium animate-pulse"> Duke analizuar... </div> )} </div> {stats && ( <div className="flex items-center justify-between text-xs text-neutral-500 dark:text-neutral-400 px-1"> <span>U gjetën {stats.count} produkte</span> <span>Koha e përgjigjes: <strong className="text-emerald-600 dark:text-emerald-400">{stats.timeMs}ms</strong></span> </div> )} {results.length > 0 && ( <div className="grid grid-cols-1 sm:grid-cols-2 gap-3 pt-2"> {results.map((item) => ( <div key={item.id} className="p-4 rounded-xl border border-neutral-200/80 dark:border-neutral-800/80 bg-neutral-50/50 dark:bg-neutral-900/50 hover:border-emerald-500/50 transition flex flex-col justify-between" > <div> <span className="text-[10px] font-semibold uppercase tracking-wider text-emerald-600 dark:text-emerald-400"> {item.category} </span> <h4 className="font-medium text-neutral-900 dark:text-neutral-100 line-clamp-1 mt-0.5"> {item.title} </h4> </div> <div className="flex items-center justify-between mt-3 pt-2 border-t border-neutral-200/40 dark:border-neutral-800/40"> <span className="font-semibold text-neutral-900 dark:text-neutral-100"> €{Number(item.price).toFixed(2)} </span> <span className="text-[11px] text-neutral-500 dark:text-neutral-400"> Stoku: {item.stock > 0 ? `${item.stock} copë` : "Mbaruar"} </span> </div> </div> ))} </div> )} </div> ); }
6. Analiza e Performancës dhe Krahasimi i Kostove: pgvector vs. SaaS
Për të kuptuar vlerën ekonomike dhe inxhinierike të kësaj zgjidhjeje, ja krahasimi i detajuar për një katalog me 100,000 produkte dhe 1 milion kërkime në muaj:
| Dimensioni | Zgjidhja Tradicionale (SQL LIKE) | SaaS e Jashtme (Algolia / Pinecone) | Arkitektura Postieri (PostgreSQL + pgvector) |
|---|---|---|---|
| Kostoja Mujore e Shërbimit | €0 (Pjesë e DB ekzistuese) | €450 - €1,800 / muaj | €0 (Pjesë e DB ekzistuese) |
| Kostoja e Embeddings | €0 | E përfshirë ose tarifë shtesë | ~€3 - €7 / muaj (vetëm për produkte të reja) |
| Toleranca ndaj Gabimeve (Typos) | ❌ Zero (Kthen 0 rezultate) | ✅ Shumë e mirë | ✅ E shkëlqyer (Trigram + Unaccent) |
| Kuptimi Semantik i Dialekteve | ❌ Dështon plotësisht | ⚠️ Mesatare (Kërkon rregulla manuale) | ✅ I lartë (Dense Vectors) |
| Vonesa Mesatare (Latency) | 120ms - 800ms (Table Scans) | 85ms - 220ms (Cross-Border HTTP) | 18ms - 38ms (Lidhje e brendshme) |
| Sinkronizimi i Stokut & Çmimit | ✅ I menjëhershëm | ❌ Rrezik për Data Drift dhe vonesa | ✅ ACID i garantuar në kohë reale |
| Pajtueshmëria Ligjore (Privatësia) | ✅ E plotë | ⚠️ Të dhënat dërgohen te palë të treta | ✅ 100% Brenda sovranitetit të serverit |
7. Përfundime dhe Rekomandime Praktike për Ndërmarrjet
Për bizneset dhe zhvilluesit në Kosovë dhe rajon, mirëmbajtja e arkitekturave të thjeshta dhe të qëndrueshme është çelësi i suksesit afatgjatë. Shtimi i shërbimeve të jashtme SaaS për çdo veçori rrit kompleksitetin e mirëmbajtjes, rrit kostot operative fikse dhe dobëson sigurinë e të dhënave.
Duke shfrytëzuar PostgreSQL pgvector me HNSW indekse dhe Next.js 16, ju arrini:
- Konversion më të Lartë të Shitjeve: Konsumatorët gjejnë menjëherë atë që kërkojnë, pavarësisht nëse shkruajnë me gabime, me dialekt apo me koncepte të përgjithshme.
- Kursim Radikal Financiar: Shmangni abonimet e fryra të SaaS të jashtme pa bërë asnjë kompromis me cilësinë apo shpejtësinë e kërkimit.
- Integritet Absolut të të Dhënave: Çmimi dhe stoku që sheh klienti janë gjithmonë 100% të sakta në sekondën që bëhet kërkimi.
Nëse ndërmarrja juaj menaxhon një platformë tregtie elektronike, katalog me shumicë (B2B) ose portal të dhënash me vëllim të lartë dhe dëshiron të modernizojë infrastrukturën e kërkimit:
- Na kontaktoni direkt në WhatsApp: +383 44 81 81 80
- Vizitoni shërbimet tona inxhinierike: postieri.xyz
- Ekipi i Postieri XYZ L.L.C. projekton dhe implementon arkitektura moderne ueb, integrime sistemesh ERP dhe zgjidhje të avancuara të inteligjencës artificiale të përshtatura posaçërisht për sfidat e tregut vendor.


