Full-text Search — GIN index, ranking, highlight

~20 daq · PostgreSQL · PostgreSQL Maxsus Imkoniyatlari

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 yetarli
  • ts_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