Hybrid pretraga u PostgreSQL: FTS i pgvector u istoj tablici
Postoji zabluda koja se uvukla u arhitekturu modernih AI sustava kao podmukli mem. Ljudi su uvjereni da je vektorska pretraga, posebno s pgvector ekstenzijom, jedino što im treba. Dovoljno je baciti tekst u embedding model, indeksirati vektore u PostgreSQL, i problem pretrage je riješen. Onda dobiju upit za OIB, broj telefona, ili specifični serijski broj, i vektorska pretraga vrati beskorisne rezultate jer je embedding model napravio vektorski prikaz koji je semantički blizak, ali doslovno netočan. S druge strane, netko tko se kladi isključivo na PostgreSQL full-text search (FTS) dobiva fantastične rezultate za egzaktne termine, ali potpuno promašuje kad korisnik upiše “auto” umjesto “vozilo” ili “bol” umjesto “sindrom”. Ovdje nema ideološkog izbora. Rješenje je hibridna pretraga, i to unutar jedne PostgreSQL tablice, bez složenih external orchestratora.
Kad kažem hibridna pretraga, mislim na konkretnu arhitekturu koja kombinira dvije potpuno različite paradigme indeksiranja unutar istog storage sloja. Prva je klasična FTS koja koristi tsvector i tsquery, s GIN indeksom za brzu pretragu po tokeniziranim riječima. Druga je pgvector koja omogućuje pohranu vektorskih embeddinga i pretragu po kosinusnoj ili euklidskoj sličnosti, s IVFFlat ili HNSW indeksom za aproksimativno najbližih susjeda. Ključna točka koju većina implementacija preskače jest da obje pretrage moraju raditi nad istim fizičkim redovima iste tablice. Ne želimo dvije baze, dvije replike, ili dvije aplikacijske instance koje se dogovaraju preko mreže. Želimo jedan SQL upit koji u jednom planu izvršavanja kombinira obje vrste pretrage.
Tehnička implementacija počinje s tablicom koja ima tri ključne kolone: tekstualni sadržaj, tsvector stupac za FTS koji se puni triggerom ili izvedenim izrazom, i vektorski stupac tipa vector(n) koji sadrži embedding istog sadržaja. To nije nikakva magija. PostgreSQL to podržava od verzije 14 nadalje, a pgvector je stabilna ekstenzija koja ne zahtijeva modificiranje kernel baze. Problem nastaje kad trebaš izračunati hibridni score koji kombinira FTS rank i vektorsku sličnost. Formula je jednostavna: final_score = alpha * ts_rank + (1 - alpha) * (1 - cosine_distance). Alpha je hiperparametar koji definiraš eksperimentalno, ovisno o prirodi podataka i tipu upita.
Konkretni SQL izgleda ovako. Prvo definiraš funkciju koja prima tekst upita, generira tsquery i embedding na aplikacijskoj strani ili unutar baze koristeći pgml ili neki drugi extension, zatim izvršavaš upit koji spaja oba indeksa. SELECT id, content, (alpha * ts_rank(fts_vector, plainto_tsquery('hrvatski', 'neki upit')) + (1 - alpha) * (1 - (embedding <=> query_embedding::vector))) AS score FROM documents WHERE fts_vector @@ plainto_tsquery('hrvatski', 'neki upit') OR (embedding <=> query_embedding::vector) < threshold ORDER BY score DESC LIMIT 20. Ovdje je ključan OR uvjet. Ne želimo da FTS uvjet odsiječe sve dokumente koji nemaju egzaktno podudaranje, ali imaju visoku semantičku sličnost. Isto tako, ne želimo da vektorska pretraga ignorira dokumente koji imaju savršeno podudaranje po FTS-u, ali su vektorski udaljeni.
Reranker fallback je sljedeći sloj koji razlikuje solidnu implementaciju od one koja se raspada kad dobiješ 50 tisuća upita na sat. Nakon što hibridni upit vrati top N rezultata, pokrećeš drugi, skuplji reranker model. To može biti cross-encoder tipa BERT koji prima par (upit, dokument) i vraća precizniji score. Ali ovaj reranker se ne pokreće nad cijelim korpusom od milijun dokumenata. Pokrećeš ga samo nad top 50 ili top 100 rezultata iz hibridne faze. To je ono što arhitekturu čini skalabilnom. Prva faza je jeftina, koristi indekse i aproksimaciju. Druga faza je skupa, ali se izvršava nad malim setom podataka. U PostgreSQL to implementiraš tako što hibridni upit staviš u CTE, a zatim nad njim pozoveš funkciju za reranking koja može biti napisana u PL/pgSQL ili Pythonu kroz PL/Python extension.
Zašto je ovo kritično za produkcijske sustave? Zato što stvarni korisnici ne upisuju upite onako kako ih embedding model očekuje. Oni upisuju OIB-e, matične brojeve, JMBG-e, brojeve polica, reference ugovora. FTS to rješava savršeno jer egzaktno tokenizira i uspoređuje nizove znakova. Vektorska pretraga će za OIB “12345678901” vratiti dokumente koji su semantički povezani s brojem 12345678901, a to je ništa, jer nema semantičke veze. S druge strane, kad netko upiše “problem s plaćanjem”, FTS će tražiti riječ “plaćanje” i promašiti dokumente koji govore o “transakciji” ili “naplati”. Vektorska pretraga će to uhvatiti. Hibridni pristup s alpha parametrom omogućuje da se oba scenarija pokriju bez da žrtvuješ jedno za drugo.
Ovaj pristup razvijamo na rinet.one — za detalje slobodno se javi.
Problem s alpha parametrom je što nije statičan. U praksi, alpha od 0.7 (veća težina FTS-u) radi dobro za korporativne dokumente s puno referenci i brojeva. Alpha od 0.3 (veća težina vektoru) radi bolje za slobodni tekst, opise proizvoda, ili korisničke komentare. Rješenje je dinamički alpha koji ovisi o tipu upita. Ako upit sadrži brojčane nizove ili poznate formate (OIB, IBAN), postaviš alpha na 0.9. Ako je upit čisti prirodni jezik, alpha pada na 0.3. To se lako implementira u PostgreSQL funkciji koja parsira ulazni upit i odlučuje o alpha vrijednosti prije izvršavanja hibridnog upita.
Performanse su uvijek pitanje. GIN indeks za FTS je brz, ali HNSW indeks za vektore zahtijeva memoriju i vrijeme izgradnje. Kod hibridnog upita s OR uvjetom, PostgreSQL optimizer može odlučiti da skenira oba indeksa i spoji rezultate, što je prihvatljivo za većinu use caseva. Ako imaš milijune redova, moraš paziti na plan izvršavanja. Preporuka je koristiti parcijalne indekse za FTS na stupcima koji su često pretraživani, i HNSW indeks s ef_search parametrom koji ograničava broj kandidata iz vektorske pretrage. U produkciji smo vidjeli latencije ispod 100ms za hibridne upite s 500 tisuća dokumenata, uz reranker koji dodaje još 200ms. To je prihvatljivo za web aplikacije i chat botove.
Jedna od skrivenih prednosti ovog pristupa jest da ne zahtijeva dodatne servise. Nema potrebe za Elasticsearchom, Milvusom, ili Qdrantom. Sve je u PostgreSQL. To znači jedan security model, jedna replikacija, jedan backup, jedan monitoring. Za tim koji gradi sovereign AI sustav, to je ključno. Ne želimo ovisiti o vanjskim vektorskim bazama koje zahtijevaju zasebne klastere, posebne operacije i dodatne certifikate. PostgreSQL s pgvector i FTS daje ti sve što trebaš za hibridnu pretragu unutar jedne instance, uz mogućnost horizontalnog skaliranja kroz Citus ili Patroni.
Testiranje hibridne pretrage nije trivijalno. Ne možeš samo pokrenuti jedan upit i reći da radi. Moraš definirati relevancy metrike poput MRR (Mean Reciprocal Rank) i NDCG (Normalized Discounted Cumulative Gain) za obje komponente zasebno i za hibridni rezultat. U praksi smo vidjeli da hibridni pristup podiže MRR za 15 do 25 posto u usporedbi s čistu vektorsku pretragu, i za 30 do 40 posto u usporedbi s čisti FTS, ovisno o domeni. To nisu teoretske brojke. To su rezultati iz A/B testova na stvarnim korisničkim upitima, gdje su ljudi ocjenjivali relevantnost prvog rezultata.
Implementacija reranker fallbacka unosi sljedeći sloj kompleksnosti. Cross-encoder modeli su skupi za pokretanje, ali ih možeš optimizirati kroz ONNX runtime ili kroz kvantizaciju. U PostgreSQL to integriraš kroz funkciju koja prima top N rezultata iz hibridne faze, šalje ih modelu (koji može biti pokrenut kao mikroservis ili kroz PL/Python), i vraća rerankane rezultate. Ključno je da reranker ne blokira glavni upit. Koristiš asinkroni poziv ili batch processing, tako da se korisniku prvo vrate brzi hibridni rezultati, a zatim se rerankani rezultati dostave kroz streaming ili callback. To je arhitektura koju koristimo u RiNET Engineeringu za sustave koji zahtijevaju i brzinu i preciznost.
Na kraju, ono što želim reći jest da ne postoji magični metak za pretragu. Embedding modeli nisu zamjena za full-text search, i puni tekst nije zamjena za semantičko razumijevanje. Hibridna pretraga u PostgreSQL, s FTS i pgvector u istoj tablici, nije kompromis. To je jedina arhitektura koja priznaje da su stvarni podaci ružni, neuredni i da zahtijevaju više od jedne vrste indeksa. Ako gradiš sovereign AI sustav koji mora raditi sa stvarnim ljudima i stvarnim podacima, ne treba ti još jedna baza. Treba ti jedna tablica, dvije vrste indeksa, i dovoljno hrabrosti da priznaš da ni jedan model nije savršen.
Ovo gradimo svaki dan na RiNET-u.
Ako tvoja institucija, banka, osiguranje ili infrastrukturna firma rješava taj tip problema — zakaži razgovor. Pokazat ćemo ti arhitekturu, ne marketing.
Zanima vas ovakva AI tehnologija?
RiNET gradi sovereign AI rjesenja za javni sektor i poduzeca - od civic intelligence do automatizacije nabave.
Kontaktirajte nas →
Komentari