Mundarija (36)
- Bu darsda
- 1. Nega bu kerak?
- 2. tsvector va tsquery
- 2.1 Matn → so'zlar ro'yxati
- 2.2 So'rov va @@
- 3. O'zbek tili: apostrof va kirill
- 3.1 Muammo: apostrof so'zni bo'ladi
- 3.2 Yechim: uz_normal funksiyasi
- 3.3 Cheklovlar — ochiq aytamiz
- 3.4 IMMUTABLE va'dasi
- 4. So'rov turlari va foydalanuvchi matni
- 4.1 Hujumchi nigohi: foydalanuvchi matni to_tsquery da
- 5. Qidiruv ustuni, GIN va saralash
- 5.1 Generated tsvector ustun
- 5.2 Qidiruv va saralash
- 5.3 Vazn bilan saralash
- 5.4 Prefiks, natijani belgilash
- 6. pg_trgm: xatoli yozuv va so'z bo'lagi
- 7. Indekslar ishlayaptimi va qancha tez
- 7.1 EXPLAIN: ifoda aynan bir xil bo'lsin
- 7.2 Kanon o'lchovi: 500 000 vazifa
- 8. Postgres FTS yoki alohida qidiruv tizimi?
- 9. Ko'p uchraydigan xatolar
- 9.1 Foydalanuvchi matnini to_tsquery ga berish
- 9.2 Matn va so'rovni turli qoidada normallashtirish
- 9.3 Konfiguratsiyasiz to_tsvector(matn)
- 9.4 Ifoda indeksi bor, so'rovda boshqa ifoda
- 9.5 unaccent lug'ati yangilangach indeksni unutish
- 10. Mashqlar
- 1-mashq (oson): Ikki konfiguratsiya
- 2-mashq (o'rta): Menyu qidiruvi
- 3-mashq (qiyin): Xatoga chidamli qidiruv
- 11. Loyiha qadami: qidiruv migratsiyasi (holat 07)
- 12. Real ishda
- Xulosa
- Manbalar
PostgreSQL full-text search: tsvector, websearch va o'zbekcha qidiruv
Qisqacha: Full-text search matnni so'zlar ro'yxatiga (
tsvector) aylantirib, so'rovni (tsquery) shu ro'yxat bilan solishtiradi —ILIKE '%…%'dan farqli ravishda indeksdan foydalanadi va natijanits_rankbilan saralaydi. O'zbek tili uchun Postgres'da lug'at yo'q, shuning uchun'simple'konfiguratsiya va o'zimizning normallashtirish (o'→oʻ, kirill → lotin) kerak. Kanonda 500 000 vazifada:ILIKE157 ms, trigram indeks 12 ms, full-text 1.8 ms.
Bu darsda
tsvector,tsqueryva@@operatori qanday ishlashini tushuntirasiz.to_tsquery,plainto_tsquery,websearch_to_tsqueryfarqini va qaysi biri foydalanuvchi matni uchun xavfsiz ekanini bilasiz.- O'zbekcha matndagi apostrof va kirill muammosini ko'rib, normallashtirish funksiyasini o'qiysiz.
- Generated
tsvectorustun, GIN indeks vats_rankbilan tez, saralangan qidiruv qurasiz. pg_trgmbilan xatoli yozilgan so'zni topasiz.
Oldin bilishingiz kerak: Indeks turlari: GIN, GiST, BRIN va pg_trgm, Ilg'or constraint'lar va generated ustunlar, Funksiyalar va PL/pgSQL.
1. Nega bu kerak?
vazifalar-api da ?q= qidiruvi bor. Hozir u shunday ishlaydi: matn ichida qidirilgan bo'lak bormi (strpos(lower(matn), …)). Bu kichik ro'yxatda yetadi. Lekin uch muammo bor:
- Sekin.
'%so'z%'ko'rinishidagi qidiruv B-tree indeksni ishlata olmaydi — har qator o'qiladi. - Tartib yo'q. Natijalar "qanchalik mos" bo'yicha saralanmaydi.
- O'zbek tili. Foydalanuvchi
o'qish,oʻqish,o‘qishyoki kirillchaўқишdeb yozishi mumkin — to'rt xil yozuv, bitta so'z.
Full-text search (to'liq matnli qidiruv) — matnni oldindan so'zlarga ajratib, so'z ro'yxati bo'yicha qidirish. O'xshatish: kitob oxiridagi ko'rsatkich (indeks sahifasi). "Bozor — 12, 45, 101-betlar". Butun kitobni varaqlamaysiz — ko'rsatkichdan so'zni topib, kerakli betga o'tasiz.
Bu darsdagi so'rovlarni biz ishga tushirdik: PostgreSQL 17.6, kanon holat 07 sxemasi. Kanon (IlmHamroh, 2026-10, PostgreSQL 18.6) bilan chiqish bir xil; farqli joy alohida aytilgan.
2. tsvector va tsquery
2.1 Matn → so'zlar ro'yxati
to_tsvector matnni leksemalar (lexeme — so'zning qidiruv uchun normallashgan shakli) ro'yxatiga aylantiradi. Birinchi argument — konfiguratsiya: qaysi til qoidalari bilan.
SELECT to_tsvector('english', 'Running books are running') AS en,
to_tsvector('simple', 'Running books are running') AS oddiy; en | oddiy
--------------------+---------------------------------
'book':2 'run':1,4 | 'are':3 'books':2 'running':1,4
(1 row)Ikki ustunni solishtiring:
english:running→run,books→book— bu stemming (so'zni o'zagiga keltirish).areyo'qoldi — u stop-so'z (har matnda uchraydigan, qidiruvga foydasiz so'z).simple: faqat kichik harfga o'tkazdi. So'z o'zgarmadi, stop-so'z yo'q.
Raqamlar (:1,4) — so'zning matndagi o'rni. Ular ibora qidiruvi va saralash uchun kerak.
Rus tili uchun ham lug'at bor:
SELECT to_tsvector('russian', 'Книги о кошках') AS ru; ru
-------------------
'книг':1 'кошк':3
(1 row)O'zbek tili uchun esa Postgres'da konfiguratsiya yo'q. english ni o'zbekcha matnga qo'llasak — foyda yo'q, faqat zarar:
SELECT to_tsvector('english', 'kitoblar o''qiyman') AS en_uz; en_uz
-------------------------------
'kitoblar':1 'o':2 'qiyman':3
(1 row)kitoblar o'zgarmadi (ingliz qoidasi o'zbek qo'shimchasini bilmaydi), o'qiyman esa ikkiga bo'lindi. Shuning uchun o'zbekcha matn uchun — simple va o'zimizning normallashtirish.
2.2 So'rov va @@
tsquery — qidiruv sharti: so'zlar va mantiqiy amallar (& va, | yoki, ! emas). @@ — "mos keladimi?" operatori. Muhim farq LIKE dan:
SELECT 'bozor' LIKE '%bo%' AS like_, to_tsvector('simple', 'bozor')
@@ to_tsquery('simple', 'bo') AS fts; like_ | fts
-------+-----
t | f
(1 row)LIKE harflar bo'lagini qidiradi, full-text esa butun so'zni. bo — boshqa so'z. Bo'lak bo'yicha qidiruv kerak bo'lsa — prefiks (bo:*) yoki trigram (pastda).
Tekshirib ko'ring: Nega
englishkonfiguratsiyasidaareyo'qoldi,simpleda esa qoldi? O'zbekcha matndava,bilanso'zlari bilan nima bo'ladi?
Javob
english ning stop-so'zlar ro'yxati bor (are, the, a …) — ular tashlanadi. simple da ro'yxat yo'q, shuning uchun o'zbekcha va, bilan ham so'z sifatida qoladi. Bu hajmni biroz oshiradi va "va" deb qidirsa hamma narsa topiladi — lekin o'zbek tili uchun tayyor ro'yxat yo'q; kerak bo'lsa o'z lug'atingizni yaratish mumkin (hujjatdagi "Simple Dictionary" + StopWords).
3. O'zbek tili: apostrof va kirill
3.1 Muammo: apostrof so'zni bo'ladi
Kanonning asosiy topilmasi. simple ajratuvchisi (parser) oddiy apostrofni ' va egri ’, ‘ ni so'z ajratuvchi deb biladi:
SELECT to_tsvector('simple',
'Kitob o''qish, G‘ayrat bilan ko’rishish') AS xom; xom
-----------------------------------------------------------------------
'ayrat':5 'bilan':6 'g':4 'kitob':1 'ko':7 'o':2 'qish':3 'rishish':8
(1 row)o'qish → o va qish; G‘ayrat → g va ayrat. "qish" deb qidirsangiz — "o'qish" topiladi; "o'qish" deb qidirsangiz — ikki so'zli shartga aylanadi. Qaysi belgi harf hisoblanadi? ts_debug har bo'lakni ko'rsatadi:
SELECT alias, token
FROM ts_debug('simple', 'o''qish oʻqish o’qish')
WHERE alias <> 'blank'; alias | token
-----------+--------
asciiword | o
asciiword | qish
word | oʻqish
asciiword | o
asciiword | qish
(5 rows)O'rtadagi oʻqish — bitta so'z. Unda U+02BB belgisi (ʻ, "modifier letter turned comma") — Unicode'da u harf hisoblanadi. O'zbek lotin alifbosining rasmiy belgisi aynan shu. Demak yechim: matndagi hamma apostrof shakllarini ʻ ga keltirish.
3.2 Yechim: uz_normal funksiyasi
Kanonning 104-qidiruv.sql migratsiyasidagi funksiya (to'liq matni «Loyiha qadami» bo'limida) uch ish qiladi, aynan shu tartibda:
- kirill → lotin (
ў→oʻ,ш→sh,ғ→gʻ…); unaccent— urg'u belgilarini olib tashlash (é→e), Extension'lar darsidagi kengaytma;- oxirida hamma apostrof shakllari (
',’,‘,`,ʼ,´) →ʻ.
Natija:
SELECT x AS asl, uz_normal(x) AS normal,
to_tsvector('simple', uz_normal(x)) AS tsv
FROM (VALUES ('Kitob o''qish, G‘ayrat bilan ko’rishish'),
('Ўқиш: китоб, шаҳар Тошкент, Ғайрат, маъно'),
('Café oʻqish a`lo')) v(x); asl | normal | tsv
-------------------------------------------+------------------------------------------------+--------------------------------------------------------------------
Kitob o'qish, G‘ayrat bilan ko’rishish | kitob oʻqish, gʻayrat bilan koʻrishish | 'bilan':4 'gʻayrat':3 'kitob':1 'koʻrishish':5 'oʻqish':2
Ўқиш: китоб, шаҳар Тошкент, Ғайрат, маъно | oʻqish: kitob, shahar toshkent, gʻayrat, maʻno | 'gʻayrat':5 'kitob':2 'maʻno':6 'oʻqish':1 'shahar':3 'toshkent':4
Café oʻqish a`lo | cafe oʻqish aʻlo | 'aʻlo':3 'cafe':1 'oʻqish':2
(3 rows)Uch xil yozuv — bir xil leksemalar. Endi lotin va kirill matn bir-birini topadi.
Nega tartib muhim? Kanon buni o'z xatosidan o'rgandi. Birinchi variantda unaccent oxirida turgan edi. unaccent esa ʻ ni oddiy ' ga aylantiradi — va normallashtirish o'z ishini o'zi buzdi: 'o' 'qish' qaytib keldi. Eng yomoni — xato yashirin edi: qidiruv baribir "topardi", chunki so'rov ham xuddi shunday ikki so'zga bo'linardi. Xato fayl bazaga qo'llanishidan oldin ts_debug bilan topilgan va tuzatilgan.
3.3 Cheklovlar — ochiq aytamiz
SELECT uz_normal('Ёзиш'), uz_normal('O‘zbekiston'), uz_normal('ЕР'); uz_normal | uz_normal | uz_normal
-----------+-------------+-----------
yozish | oʻzbekiston | er
(1 row)ер → er, lekin to'g'risi yer. So'z boshidagi е → ye qoidasi translate bilan qilinmaydi (buning uchun so'z chegarasini bilish kerak). Ikkinchi cheklov — stemming yo'q: o'zbekcha qo'shimchalarni (-lar, -ni, -dan) olib tashlaydigan lug'at yo'q. Buni pastda prefiks qidiruvi bilan qisman yechamiz.
3.4 IMMUTABLE va'dasi
Funksiyalar va PL/pgSQL darsida "yolg'on IMMUTABLE — xavfli" degandik. uz_normal aynan shunday ongli istisno:
SELECT proname, provolatile FROM pg_proc
WHERE proname IN ('unaccent', 'uz_normal') ORDER BY proname; proname | provolatile
-----------+-------------
unaccent | s
unaccent | s
uz_normal | i
(3 rows)unaccent — STABLE: uning lug'ati (fayl) o'zgarishi mumkin. Generated ustun va ifoda indeksi esa faqat IMMUTABLE funksiyani qabul qiladi. Kanon unaccent ni lug'at nomini aniq ko'rsatib chaqiradi va o'zini IMMUTABLE deb e'lon qiladi. Narxi migratsiya izohida yozilgan: unaccent lug'ati yangilansa — indeks va ustunni qayta hisoblash (REINDEX) kerak. Yolg'on va'da qachon kechiriladi? Faqat oqibati hujjatlangan va boshqariladigan bo'lsa.
4. So'rov turlari va foydalanuvchi matni
Postgres'da tsquery yasashning to'rt yo'li bor:
SELECT to_tsquery('simple', 'bozor & !non') AS a,
plainto_tsquery('simple', 'bozor non') AS b,
websearch_to_tsquery('simple',
'"kitob qaytarish" -kutubxona OR ingliz') AS c,
phraseto_tsquery('simple', 'kitob qaytarish') AS d; a | b | c | d
------------------+-----------------+---------------------------------------------------+-------------------------
'bozor' & !'non' | 'bozor' & 'non' | 'kitob' <-> 'qaytarish' & !'kutubxona' | 'ingliz' | 'kitob' <-> 'qaytarish'
(1 row)| Funksiya | Kirish | Kimga |
|---|---|---|
to_tsquery |
operatorlar bilan: bozor & !non |
dasturchi yozgan shart |
plainto_tsquery |
oddiy matn, hamma so'z & |
oddiy qidiruv maydoni |
websearch_to_tsquery |
Google uslubi: "ibora", -so'z, OR |
foydalanuvchi qidiruv maydoni |
phraseto_tsquery |
so'zlar ketma-ket (<-> — "keyingi so'z") |
aniq ibora |
4.1 Hujumchi nigohi: foydalanuvchi matni to_tsquery da
Qidiruv maydoniga foydalanuvchi ikki so'z yozdi:
SELECT to_tsquery('simple', 'non olish');ERROR: syntax error in tsquery: "non olish"to_tsquery operatorsiz ikki so'zni tushunmaydi. Ilova bu xatoni ushlamasa — har shunday so'rov 500 javob. Hujumchi esa ((( yoki a & | b yuborib, log'ni xatolar bilan to'ldiradi va server xatosi matnida nimadir sizib chiqishini kutadi. websearch_to_tsquery esa hech qachon sintaksis xatosi bermaydi — tushunmaganini tashlab yuboradi:
SELECT to_tsquery('simple', 'osh:*') AS a,
websearch_to_tsquery('simple', 'osh & ') AS b,
websearch_to_tsquery('simple', '(((') AS c;NOTICE: text-search query contains only stop words or doesn't contain lexemes, ignored
a | b | c
---------+-------+---
'osh':* | 'osh' |
(1 row)((( — bo'sh so'rov va ogohlantirish, xato emas. Qoida: foydalanuvchi matni — faqat websearch_to_tsquery (yoki plainto_), to_tsquery — faqat siz yig'gan satr uchun. Va albatta parametr bilan ($1), satr ulab emas (Parametrli so'rovlar va SQL injection).
Ikkinchi tuzoq — LIKE ga foydalanuvchi matni. % va _ — LIKE ning maxsus belgilari: foydalanuvchi % yozsa, hamma qator topiladi. Kanon shu qismdagi ORM tajribasida aynan buni topdi: Prisma (Prisma: filtrlash, pagination va agregatlar darsida o'rganiladigan ORM) contains filtrida % va _ ni ekranlamaydi. Full-text'da bu muammo yo'q — % u yerda shunchaki belgi.
Tekshirib ko'ring: Foydalanuvchi
-bozordeb yozdi.websearch_to_tsqueryqanday shart yasaydi va nima topiladi?
Javob
!'bozor' — "bozor so'zi YO'Q". Bunday shart deyarli hamma qatorga mos keladi va GIN indeks undan foyda ko'rmaydi (inkorni indeksdan qidirib bo'lmaydi). Amalda ilovalar faqat inkordan iborat so'rovni rad etadi yoki uni boshqa shart bilan birga talab qiladi.
5. Qidiruv ustuni, GIN va saralash
5.1 Generated tsvector ustun
Har so'rovda to_tsvector(…) ni hisoblash — sekin (pastda o'lchov bor). Shuning uchun natija saqlanadi — Ilg'or constraint'lar va generated ustunlar darsidagi STORED generated ustun bilan. Kanon 104 dan:
ALTER TABLE vazifalar ADD COLUMN qidiruv tsvector
GENERATED ALWAYS AS
(to_tsvector('simple'::regconfig, uz_normal(matn))) STORED;
CREATE INDEX vazifalar_qidiruv_idx ON vazifalar USING gin (qidiruv);'simple'::regconfig — konfiguratsiya nomi aniq turda. Bu ham IMMUTABLE talabidan: to_tsvector(matn) (konfiguratsiyasiz) sukut sozlamaga (default_text_search_config) bog'liq — generated ustunda ishlamaydi. GIN indeks (Generalized Inverted Index) — aynan "so'z → qatorlar ro'yxati" ko'rsatkichi (Indeks turlari darsida JSONB va massiv uchun ko'rgansiz).
Matn o'zgarsa — ustun o'zi yangilanadi, trigger kerak emas:
BEGIN;
UPDATE vazifalar SET matn = 'Ўқитувчига хат ёзиш' WHERE id = 1
RETURNING matn, qidiruv;
ROLLBACK;BEGIN
matn | qidiruv
---------------------+-------------------------------------
Ўқитувчига хат ёзиш | 'oʻqituvchiga':1 'xat':2 'yozish':3
(1 row)
UPDATE 1
ROLLBACK5.2 Qidiruv va saralash
So'rov ham xuddi shu uz_normal dan o'tadi — matn va so'rov bir xil qoidada bo'lishi shart:
SELECT id, matn, ts_rank(qidiruv, q) AS rank
FROM vazifalar,
websearch_to_tsquery('simple', uz_normal('o''qish')) q
WHERE qidiruv @@ q ORDER BY rank DESC, id; id | matn | rank
----+--------------------------------------------------+------------
1 | JavaScript kursining 26-qismini boshlash #o'qish | 0.06079271
2 | Postgres'ni Docker'da ko'tarish #o'qish #docker | 0.06079271
5 | Ingliz tili: 20 ta yangi so'z #o'qish #ingliz | 0.06079271
7 | Kurs ishi: ma'lumotlar bazasi bo'limi #o'qish | 0.06079271
13 | Kutubxonaga kitob qaytarish #o'qish | 0.06079271
25 | Algoritmlar: 5 ta masala #o'qish | 0.06079271
31 | Python laboratoriya ishi #o'qish | 0.06079271
34 | SQL JOIN mashqlarini yechish #o'qish | 0.06079271
(8 rows)8 ta vazifa. #o'qish tegidagi # tashlandi — teg ham so'z sifatida topildi. Endi xuddi shu so'rov kirillcha:
SELECT id, matn FROM vazifalar
WHERE qidiruv @@ websearch_to_tsquery('simple', uz_normal('ўқиш'))
ORDER BY id; id | matn
----+--------------------------------------------------
1 | JavaScript kursining 26-qismini boshlash #o'qish
2 | Postgres'ni Docker'da ko'tarish #o'qish #docker
5 | Ingliz tili: 20 ta yangi so'z #o'qish #ingliz
7 | Kurs ishi: ma'lumotlar bazasi bo'limi #o'qish
13 | Kutubxonaga kitob qaytarish #o'qish
25 | Algoritmlar: 5 ta masala #o'qish
31 | Python laboratoriya ishi #o'qish
34 | SQL JOIN mashqlarini yechish #o'qish
(8 rows)Aynan o'sha 8 ta. Telefonida kirill klaviatura qolib ketgan foydalanuvchi ham natija oladi.
5.3 Vazn bilan saralash
Yuqorida hamma rank bir xil — har matnda so'z bir marta. Real ilovada sarlavhadagi so'z izohdagidan muhimroq. setweight har qismga vazn beradi: A (eng muhim) … D:
SELECT nom, round(ts_rank(
setweight(to_tsvector('simple', nom), 'A')
|| setweight(to_tsvector('simple', tavsif), 'D'),
to_tsquery('simple', 'osh'))::numeric, 4) AS rank
FROM (VALUES ('Osh', 'guruch va sabzi'),
('Palov', 'osh uchun guruch')) AS t(nom, tavsif)
ORDER BY rank DESC; nom | rank
-------+--------
Osh | 0.6079
Palov | 0.0608
(2 rows)Nomida "osh" bor taom 10 barobar yuqori. || — ikki tsvector ni birlashtiradi.
5.4 Prefiks, natijani belgilash
Yozish davomida qidirish ("bozo…") — prefiks :*:
SELECT id, matn FROM vazifalar
WHERE qidiruv @@ to_tsquery('simple', 'bozo:*') ORDER BY id; id | matn
----+------------------------------------------
8 | Bozordan kartoshka va piyoz olish #bozor
16 | Bozordan meva olish #bozor
22 | Un va yog' xarid qilish #bozor #oshxona
48 | Bozordan kartoshka va piyoz olish #bozor
(4 rows)Prefiks stemming yo'qligini qisman to'ldiradi:
SELECT count(*) FILTER (WHERE qidiruv @@
to_tsquery('simple', 'kitoblar')) AS kitoblar,
count(*) FILTER (WHERE qidiruv @@
to_tsquery('simple', 'kitob')) AS kitob,
count(*) FILTER (WHERE qidiruv @@
to_tsquery('simple', 'kitob:*')) AS kitob_prefiks
FROM vazifalar; kitoblar | kitob | kitob_prefiks
----------+-------+---------------
0 | 1 | 1
(1 row)"kitoblar" deb qidirgan foydalanuvchi "kitob" ni topmaydi; ilova oxirgi so'zga :* qo'shsa — "kitob" bilan boshlangan hamma so'z. Teskari holat ("kitob" deb qidirib "kitoblar" ni topish) prefiks bilan ishlaydi.
ts_headline — natijada topilgan so'zni belgilaydi (saytda <mark> qilish uchun):
SELECT ts_headline('simple', matn,
to_tsquery('simple', 'bozordan'),
'StartSel=[, StopSel=]')
FROM vazifalar
WHERE qidiruv @@ to_tsquery('simple', 'bozordan') ORDER BY id; ts_headline
--------------------------------------------
[Bozordan] kartoshka va piyoz olish #bozor
[Bozordan] meva olish #bozor
[Bozordan] kartoshka va piyoz olish #bozor
(3 rows)ts_headline asl matn bilan ishlaydi (indekssiz) — uni faqat sahifadagi 20 ta natija uchun chaqiring, hamma qator uchun emas. Natijani HTML'ga qo'yishdan oldin matnni ekranlang (XSS va DOM xavfsizligi darsidagi qoida — [ ] o'rniga <mark> qo'ysangiz, matndagi <script> ham birga keladi).
Tekshirib ko'ring: Nega
ORDER BY rank DESC, iddaidham bor?
Javob
Rank'lar teng bo'lishi mumkin (yuqorida 8 tasi bir xil). Teng qatorlar tartibi kafolatlanmagan — sahifalashda bir qator ikki sahifada chiqishi yoki tushib qolishi mumkin. id tartibni barqaror qiladi (Keyset pagination va katta OFFSET darsidagi qoida).
6. pg_trgm: xatoli yozuv va so'z bo'lagi
Full-text butun so'zni qidiradi. Foydalanuvchi "kartoska" deb xato yozsa — hech narsa topmaydi. Bu yerda trigramlar yordam beradi: matn uch harfli bo'laklarga ajratiladi va o'xshashlik o'lchanadi (Indeks turlari: GIN, GiST, BRIN va pg_trgm darsida ko'rgansiz):
SELECT show_trgm('non'); show_trgm
-------------------------
{" n"," no",non,"on "}
(1 row)word_similarity — so'rov matndagi eng o'xshash so'zga qanchalik yaqin (0…1). <% — "o'xshashlik chegaradan (sukut 0.6) yuqori":
SELECT id, matn, round(word_similarity(uz_normal('kartoska'),
uz_normal(matn))::numeric, 2) AS xato_bilan
FROM vazifalar WHERE uz_normal('kartoska') <% uz_normal(matn)
ORDER BY 3 DESC, id; id | matn | xato_bilan
----+------------------------------------------+------------
8 | Bozordan kartoshka va piyoz olish #bozor | 0.67
48 | Bozordan kartoshka va piyoz olish #bozor | 0.67
(2 rows)Bitta harf tushib qolgan — baribir topildi (to'g'ri yozilganda 1.00). Trigram indeks so'z bo'lagini (LIKE '%…%') ham tezlashtiradi:
SELECT id, matn FROM vazifalar
WHERE uz_normal(matn) LIKE '%' || uz_normal('to''lov') || '%'
ORDER BY id; id | matn
----+----------------------------------------------
18 | Yotoqxona to'lovi #to'lov
19 | Universitet kontrakti #to'lov
28 | Elektr to'lovini Click orqali qilish #to'lov
29 | Gaz to'lovi #to'lov
32 | Soliq hisobotini topshirish #ish #to'lov
(5 rows)to'lovi, to'lovini — full-text bularni to'lov deb bilmaydi (stemming yo'q), bo'lak qidiruvi esa topdi. Amalda ikkalasi birga ishlatiladi: avval full-text, natija bo'lmasa — trigram ("Balki … ni nazarda tutdingiz?").
7. Indekslar ishlayaptimi va qancha tez
7.1 EXPLAIN: ifoda aynan bir xil bo'lsin
Kichik jadvalda planner Seq Scan tanlaydi, shuning uchun uni vaqtincha o'chiramiz:
SET enable_seqscan = off;
EXPLAIN (COSTS OFF) SELECT id FROM vazifalar
WHERE qidiruv @@ websearch_to_tsquery('simple', 'bozor');
EXPLAIN (COSTS OFF) SELECT id FROM vazifalar
WHERE uz_normal(matn) LIKE '%lov%';
EXPLAIN (COSTS OFF) SELECT id FROM vazifalar
WHERE matn ILIKE '%lov%';SET
QUERY PLAN
-------------------------------------------------------
Bitmap Heap Scan on vazifalar
Recheck Cond: (qidiruv @@ '''bozor'''::tsquery)
-> Bitmap Index Scan on vazifalar_qidiruv_idx
Index Cond: (qidiruv @@ '''bozor'''::tsquery)
(4 rows)
QUERY PLAN
--------------------------------------------------------
Bitmap Heap Scan on vazifalar
Recheck Cond: (uz_normal(matn) ~~ '%lov%'::text)
-> Bitmap Index Scan on vazifalar_matn_trgm_idx
Index Cond: (uz_normal(matn) ~~ '%lov%'::text)
(4 rows)
QUERY PLAN
------------------------------------
Seq Scan on vazifalar
Filter: (matn ~~* '%lov%'::text)
(2 rows)Uchinchi so'rovga qarang: trigram indeks uz_normal(matn) ifodasi ustiga qurilgan. matn ILIKE — boshqa ifoda, indeks ishlamaydi, Seq Scan taqiqlangan bo'lsa ham. Ifoda indeksi faqat so'rovda aynan o'sha ifoda yozilganda ishlaydi (Partial, expression va covering indekslar). PostgreSQL 18 bu holda rejaga Disabled: true qatorini ham qo'shadi (kanon chiqishida bor) — "taqiqlangan usulni majburan tanladim" degani.
7.2 Kanon o'lchovi: 500 000 vazifa
Kanon katta to'plamda (500 000 vazifa, issiq kesh, PostgreSQL 18.6) bir xil qidiruvni turli usulda o'lchagan:
- to_tsvector har so'rovdagenerated ustunsiz2 284 ms
- ILIKE indekssizParallel Seq Scan157 ms
- trigram indeks12 ms
- full-text GIN1,8 ms
Manba: IlmHamroh kanon o'lchovi, 2026-10-24: PostgreSQL 18.6 (Docker), 500 000 vazifa, issiq kesh
Nimaga qarash kerak:
- Full-text + GIN — 1.8 ms,
ILIKEdan 87 barobar tez. - Generated ustunsiz
to_tsvector(…) @@ …— 2 284 ms: har so'rovda 500 000 matnni qayta tahlil qiladi. Saqlangan ustun shuning uchun. - Ko'p natijali so'z boshqacha: "kitob o'qish" 49 499 qatorga mos keldi va top-20
ts_rankbilan 69–83 ms oldi —ts_rankhamma mos qatorni baholaydi, indeks saralashga yordam bermaydi. - Xatoli so'rov (
<%trigram bilan) — 22 ms.
Narxi ham bor: trigram indeksi 39 MB, tsvector GIN 33 MB (jadvalning o'zi 115 MB). 10 000 ta INSERT ikkala GIN bilan 1 376 ms, ularsiz 1 073 ms — yozish 28 % sekin.
8. Postgres FTS yoki alohida qidiruv tizimi?
Elasticsearch, Meilisearch, Typesense — alohida qidiruv dvigatellari. Ular haqida kursda alohida dars bor (Qidiruv tizimlari qanday ishlaydi); hozir bilish shart emas. Qisqacha qaror:
| Postgres FTS yetadi | Alohida dvigatel kerak |
|---|---|
| Millionlab hujjatgacha, oddiy qidiruv | Yuz millionlab hujjat, murakkab relevantlik |
| Ma'lumot bilan bitta tranzaksiyada, sinxronlash yo'q | Facet'lar, sinonimlar, xatoga chidamlilik "qutidan" |
| Qo'shimcha server yo'q | Qidiruvga alohida jamoa va infratuzilma bor |
Ko'p loyihalar Postgres bilan boshlaydi va faqat o'lchov "yetmayapti" desa ko'chadi.
9. Ko'p uchraydigan xatolar
9.1 Foydalanuvchi matnini to_tsquery ga berish
syntax error in tsquery. websearch_to_tsquery.
9.2 Matn va so'rovni turli qoidada normallashtirish
Ustun uz_normal(matn) dan, so'rov esa xom — o'qish hech narsa topmaydi. Ikkala tomonga bir xil funksiya.
9.3 Konfiguratsiyasiz to_tsvector(matn)
Natija server sozlamasiga bog'liq; generated ustun va indeks qabul qilmaydi. to_tsvector('simple', …) — aniq konfiguratsiya.
9.4 Ifoda indeksi bor, so'rovda boshqa ifoda
matn ILIKE — trigram indeks uz_normal(matn) ustida, ishlamaydi. So'rovda indeksdagi ifodaning o'zi.
9.5 unaccent lug'ati yangilangach indeksni unutish
IMMUTABLE va'dasi buzildi — eski qatorlar eski qoidada. Lug'at o'zgarsa REINDEX va ustunni qayta hisoblash (migratsiya izohida yozing).
10. Mashqlar
Mashqlar kurs/mashqlar/27/07-fts/ da. Ular o'z jadvallaringiz bilan, istalgan bo'sh bazada bajariladi; yechimlar tekshirilgan (biz ishga tushirdik, PostgreSQL 17.6).
1-mashq (oson): Ikki konfiguratsiya
'Kitoblar va daftarlar' matni uchun english va simple konfiguratsiyasidagi to_tsvector ni chiqaring. Farq bormi? Nega?
Yechim
SELECT to_tsvector('english', 'Kitoblar va daftarlar');
SELECT to_tsvector('simple', 'Kitoblar va daftarlar');Biz ishga tushirdik (psql -X -q -A -t, PostgreSQL 17.6):
'daftarlar':3 'kitoblar':1 'va':2
'daftarlar':3 'kitoblar':1 'va':2Farq yo'q: ingliz stemmeri o'zbekcha -lar ni bilmaydi, va esa uning stop-so'zlar ro'yxatida yo'q. O'zbekcha matnga english hech narsa bermaydi — simple aniqroq niyatni bildiradi.
2-mashq (o'rta): Menyu qidiruvi
m07_menyu (id int, nom text, tavsif text) jadvalini yarating. Taomlar:
- osh — "guruch, sabzi, go'sht";
- manti — "xamir, go'sht, piyoz";
- lag'mon — "cho'zma xamir, go'sht";
- ko'k choy — "choy bargi".
qidiruv — generated tsvector ustun (nom — A, tavsif — D vazni, apostrofni ʻ ga almashtirish uchun replace(matn, '''', 'ʻ')), GIN indeks. websearch_to_tsquery bilan foydalanuvchining go'sht so'rovini (so'rov ham replace dan o'tsin) bajaring va natijani ts_rank kamayishi, keyin id bo'yicha chiqaring. Ishora: generated ifodada 'simple'::regconfig yozing (aks holda generation expression is not immutable).
Yechim
CREATE TABLE m07_menyu (
id int PRIMARY KEY, nom text, tavsif text,
qidiruv tsvector GENERATED ALWAYS AS (
setweight(to_tsvector('simple'::regconfig,
replace(nom, '''', 'ʻ')), 'A')
|| setweight(to_tsvector('simple'::regconfig,
replace(tavsif, '''', 'ʻ')), 'D')) STORED
);
CREATE INDEX m07_menyu_qidiruv_idx ON m07_menyu USING gin (qidiruv);
INSERT INTO m07_menyu (id, nom, tavsif) VALUES
(1, 'osh', 'guruch, sabzi, go''sht'),
(2, 'manti', 'xamir, go''sht, piyoz'),
(3, 'lag''mon', 'cho''zma xamir, go''sht'),
(4, 'ko''k choy', 'choy bargi');
SELECT nom FROM m07_menyu,
websearch_to_tsquery('simple', replace('go''sht', '''', 'ʻ')) q
WHERE qidiruv @@ q ORDER BY ts_rank(qidiruv, q) DESC, id;
SELECT nom FROM m07_menyu
WHERE qidiruv @@ websearch_to_tsquery('simple', 'choy');
DROP TABLE m07_menyu;Biz ishga tushirdik (psql -X -q -A -t, PostgreSQL 17.6):
osh
manti
lag'mon
ko'k choyBirinchi so'rov uchta taom (hammasida goʻsht bir marta, D vaznida — rank teng, tartib id bo'yicha). Ikkinchi so'rov — "choy" nomida ham (A), tavsifda ham bor bitta taom. replace siz go'sht → go + sht bo'lib, "go" so'zi bor har qatorni topardi.
3-mashq (qiyin): Xatoga chidamli qidiruv
m07_shahar (nom text) ga Toshkent, Samarqand, Buxoro, Xiva, Namangan, Andijon ni qo'shing; pg_trgm kengaytmasini yoqing (CREATE EXTENSION IF NOT EXISTS pg_trgm) va lower(nom) ustiga gin_trgm_ops indeks quring. Foydalanuvchi "samarkand" va "toshkend" deb yozdi. Har biri uchun eng o'xshash shaharni similarity bilan (2 xonali) chiqaring. Ishora: ORDER BY similarity(…) DESC LIMIT 1 va lower.
Yechim
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE TABLE m07_shahar (nom text);
INSERT INTO m07_shahar VALUES ('Toshkent'), ('Samarqand'),
('Buxoro'), ('Xiva'), ('Namangan'), ('Andijon');
CREATE INDEX m07_shahar_trgm_idx
ON m07_shahar USING gin (lower(nom) gin_trgm_ops);
SELECT s.soz || ' -> ' || t.nom || ' '
|| round(similarity(lower(t.nom), s.soz)::numeric, 2)
FROM (VALUES ('samarkand'), ('toshkend')) AS s(soz)
CROSS JOIN LATERAL (
SELECT nom FROM m07_shahar
ORDER BY similarity(lower(nom), s.soz) DESC LIMIT 1) t;
DROP TABLE m07_shahar;Biz ishga tushirdik (psql -X -q -A -t, PostgreSQL 17.6):
samarkand -> Samarqand 0.54
toshkend -> Toshkent 0.64LATERAL — har so'z uchun alohida ichki so'rov (Subquery va LATERAL). Bu yerda chegara emas, "eng yaqini" olindi. Chegara bilan qidirsangiz, % operatorining sukut chegarasi 0.3 — ikkala so'z ham o'tadi. Katta jadvalda ORDER BY lower(nom) <-> s.soz (masofa operatori) GiST indeks bilan tezroq.
11. Loyiha qadami: qidiruv migratsiyasi (holat 07)
Holat 04 → holat 07. Bitta fayl — sql/104-qidiruv.sql. Kanon fayli to'liq; uzun qatorlar telefon uchun bo'lindi, mazmuni aynan (faqat bitta izohdagi ichki o'lchov fayli nomi tushirildi):
-- 104-qidiruv.sql — 27-qism #07 (DB-68, holat 07): vazifalar bo'yicha
-- qidiruv — full-text (tsvector) + pg_trgm (xatoli yozuv).
-- O'zbek tili uchun Postgres'da lug'at (stemming) YO'Q → 'simple'
-- konfiguratsiya (so'zni o'zgartirmaydi, faqat kichik harf).
-- Muammo (o'lchandi): 'simple' parser oddiy
-- apostrofni (') va ’ ni so'z ajratuvchi deb biladi:
-- o'qish → 'o' + 'qish'; ʻ (U+02BB) esa harf → 'oʻqish' bitta so'z.
-- Yechim — normallashtirish: hamma apostrof shakllari → ʻ (U+02BB),
-- kirill → lotin (foydalanuvchi "ўқиш" deb qidirsa ham topilsin),
-- unaccent (é → e). Shu funksiya ham matnga, ham so'rovga
-- qo'llanadi.
CREATE EXTENSION IF NOT EXISTS unaccent;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
-- IMMUTABLE va'dasi: unaccent() o'zi STABLE (lug'at o'zgarishi
-- mumkin) — lug'atni aniq ko'rsatib o'rab IMMUTABLE deymiz
-- (generated ustun va indeks uchun shart). unaccent lug'ati
-- yangilansa — REINDEX/qayta hisoblash kerak (izohda ochiq aytilgan).
CREATE FUNCTION uz_normal(t text) RETURNS text
LANGUAGE sql IMMUTABLE PARALLEL SAFE STRICT
-- tartib muhim (o'lchandi): unaccent ʻ ni ASCII ' ga aylantiradi va
-- ё → е qiladi — shuning uchun 1) kirill → lotin, 2) unaccent,
-- 3) OXIRIDA hamma apostrof shakllari → ʻ
RETURN translate(
public.unaccent('public.unaccent'::regdictionary,
translate(
replace(replace(replace(replace(replace(replace(
replace(replace(replace(replace(replace(replace(
lower(t),
'ў', 'oʻ'), 'ғ', 'gʻ'), 'ш', 'sh'), 'ч', 'ch'),
'ё', 'yo'), 'ю', 'yu'), 'я', 'ya'), 'ц', 'ts'),
'щ', 'sh'), 'қ', 'q'), 'ҳ', 'h'), 'ъ', 'ʻ'),
'абвгдежзийклмнопрстуфхэь',
'abvgdejziyklmnoprstufxe')),
'ʼ''’‘`´', 'ʻʻʻʻʻʻ');
ALTER TABLE vazifalar ADD COLUMN qidiruv tsvector
GENERATED ALWAYS AS
(to_tsvector('simple'::regconfig, uz_normal(matn))) STORED;
CREATE INDEX vazifalar_qidiruv_idx ON vazifalar USING gin (qidiruv);
-- Xatoli yozuv / so'z bo'lagi (ILIKE '%…%', similarity) uchun trigram
CREATE INDEX vazifalar_matn_trgm_idx
ON vazifalar USING gin (uz_normal(matn) gin_trgm_ops);
-- SELECT * view yangi ustunni ko'rmaydi (26 #40 topilmasi)
-- — qayta yaratiladi
CREATE OR REPLACE VIEW faol_vazifalar AS
SELECT * FROM vazifalar WHERE ochirilgan_vaqt IS NULL;
COMMENT ON FUNCTION uz_normal(text) IS
'Qidiruv uchun: kichik harf, kirill→lotin, apostroflar→ʻ, '
'unaccent. IMMUTABLE — unaccent lug''ati o''zgarsa REINDEX';
COMMENT ON COLUMN vazifalar.qidiruv IS
'to_tsvector(simple, uz_normal(matn)) — '
'STORED generated, GIN indeks';Uch tafsilot:
translate(matn, 'абв…', 'abv…')— har harfni mos harfga almashtiradi; ikki harfli moslar (ш→sh) uchun undan oldinreplacezanjiri.'…' '…'(ikki satr orasida yangi qator) — SQL'da yonma-yon satrlar bitta satrga qo'shiladi; uzun izohni bo'lish uchun. Kanonda bu izoh bitta qatorda.faol_vazifalarview qayta yaratiladi —SELECT *view yaratilgan paytdagi ustunlarni eslab qoladi.
Faylni avvalgi migratsiyalar kabi qo'llang va kanon tekshiruvi kabi holatni noldan qurib ko'ring (kanon: namunasiz va namuna bilan — ikkalasi exit 0). Commit:
git add sql/104-qidiruv.sql
M="db: qidiruv — uz_normal (apostrof, kirill),"
M+=" generated tsvector + GIN, pg_trgm (104)"
git commit -m "$M"Halol eslatma: vazifalar-api ning ?q= qidiruvi kanonda hali strpos(lower(matn), …) bilan ishlaydi — bazada qidiruv ustuni tayyor, lekin API'ni unga ulash kanon qadamlariga kirmagan. Ulash uchun so'rov sharti qidiruv @@ websearch_to_tsquery('simple', uz_normal($1)) bo'ladi; buni o'z loyihangizda sinab ko'rishingiz mumkin.
12. Real ishda
- Onlayn do'kon, e'lonlar sayti, bilimlar bazasi — "tez va arzon qidiruv" ko'pincha aynan Postgres FTS + pg_trgm. O'zbek bozoridagi loyihalarda lotin/kirill aralashuvi — kundalik muammo; normallashtirish funksiyasi shu yerda pul tejaydi.
- Supabase va boshqa Postgres platformalari qidiruvni shu
tsvector+ GIN orqali tavsiya qiladi. - AI qidiruv (ma'no bo'yicha) — vektor bazalar va
pgvector; ko'pincha full-text bilan birga ("gibrid qidiruv"). Keyingi darsda kengaytma sifatida tilga olinadi. - Intervyu: "LIKE '%x%' nega sekin va nima bilan almashtirasiz?", "GIN indeks qanday ishlaydi?".
Xulosa
- Full-text: matn →
tsvector(leksemalar), so'rov →tsquery,@@— moslik; butun so'z bo'yicha,LIKEkabi bo'lak bo'yicha emas. - O'zbek tili uchun lug'at yo'q:
simple+uz_normal(kirill → lotin,unaccent, oxirida apostroflar →ʻ); tartib muhim. - Foydalanuvchi matni —
websearch_to_tsquery(sintaksis xatosi bermaydi). - Generated
STOREDustun + GIN: kanonda 1.8 ms; har so'rovda hisoblash — 2 284 ms. ts_rank+setweight— saralash;:*— prefiks;ts_headline— belgilash.pg_trgm— xatoli yozuv va so'z bo'lagi; ifoda indeksi so'rovda aynan o'sha ifoda bilan ishlaydi.
Keyingi dars: Extension'lar — unaccent va pg_trgm kabi kengaytmalar qanday o'rnatiladi, qaysilari bor va managed bazada nimaga ruxsat.
Manbalar
- PostgreSQL 18 hujjati: Full Text Search (Controlling Text Search, Ranking, Headline) — 2026-10-25 da ko'rildi.
- PostgreSQL 18 hujjati: Text Search Functions and Operators (
websearch_to_tsquery). - PostgreSQL 18 hujjati: pg_trgm, unaccent.
- Unicode: U+02BB MODIFIER LETTER TURNED COMMA — o'zbek lotin alifbosidagi
ʻbelgisi.
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!