Full-text Search — GIN index, ranking, highlight
Muammodan boshlaylik
Oldingi darsda FTS ni ishga tushirdik. search_vector ustuni saqlandi, trigger bilan yangilanadi. Lekin jadvalda 500 000 maqola bo'lsa, qidiruv hali ham sekin — chunki index yo'q.
Bundan tashqari, qidiruv natijalarini foydalanuvchiga ko'rsatishda ikkita narsa kerak: qaysi natija eng mos — ranking, va topilgan so'z qayerda turganini ko'rsatish — highlight. Bu darsda shu uchala muammo yechiladi.
GIN index — tsvector uchun
tsvector ustuniga GIN index — FTS qidiruvini Seq Scan dan Index Scan ga o'tkazadi.
CREATE INDEX idx_maqolalar_search
ON maqolalar USING GIN (search_vector);
Index yaratilgandan keyin @@ operatori bilan qidiruv avtomatik indeksdan foydalanadi:
SELECT sarlavha
FROM maqolalar
WHERE search_vector @@ plainto_tsquery('simple', 'funksiya');
-- Bitmap Index Scan on idx_maqolalar_search ✅
Qancha tezlashadi?
Tasavvur qiling: 500 000 maqola, o'rtacha 200 so'zlik matn.
Index yo'q (Seq Scan):
Har qatorning search_vector ni ochib tekshiradi
500 000 tekshiruv → ~800 ms
GIN index bilan:
"funksiya" so'zi qaysi qatorlarda bor — oldindan ma'lum
To'g'ridan-to'g'ri o'sha qatorlarga boradi → ~5 ms
160 barobar tez. Foydalanuvchi qidiruvida bu farq — seziladi.
ts_rank — dolzarblik reytingi
Bir nechta natija topilganda qaysi biri eng mos? ts_rank — so'z qanchalik ko'p va muhim joyda uchraganini hisoblaydi.
SELECT
sarlavha,
ts_rank(search_vector, plainto_tsquery('simple', 'javascript funksiya')) AS rang
FROM maqolalar
WHERE search_vector @@ plainto_tsquery('simple', 'javascript funksiya')
ORDER BY rang DESC;
sarlavha │ rang
-------------------------+-----------
JavaScript Asoslari │ 0.1234567
React Komponentlar │ 0.0759906
Async Dasturlash │ 0.0607927
"JavaScript Asoslari" da ikkala so'z ham bor va ko'p takrorlanadi — rang yuqori.
ts_rank_cd — masofa hisobi
ts_rank_cd so'zlar bir-biriga qanchalik yaqin ekanini ham hisobga oladi. "javascript funksiya" bir jumlada yonma-yon tursa — oddiy ts_rank dan ko'ra yuqori rang:
SELECT
sarlavha,
ts_rank_cd(search_vector, plainto_tsquery('simple', 'javascript funksiya')) AS rang
FROM maqolalar
WHERE search_vector @@ plainto_tsquery('simple', 'javascript funksiya')
ORDER BY rang DESC;
Qachon qaysi?
ts_rank— tez, ko'pchilik holatlar uchun yetarlits_rank_cd— so'zlar yaqinligi muhim bo'lganda (ilmiy matnlar, aniq iboralar)
Normalizatsiya — rang hisoblashni sozlash
ts_rank ikkinchi argument sifatida normalizatsiya parametrini qabul qiladi:
ts_rank(search_vector, query, normalizatsiya)
0 → standart (normalizatsiya yo'q)
1 → matn uzunligiga bo'lish (uzun matnda rang pasayadi)
2 → noyob so'zlar soniga bo'lish
4 → bo'shliq bo'yicha bo'lish
8 → eng yuqori pozitsiya bo'yicha bo'lish
16 → noyob so'zlar/jami so'zlar nisbati
32 → rang yarmini qo'shish
Amalda eng ko'p ishlatiladigan — 1 (uzun maqolalar qisqalarga nisbatan kamroq ustunlikka ega bo'lsin):
ts_rank(search_vector, query, 1)
ts_headline — topilgan joyni ko'rsatish
Foydalanuvchi "javascript funksiya" deb qidirdi. Natijada faqat sarlavha emas — topilgan so'z qaysi kontekstda turganini ko'rsatish kerak. ts_headline — ana shu.
SELECT
sarlavha,
ts_headline(
'simple', -- til
matn, -- asl matn
plainto_tsquery('simple', 'javascript funksiya'), -- qidiruv
'MaxWords=20, MinWords=10, MaxFragments=2, StartSel=**, StopSel=**'
) AS parcha
FROM maqolalar
WHERE search_vector @@ plainto_tsquery('simple', 'javascript funksiya')
ORDER BY ts_rank(search_vector, plainto_tsquery('simple', 'javascript funksiya')) DESC;
sarlavha │ parcha
---------------------+----------------------------------------------------------
JavaScript Asoslari │ **JavaScript** brauzerda ishlaydigan dasturlash tili. **Funksiyalar** va ob'ektlar...
React Komponentlar │ React da **funksional** komponentlar va hooks bilan ishlash...
StartSel va StopSel — topilgan so'z atrofiga qo'yiladigan belgilar. HTML uchun <b> va </b>, markdown uchun **.
ts_headline parametrlari
MaxWords → parchada maksimal so'zlar soni (standart: 35)
MinWords → parchada minimal so'zlar soni (standart: 15)
MaxFragments → nechta alohida parcha ko'rsatish (standart: 0 = bitta uzluksiz)
StartSel → topilgan so'z boshi belgisi
StopSel → topilgan so'z oxiri belgisi
FragmentDelimiter → parchalar orasidagi ajratuvchi (standart: " ... ")
To'liq qidiruv API — amaliy misol
Saytda qidiruv API yozish uchun:
CREATE OR REPLACE FUNCTION qidirish(
so_rov TEXT,
sahifa INT DEFAULT 1,
hajm INT DEFAULT 10
)
RETURNS TABLE (
id INT,
sarlavha TEXT,
parcha TEXT,
rang REAL
) AS $
DECLARE
tsq tsquery := plainto_tsquery('simple', so_rov);
BEGIN
RETURN QUERY
SELECT
m.id,
m.sarlavha,
ts_headline(
'simple', m.matn, tsq,
'MaxWords=25, MinWords=10, MaxFragments=1, StartSel=<mark>, StopSel=</mark>'
),
ts_rank(m.search_vector, tsq)
FROM maqolalar m
WHERE m.search_vector @@ tsq
ORDER BY ts_rank(m.search_vector, tsq) DESC
LIMIT hajm
OFFSET (sahifa - 1) * hajm;
END;
$ LANGUAGE plpgsql;
Ishlatish:
SELECT * FROM qidirish('javascript async', sahifa := 1, hajm := 5);
Sarlavha va matnni alohida og'irlik bilan indekslash
Sarlavhadagi so'z matndagi so'zdan muhimroq. setweight — tsvector ga og'irlik belgilaydi:
-- A > B > C > D — og'irlik darajalari
UPDATE maqolalar
SET search_vector =
setweight(to_tsvector('simple', sarlavha), 'A') || -- sarlavha og'irroq
setweight(to_tsvector('simple', matn), 'B'); -- matn engil
-- Trigger ham yangilanishi kerak:
CREATE FUNCTION maqolalar_search_update() RETURNS trigger AS $
BEGIN
NEW.search_vector :=
setweight(to_tsvector('simple', NEW.sarlavha), 'A') ||
setweight(to_tsvector('simple', NEW.matn), 'B');
RETURN NEW;
END;
$ LANGUAGE plpgsql;
Endi sarlavhada topilgan so'z reytingda matndagidan yuqori — natijalar aniqroq.
Tez-tez uchraydigan xatolar
ts_headline ga search_vector emas, asl matn uzatish:
-- ❌ — search_vector — binary format, uni highlight qilib bo'lmaydi
ts_headline('simple', search_vector, tsq)
-- ✅ — asl matn kerak
ts_headline('simple', matn, tsq)
ts_headline asl matndan parcha kesib oladi — shu sababli tsvector emas, TEXT kerak.
MaxFragments=0 ni "fragment yo'q" deb tushunish:
-- MaxFragments=0 → bitta uzluksiz parcha (standart)
-- MaxFragments=2 → ikkita alohida parcha
'MaxFragments=0' -- ← "nol fragment" emas, "fragmentlash yo'q"
MaxFragments=0 — topilgan joylar atrofida bitta uzluksiz matn. MaxFragments=2 — ikkita alohida parcha, orasida ... bo'ladi.
Xulosa
FTS ni production ga tayyorlash uchta qadamdan iborat:
1. tsvector ustun — oldindan hisoblab saqlash
2. GIN index — tez qidirish uchun
3. Trigger — yangi qatorlarda avtomatik yangilanish
Qidiruv natijalarini ko'rsatish:
ts_rank → dolzarblik reytingi, ORDER BY uchun
ts_rank_cd → so'zlar yaqinligi ham muhim bo'lganda
ts_headline → topilgan joyni belgilab parcha ko'rsatish
setweight → sarlavha vs matn og'irligini belgilash
Keyingi dars
27-dars: UUID — gen_random_uuid, uuid-ossp extension