Mundarija (26)
- Bu darsda
- 1. Nega bu kerak?
- 2. Ishga tushirish va asosiy tushunchalar
- 2.1 Lokal server
- 2.2 Indeks, hujjat, task
- 3. Indeks yaratish va sozlash
- 4. Qidiruv: typo tolerance, filtr, facet
- 4.1 Oddiy qidiruv
- 4.2 Filtr va facet
- 4.3 Filtrni xavfsiz yig'ish
- 5. Tezlik: kanon nimani o'lchadi
- 6. API kalitlar va tenant token
- 6.1 Kalit turlari
- 6.2 Tenant token
- 7. Bazadan indeksga: sinxronlash
- 8. Typesense — hujjat bo'yicha
- 9. Hujumchi nigohi
- 10. Ko'p uchraydigan xatolar
- 11. Mashqlar
- 1-mashq (oson): bo'shliqlarni to'ldiring
- 2-mashq (o'rta): sana oralig'i bilan filtr
- 3-mashq (qiyin): outbox voqealarini relay to'plamiga aylantirish
- 12. Loyiha qadami: kanon tajribasi, loyihaga kirmaydi
- 13. Real ishda
- Xulosa
- Manbalar
Meilisearch va Typesense: tayyor qidiruv dvigateli Node.js'dan
Qisqacha: Meilisearch — o'rnatish oson, xato yozilgan so'zni ham topadigan ochiq kodli qidiruv dvigateli. Hujjatlarni indeksga yuklaysiz, qaysi maydonlarda qidirish va qaysilari bo'yicha filtrlash mumkinligini sozlaysiz, keyin
index.search()bir necha millisekundda saralangan natija va facet sanoqlarini qaytaradi. Brauzerga master kalit emas, filtri imzo ichiga yozilgan tenant token beriladi. Typesense — o'xshash, ma'lumotni xotirada saqlaydigan muqobil.
Bu darsda
- Meilisearch'ga hujjat yuklab,
searchableAttributes,filterableAttributesvasortableAttributesni sozlaysiz. - Typo tolerance, filtr va facet bilan qidiruv yasaysiz va kanon chiqishini o'qiysiz.
- API kalitlar turlarini va tenant token qanday ishlashini tushuntirasiz.
- Bazadagi o'zgarishni indeksga outbox orqali yetkazish naqshini bilasiz.
- Meilisearch, Typesense va Postgres FTS orasida tanlay olasiz.
Oldin bilishingiz kerak: Qidiruv tizimlari qanday ishlaydi, Transactional outbox naqshi, Postgres'da full-text search.
1. Nega bu kerak?
Oldingi darsda inverted index, BM25, typo tolerance va facet'larni o'zimiz yozdik. Ishladi, lekin bitta jarayon xotirasida. Qayta ishga tushsa — indeks yo'qoladi. Yangi hujjat qo'shilsa — qayta qurish kerak. Ikki server bo'lsa — ikkita indeks.
Haqiqiy loyihada bu ishni alohida xizmat qiladi: u indeksni diskda saqlaydi, hujjatlarni fonda qo'shadi va HTTP API orqali javob beradi. Meilisearch — shunday xizmatlardan eng soddasi. Bitta dastur (Rust'da yozilgan), sozlamasiz ishga tushadi, sukut bo'yicha xato yozilgan so'zni topadi.
Bu darsning hamma chiqishlari — kanon tajribasidan: Meilisearch 1.55.0 (Docker image getmeili/meilisearch:v1.55.0), meilisearch-js 0.62.0, Node 24.21.0, Windows 11. Biz bu darsda Docker'ni yoqmadik. Typesense esa sinalmagan — u haqidagi gaplar hujjat bo'yicha (2026-10).
2. Ishga tushirish va asosiy tushunchalar
2.1 Lokal server
Meilisearch hujjati Docker bilan ishga tushirishni tavsiya qiladi. Kanon shu buyruqqa o'xshash konteyner ishlatgan (port faqat 127.0.0.1 da):
docker run -d --name meili -p 127.0.0.1:7700:7700 \
-e MEILI_MASTER_KEY="$MEILI_MASTER_KEY" \
-v meili-data:/meili_data \
getmeili/meilisearch:v1.55.0MEILI_MASTER_KEY — bosh kalit. U bilan hamma narsa mumkin: indeks yaratish, o'chirish, boshqa kalitlar yasash. Hujjatga ko'ra production rejimida u kamida 16 bayt bo'lishi shart. Image versiyasini aniq yozing (latest emas) — indeks formati versiyalar orasida o'zgarishi mumkin.
.env.example dagi namuna:
MEILI_URL=http://127.0.0.1:7700
MEILI_MASTER_KEY=lokal-uchun-kamida-16-bayt2.2 Indeks, hujjat, task
| Atama | Ma'nosi | Postgres'dagi o'xshashi |
|---|---|---|
| indeks (index) | bir turdagi hujjatlar to'plami | jadval |
| hujjat (document) | JSON obyekt | qator |
| birlamchi kalit (primary key) | har hujjatning noyob maydoni | PRIMARY KEY |
| task | fonda bajariladigan yozish amali | — |
Oxirgi qator muhim. Meilisearch'ga hujjat qo'shish asinxron: API darhol taskUid qaytaradi, indekslash esa fonda ketadi. Hujjat qidiruvda qachon ko'rinadi — task tugaganda. meilisearch-js 0.6x da har yozish amali .waitTask() metodiga ega: u task tugashini kutadi.
3. Indeks yaratish va sozlash
Kanon kodi (telefon uchun qatorlarni bo'ldik, master kalitni env'dan oldik):
import { Meilisearch } from "meilisearch";
const meili = new Meilisearch({
host: process.env.MEILI_URL,
apiKey: process.env.MEILI_MASTER_KEY,
});
await meili
.createIndex("vazifalar", { primaryKey: "id" })
.waitTask();
const ix = meili.index("vazifalar");
await ix.updateSettings({
searchableAttributes: ["matn"],
filterableAttributes: ["egasi", "bajarildi", "ustuvorlik"],
sortableAttributes: ["id"],
typoTolerance: {
minWordSizeForTypos: { oneTypo: 4, twoTypos: 8 },
},
}).waitTask();
const task = await ix
.addDocuments(vazifalar, { primaryKey: "id" })
.waitTask({ timeout: 120_000 });
console.log(task.status);vazifalar — oldingi darsdagi 50 000 ta sun'iy vazifa: { id, egasi, matn, bajarildi, ustuvorlik }. Kanon natijasi:
50 000 hujjat qo'shish (asinxron task): 1058 ms, holat succeededSozlamalarni birma-bir ko'ramiz:
searchableAttributes— qaysi maydonlarda so'z qidiriladi. Sukut —["*"], ya'ni hammasida. Bu xavfli:egasi: 7raqami ham qidiruvga tushadi. Ro'yxat tartibi ham ma'noli — oldingi maydondagi moslik kuchliroq (sarlavha, keyin tavsif).filterableAttributes— filtr va facet uchun maydonlar. Sukut — bo'sh. Ro'yxatda yo'q maydon bo'yicha filtr xato beradi. Dvigatel shu maydonlarga alohida indeks quradi — oldingi darsdagi facet indeksining o'zi.sortableAttributes—sort: ["id:desc"]uchun.typoTolerance.minWordSizeForTypos— necha harfdan boshlab xatoga ruxsat. Sukut — 5 va 9. Kanon 4 va 8 qilgan, chunkikitobkabi qisqa so'zlar ko'p.
Diqqat: Sozlamani o'zgartirish ham task. U indeksni qayta quradi — katta indeksda bu daqiqalar olishi mumkin. Sozlamalarni hujjat yuklashdan oldin bering.
Saralash qoidalari (rankingRules) sukut bo'yicha: words, typo, proximity, attributeRank, sort, wordPosition, exactness. Ma'nosi: avval so'rovdagi so'zlardan ko'prog'i topilgan hujjatlar, ular teng bo'lsa — kamroq xato bilan topilganlar, keyin so'zlari bir-biriga yaqin turganlar. Qolgan to'rt qoida ham shu tartibda ishlaydi. Meilisearch BM25 ishlatmaydi: u qoidalarni ketma-ket qo'llaydi, oldingi qoida teng qoldirgan hujjatlarni keyingisi ajratadi.
4. Qidiruv: typo tolerance, filtr, facet
4.1 Oddiy qidiruv
const sorovlar = ["kitob o'qish", "kitb oqish", "hisobt", "o‘qish"];
for (const s of sorovlar) {
const r = await ix.search(s, { limit: 3 });
const jami = r.estimatedTotalHits;
console.log(
`"${s}" → ~${jami} ta, ${r.processingTimeMs} ms;` +
` 1-si "${r.hits[0]?.matn}"`,
);
}Kanon chiqishi (to'rtinchi so'rov boshqa apostrof bilan yozilgan):
"kitob o'qish" → ~6857 ta, 8 ms; 1-si "kitob o'qish do'kon ariza kommunal dorixona"
"kitb oqish" → ~6857 ta, 4 ms; 1-si "kitob olish qo'ng'iroq"
"hisobt" → ~6837 ta, 1 ms; 1-si "hisobot hisobot tili sut"
"o‘qish" → ~6199 ta, 2 ms; 1-si "o'qish dars to'lov ota-ona sport"Qatorma-qator:
estimatedTotalHits— taxminiy son (~shuning uchun). Meilisearch aniq sanoqqa vaqt sarflamaydi. Aniq son kerak bo'lsa —pagevahitsPerPagebilan sahifalash (undatotalHitsaniq, lekin sukut chegarasi 1 000).kitb oqish— ikkala so'z xato.kitb→kitobtopildi.oqishesaolishga ham,o'qishga ham bitta harf masofada. Birinchi natijadaolishchiqdi. Typo tolerance "foydalanuvchi nimani nazarda tutgan" ni bilmaydi — u faqat masofani biladi.hisobt— 6 harf, bitta xato ruxsat,hisobottopildi.processingTimeMs— dvigatel ichidagi vaqt: 1–8 ms.
4.2 Filtr va facet
const f = await ix.search("kitob", {
filter: "egasi = 7 AND bajarildi = false",
facets: ["ustuvorlik"],
limit: 2,
});
console.log(f.estimatedTotalHits, f.facetDistribution);filtr egasi=7, bajarilmagan: 8 ta, facet ustuvorlik: {"ustuvorlik":{"orta":4,"past":1,"yuqori":3}}Filtr — oddiy matnli ifoda: =, !=, >, >=, <, <=, TO (oraliq), IN, EXISTS, IS NULL, NOT, AND, OR. Bo'sh joyli qiymat qo'shtirnoq ichida: ustuvorlik = "juda yuqori". facetDistribution — oldingi darsda qo'lda yozgan facetlar() funksiyasining natijasi, lekin indeksdan.
4.3 Filtrni xavfsiz yig'ish
Filtr — matn. Uni foydalanuvchi kiritmasi bilan qo'shib yig'sangiz, Backend'dagi injection darsidagi muammo qaytadi: ?ustuvorlik=past OR egasi = 8 kabi qiymat filtrni kengaytiradi. Hujjatda qo'shtirnoq ichidagi qo'shtirnoqni qanday qochirish aniq yozilmagan. Shuning uchun eng ishonchli yo'l — qiymatni ruxsat ro'yxati va tur bo'yicha tekshirib, keyin qo'shish:
const USTUVORLIK = new Set(["past", "orta", "yuqori"]);
function filtrYasa(egasiId, sorov) {
if (!Number.isSafeInteger(egasiId) || egasiId <= 0) {
throw new Error("egasiId noto'g'ri");
}
const qismlar = [`egasi = ${egasiId}`];
if (sorov.ustuvorlik !== undefined) {
if (!USTUVORLIK.has(sorov.ustuvorlik)) {
throw new Error(`ustuvorlik noto'g'ri: ${sorov.ustuvorlik}`);
}
qismlar.push(`ustuvorlik = ${sorov.ustuvorlik}`);
}
if (sorov.bajarildi === "true" || sorov.bajarildi === "false") {
qismlar.push(`bajarildi = ${sorov.bajarildi}`);
}
return qismlar.join(" AND ");
}
console.log(
filtrYasa(7, { ustuvorlik: "yuqori", bajarildi: "false" }),
);
console.log(filtrYasa(7, {}));
try {
filtrYasa(7, { ustuvorlik: "past OR egasi = 8" });
} catch (e) {
console.log(e.message);
}Konsolda:
egasi = 7 AND ustuvorlik = yuqori AND bajarildi = false
egasi = 7
ustuvorlik noto'g'ri: past OR egasi = 8Egasi filtri doim birinchi va har doim server tomonda qo'yiladi — mijoz uni olib tashlay olmaydi. Uchinchi so'rov ruxsat ro'yxatidan o'tmadi. Bu Zod 4 serverda darsidagi z.enum bilan ham yoziladi — g'oya bir xil.
Tekshirib ko'ring: Nega
bajarildiuchunBoolean(sorov.bajarildi)yozish xato bo'lardi?
Javob
Query parametr — satr. Boolean("false") — true, chunki bo'sh bo'lmagan har satr truthy (Boolean va truthy/falsy qiymatlar). Foydalanuvchi "bajarilmaganlar" so'rasa, bajarilganlar chiqardi. Shuning uchun aniq ikki satr bilan solishtirdik.
5. Tezlik: kanon nimani o'lchadi
Kanon filtrli qidiruvni meilisearch-js orqali 100 marta yuborib, vaqtni o'lchadi:
HTTP orqali so'rov (filtr bilan): mediana 51.2 ms (dvigatel ichida 1–2 ms; curl bilan o'sha so'rov 5–16 ms — meilisearch-js orqali farq sababi ANIQLANMADI; Postgres o'sha Docker proksisi orqali 4 ms)Halol o'qiymiz. Dvigatel ichida 1–2 ms. curl bilan to'liq HTTP aylanishi 5–16 ms. meilisearch-js orqali esa ~51 ms — va kanon bu farqning sababini topmagan. Docker Desktop'ning Windows'dagi tarmoq proksisi gumon qilingan, lekin Postgres o'sha proksi orqali 4 ms bergan. Shuning uchun "Meilisearch 50 ms" deb xulosa qilmang: bu bitta mashina, bitta muhit va tushuntirilmagan o'lchov. O'z muhitingizda o'lchang — processingTimeMs ni va to'liq vaqtni alohida.
6. API kalitlar va tenant token
6.1 Kalit turlari
| Kalit | Kimda | Nima qila oladi |
|---|---|---|
| master key | faqat server env'ida | hamma narsa, boshqa kalitlarni yaratish |
| admin API key | backend | indeks va hujjatlarni boshqarish |
| search API key | brauzer ham olishi mumkin | faqat qidiruv |
| tenant token | brauzer | faqat qidiruv + imzolangan filtr |
Meilisearch master kalit bilan ishga tushganda ikkita sukut kalit yaratadi: "Default Search API Key" va "Default Admin API Key". O'zingiz createKey bilan kerakli actions va indexes li kalit yasaysiz.
Search kalitini brauzerga bersangiz, qidiruvni frontend to'g'ridan Meilisearch'ga yuborishi mumkin — backend'dan o'tmasdan. Tez. Lekin vazifalar indeksida hamma foydalanuvchining vazifalari bor. Search kaliti hammasini qidira oladi.
6.2 Tenant token
Tenant token — search kaliti bilan imzolangan JWT. Ichida searchRules: qaysi indeksda, qaysi filtr bilan qidirish mumkin. Imzoni backend qo'yadi, brauzer uni o'zgartira olmaydi. JWT tuzilishi 28-qismda chuqur o'rganiladi — bu yerda "imzolangan, muddatli ruxsat" deb tushunish yetadi.
sequenceDiagram
participant B as Brauzer
participant A as Bizning API
participant M as Meilisearch
B->>A: GET /qidiruv-token (sessiya bilan)
A->>A: search kaliti bilan imzolaydi, filtr egasi = 7
A-->>B: tenant token (1 soat)
B->>M: search("kitob"), token bilan
M->>M: imzoni tekshiradi, filtrni qo'shadi
M-->>B: faqat egasi 7 hujjatlariDiagrammaga qarang: so'rovning o'zi bizning API'dan o'tmaydi. API faqat token beradi. Kanon kodi:
import { generateTenantToken } from "meilisearch/token";
const kalit = await meili.createKey({
description: "faqat qidiruv",
actions: ["search"],
indexes: ["vazifalar"],
expiresAt: null,
});
const token = await generateTenantToken({
apiKey: kalit.key,
apiKeyUid: kalit.uid,
searchRules: { vazifalar: { filter: "egasi = 7" } },
expiresAt: new Date(Date.now() + 3600_000),
});
const mijoz = new Meilisearch({
host: process.env.MEILI_URL,
apiKey: token,
});Kanon uchta narsani tekshirdi:
tenant token bilan "kitob": 16 ta, egasilar: [7]
mijoz o'zi "egasi = 8" filtrini qo'shsa: 0 ta (token filtri AND bilan qo'shiladi)
token bilan yozish: invalid_api_keyBirinchi — faqat 7-foydalanuvchi hujjatlari. Ikkinchi — mijoz o'z filtrini qo'shsa, u token filtri bilan AND bo'ladi: egasi = 7 AND egasi = 8 — bo'sh. Uchinchi — token faqat qidiruvga, yozish rad etildi.
Kanon topilmasi: ko'p maqola va eski hujjatda client.generateTenantToken(...) yozilgan. meilisearch-js 0.6x da bunday metod yo'q. Funksiya alohida kirish nuqtasiga ko'chgan: import { generateTenantToken } from "meilisearch/token". Kutubxona versiyasini va uning README'sini doim tekshiring.
7. Bazadan indeksga: sinxronlash
Haqiqat manbai — Postgres. Meilisearch — uning qidiruv uchun nusxasi. Vazifa o'zgarsa, indeks ham yangilanishi kerak. Uch yo'l bor:
| Usul | Qanday | Muammo |
|---|---|---|
| Ikki yozish | handler DB'ga, keyin Meili'ga yozadi | Meili ishlamasa — indeks eskiradi, xato yo'qoladi |
| Davriy to'liq qayta yuklash | cron har tunda hammasini yuklaydi | kun davomida eski natija |
| Outbox | DB tranzaksiyasida voqea yoziladi, relay Meili'ga yuboradi | biroz kechikish, lekin yo'qolmaydi |
Transactional outbox darsida aynan shu muammoni yechgandik: voqea biznes yozuvi bilan bitta tranzaksiyada saqlanadi, alohida jarayon uni yetkazadi. Qidiruv indeksi — outbox'ning eng ko'p uchraydigan iste'molchisi. Kanon relay qadamini o'lchadi:
await ix
.updateDocuments([{ id: 7, matn: "kitob o'qish yangilandi" }])
.waitTask();
await ix.deleteDocument(8).waitTask();bitta yangilash ko'rinishigacha: 151 ms (asinxron task — darhol emas)updateDocuments — qisman yangilash: faqat berilgan maydonlar almashadi (addDocuments esa hujjatni butunlay almashtiradi). 151 ms — foydalanuvchi vazifani saqlab, darhol qidirsa, eski matnni ko'rishi mumkin. Bu oxir-oqibat izchillik (eventual consistency, ACID, BASE, CAP va PACELC): indeks bazadan biroz orqada. UI'da buni hisobga oling — saqlangan vazifani ro'yxatga indeksdan emas, API javobidan qo'shing.
Relay'ga yana ikki maslahat. Birinchisi — voqealarni bittadan emas, to'plab yuboring: 100 ta updateDocuments emas, bitta 100 hujjatli chaqiruv. Ikkinchisi — o'chirishni unutmang: DB'dan o'chgan vazifa indeksda qolsa, qidiruvda "arvoh" chiqadi.
8. Typesense — hujjat bo'yicha
Typesense kanonda sinalmagan (disk va vaqt tejaldi). Quyidagilar rasmiy hujjat bo'yicha (typesense.org/docs, 2026-10):
| Meilisearch | Typesense | |
|---|---|---|
| Til | Rust | C++ |
| Indeks qayerda | diskda (xotirada keshlanadi) | to'liq operativ xotirada |
| Sxema | sxemasiz, sozlamalar bilan | kolleksiya sxemasi oldindan |
| Brauzer kaliti | tenant token (JWT) | scoped search key (ichida filtr) |
| Klaster | asosan bitta tugun | o'rnatilgan Raft klasteri |
Eng muhim farq — xotira. Typesense butun indeksni RAM'da ushlaydi: tez, lekin ma'lumot hajmi RAM bilan cheklanadi. Typesense'da har kolleksiya uchun maydonlar va turlarini oldindan e'lon qilasiz — xato tur bilan hujjat rad etiladi. Bu Postgres'ga o'xshash qat'iylik. Brauzer uchun g'oya bir xil: search kalitidan filtr ichiga yozilgan "scoped" kalit yasaladi.
Tanlov odatda oddiy: kichik va o'rta katalog, tez boshlash — ikkalasi ham mos. Jamoa tajribasi, hosting narxi va kerakli imkoniyat (masalan geo-qidiruv, vektor qidiruv) hal qiladi. Ikkalasida ham bulutli pullik xizmat bor.
9. Hujumchi nigohi
- Master kalitni frontend'ga qo'yadi.
NEXT_PUBLIC_MEILI_KEYga master yoki admin kalit yozilsa — har kim indeksni o'chiradi. Himoya: brauzerga faqat tenant token. - Filtrni kengaytiradi. Foydalanuvchi matni filtr satriga qo'shilsa —
ORbilan begona hujjatlar. Himoya: ruxsat ro'yxati va tur tekshiruvi, egasi filtri — server tokenida. - Eski tokenni ishlatadi. Foydalanuvchi bloklangan, token esa amal qilyapti. Himoya: qisqa
expiresAt(masalan 1 soat), zarur bo'lsa kalitning o'zini o'chirish — u bilan imzolangan hamma tokenlar o'ladi. - Maxfiy maydonni qidiradi.
searchableAttributes: ["*"]bo'lsa, hujjatdagi har maydon — telefon, izoh — qidiriladi va javobda qaytadi. Himoya: indeksga faqat kerakli maydonlarni yuboring,displayedAttributesbilan javobni cheklang. - Meilisearch portiga to'g'ridan kiradi.
-p 7700:7700hamma interfeysda ochiq. Himoya:127.0.0.1ga bog'lash yoki ichki tarmoq, oldida HTTPS.
10. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
invalid_search_filter / atribut filtrlanmaydi |
maydon filterableAttributes da yo'q |
sozlamaga qo'shing (indeks qayta quriladi) |
| Qo'shilgan hujjat qidiruvda yo'q | task hali tugamagan | .waitTask() yoki task holatini kuzating |
client.generateTenantToken is not a function |
kutubxona API'si ko'chgan | meilisearch/token dan import |
estimatedTotalHits aniq emas |
taxminiy sanoq | page/hitsPerPage sahifalash |
| O'chirilgan vazifa qidiruvda | relay o'chirishni yubormaydi | outbox'da ochirildi voqeasi |
"false" filtri true bo'lib ketdi |
satrni Boolean() qildi |
aniq solishtirish |
11. Mashqlar
Mashqlar kurs/mashqlar/29/53-meilisearch/ da.
1-mashq (oson): bo'shliqlarni to'ldiring
- Hujjat qo'shish natijasi darhol emas, fonda — bu
- Filtr va facet uchun maydon ro'yxati —
- Brauzerga beriladigan, filtri imzo ichida bo'lgan kalit — tenant
- Tenant token filtri bilan mijoz filtri orasidagi amal —
- Butun indeksni RAM'da saqlaydigan dvigatel —
Yechim
- task —
.waitTask()uning tugashini kutadi. - filterableAttributes — sukut bo'yicha bo'sh.
- token — search kaliti bilan imzolangan JWT.
- AND — mijoz filtrni faqat toraytira oladi.
- Typesense — Meilisearch esa indeksni diskda saqlaydi.
2-mashq (o'rta): sana oralig'i bilan filtr
filtrYasa ga muddatdan va muddatgacha parametrlarini qo'shing (satr, YYYY-MM-DD). Indeksda muddat — UNIX vaqt soniyalarda (son). Sana to'g'ri formatda bo'lmasa — xato. Ikkalasi berilsa — muddat X TO Y (Meilisearch oraliq operatori), bittasi berilsa — >= yoki <=. Sana Toshkent vaqti emas, UTC yarim tun deb olinsin.
Yechim
const SANA = /^\d{4}-\d{2}-\d{2}$/;
function soniya(sana) {
if (!SANA.test(sana)) throw new Error(`sana noto'g'ri: ${sana}`);
const ms = Date.parse(`${sana}T00:00:00Z`);
if (Number.isNaN(ms)) throw new Error(`sana noto'g'ri: ${sana}`);
return ms / 1000;
}
function filtrYasa(egasiId, sorov) {
if (!Number.isSafeInteger(egasiId) || egasiId <= 0) {
throw new Error("egasiId noto'g'ri");
}
const qismlar = [`egasi = ${egasiId}`];
const dan = sorov.muddatdan && soniya(sorov.muddatdan);
const gacha = sorov.muddatgacha && soniya(sorov.muddatgacha);
if (dan && gacha) qismlar.push(`muddat ${dan} TO ${gacha}`);
else if (dan) qismlar.push(`muddat >= ${dan}`);
else if (gacha) qismlar.push(`muddat <= ${gacha}`);
return qismlar.join(" AND ");
}
console.log(filtrYasa(7, {
muddatdan: "2026-11-01", muddatgacha: "2026-11-30",
}));
console.log(filtrYasa(7, { muddatdan: "2026-11-01" }));
for (const yomon of ["2026-13-45", "1 OR 1"]) {
try {
filtrYasa(7, { muddatgacha: yomon });
} catch (e) {
console.log(e.message);
}
}Konsolda:
egasi = 7 AND muddat 1793491200 TO 1795996800
egasi = 7 AND muddat >= 1793491200
sana noto'g'ri: 2026-13-45
sana noto'g'ri: 1 OR 12026-13-45 regex'dan o'tadi, lekin Date.parse uni NaN qiladi — shuning uchun ikkala tekshiruv kerak. Sana soniyaga aylantirilgach, filtrga faqat son tushadi — matnli in'eksiya uchun joy qolmaydi. Oy oxirini "shu kun ham kiradi" deb hisoblamoqchi bo'lsangiz, muddatgacha ga bir kun qo'shib < ishlating.
3-mashq (qiyin): outbox voqealarini relay to'plamiga aylantirish
Outbox jadvalidan tartib bilan voqealar keldi: { tur: "yaratildi" | "yangilandi" | "ochirildi", id, matn? }. toplam(voqealar) funksiyasini yozing: u { yangilash: [...hujjatlar], ochirish: [...id] } qaytarsin — Meilisearch'ga ikki chaqiruv bilan yuborish uchun. Qoidalar: bitta id bo'yicha oxirgi voqea yutadi; oxirgisi ochirildi bo'lsa — id ochirish ga; aks holda oxirgi matn bilan yangilash ga. Natija id bo'yicha o'sish tartibida.
Yechim
function toplam(voqealar) {
const oxirgi = new Map();
for (const v of voqealar) oxirgi.set(v.id, v);
const yangilash = [];
const ochirish = [];
const idlar = [...oxirgi.keys()].sort((a, b) => a - b);
for (const id of idlar) {
const v = oxirgi.get(id);
if (v.tur === "ochirildi") ochirish.push(id);
else yangilash.push({ id, matn: v.matn });
}
return { yangilash, ochirish };
}
console.log(toplam([
{ tur: "yaratildi", id: 12, matn: "Non olish" },
{ tur: "yangilandi", id: 7, matn: "Kitob o'qish" },
{ tur: "yangilandi", id: 12, matn: "Non va sut olish" },
{ tur: "ochirildi", id: 7 },
{ tur: "yaratildi", id: 3, matn: "Hisobot" },
]));Konsolda:
{
yangilash: [ { id: 3, matn: 'Hisobot' }, { id: 12, matn: 'Non va sut olish' } ],
ochirish: [ 7 ]
}Besh voqea — Meilisearch'ga ikki chaqiruv. 12-vazifaning oraliq holati (Non olish) yuborilmadi: indeksga faqat oxirgi holat kerak. 7-vazifa yangilanib, keyin o'chirildi — faqat o'chirish ketdi. Bu tartib outbox'dan voqealar tartib bilan o'qilganda to'g'ri. Agar relay bir nechta nusxada parallel ishlasa, eski voqea yangisidan keyin kelishi mumkin — u holda hujjatga versiya maydoni qo'shib, eskisini tashlab yuborish kerak.
12. Loyiha qadami: kanon tajribasi, loyihaga kirmaydi
vazifalar-api ga Meilisearch qo'shilmaydi. Kanon xulosasi oldingi darsdagidek: shaxsiy vazifalar uchun 27-qismdagi Postgres FTS yetarli, alohida dvigatel esa yana bitta xizmat, zaxira va sinxronlash. Meilisearch kursda 38-qismdagi bozor mahsulot katalogida ishlatiladi — u yerda typo tolerance va facet'lar haqiqatan kerak.
O'zingiz sinab ko'ring (Docker bor bo'lsa). Darsdagi docker run bilan lokal Meilisearch oching. Oldingi darsdagi kabi bir necha ming sun'iy vazifa yuklang. filtrYasa va tenant token bilan backend'da ikki marshrut yozing: GET /qidiruv?q= (server orqali) va GET /qidiruv-token (brauzer uchun token). Ikkalasining vaqtini o'lchab solishtiring.
13. Real ishda
- Internet-do'kon, e'lonlar sayti, ma'lumotnoma — Meilisearch va Typesense'ning asosiy joyi: typo tolerance, facet, sinonimlar.
- Laravel Scout, Strapi, Directus kabi vositalarda Meilisearch tayyor plagin sifatida bor.
- Frontend kutubxonalari — InstantSearch (Algolia'niki) Meilisearch va Typesense bilan adapter orqali ishlaydi: qidiruv qutisi, facet ro'yxati, sahifalash tayyor komponentlar.
- Bulut — Meilisearch Cloud va Typesense Cloud: o'zingiz serverni boshqarmaysiz, pullik.
- Intervyuda: "Qidiruv indeksini DB bilan qanday sinxron ushlaysiz?", "Frontend qidiruv dvigateliga to'g'ridan murojaat qilsa, ma'lumot sizib ketmaydimi?", "Meilisearch va Elasticsearch farqi?"
Xulosa
- Meilisearch: indeks → hujjatlar; yozish — asinxron task,
.waitTask()bilan kutiladi. searchableAttributes— qayerda qidirish,filterableAttributes— filtr va facet; sozlamalar hujjatlardan oldin.- Typo tolerance sukut bo'yicha yoqilgan; qisqa so'zlar chegarasi sozlanadi.
- Filtr matnini foydalanuvchi kiritmasidan to'g'ridan yig'mang — ruxsat ro'yxati va tur tekshiruvi.
- Brauzerga faqat tenant token: filtr imzo ichida, mijoz filtri
ANDbilan qo'shiladi. - Indeks — bazaning nusxasi; uni outbox orqali yangilang va kechikishni (oxir-oqibat izchillik) hisobga oling.
Keyingi dars: Elasticsearch va OpenSearch — katta hajm va analitika uchun sanoat standarti: mapping, analyzer, query DSL va aggregation'lar.
Manbalar
- Meilisearch — sozlamalar (
rankingRules,typoTolerance,filterableAttributessukutlari): https://www.meilisearch.com/docs/reference/api/settings - Meilisearch — filtr ifodalari: https://www.meilisearch.com/docs/learn/filtering_and_sorting/filter_expression_reference
- Meilisearch — tenant token: https://www.meilisearch.com/docs/learn/security/multitenancy_tenant_tokens
- meilisearch-js (0.62.0,
meilisearch/token): https://github.com/meilisearch/meilisearch-js - Typesense hujjati: https://typesense.org/docs/
- Kanon tajribasi (IlmHamroh, 2026-10-27): Meilisearch 1.55.0, meilisearch-js 0.62.0, Node 24.21.0, Windows 11, Docker Desktop. Typesense — sinalmagan.
filtrYasava mashq yechimlari — biz ishga tushirdik (2026-10-28, Node 24.21.0).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!