IlmHamroh
JavaScript Full-stack/29-qism. Backend integratsiyalar: fayl, to'lov, bot, realtime53/69-dars17 daqiqa
Mundarija (26)

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, filterableAttributes va sortableAttributes ni 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):

bash
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.0

MEILI_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:

text
MEILI_URL=http://127.0.0.1:7700
MEILI_MASTER_KEY=lokal-uchun-kamida-16-bayt

2.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):

js
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:

text
50 000 hujjat qo'shish (asinxron task): 1058 ms, holat succeeded

Sozlamalarni birma-bir ko'ramiz:

  • searchableAttributes — qaysi maydonlarda so'z qidiriladi. Sukut — ["*"], ya'ni hammasida. Bu xavfli: egasi: 7 raqami 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, chunki kitob kabi 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

js
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):

text
"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 — page va hitsPerPage bilan sahifalash (unda totalHits aniq, lekin sukut chegarasi 1 000).
  • kitb oqish — ikkala so'z xato. kitb → kitob topildi. oqish esa olish ga ham, o'qish ga ham bitta harf masofada. Birinchi natijada olish chiqdi. Typo tolerance "foydalanuvchi nimani nazarda tutgan" ni bilmaydi — u faqat masofani biladi.
  • hisobt — 6 harf, bitta xato ruxsat, hisobot topildi.
  • processingTimeMs — dvigatel ichidagi vaqt: 1–8 ms.

4.2 Filtr va facet

js
const f = await ix.search("kitob", {
  filter: "egasi = 7 AND bajarildi = false",
  facets: ["ustuvorlik"],
  limit: 2,
});
console.log(f.estimatedTotalHits, f.facetDistribution);
text
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:

js
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:

text
egasi = 7 AND ustuvorlik = yuqori AND bajarildi = false
egasi = 7
ustuvorlik noto'g'ri: past OR egasi = 8

Egasi 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 bajarildi uchun Boolean(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:

text
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 hujjatlari

Diagrammaga qarang: so'rovning o'zi bizning API'dan o'tmaydi. API faqat token beradi. Kanon kodi:

js
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:

text
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_key

Birinchi — 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:

js
await ix
  .updateDocuments([{ id: 7, matn: "kitob o'qish yangilandi" }])
  .waitTask();
await ix.deleteDocument(8).waitTask();
text
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

  1. Master kalitni frontend'ga qo'yadi. NEXT_PUBLIC_MEILI_KEY ga master yoki admin kalit yozilsa — har kim indeksni o'chiradi. Himoya: brauzerga faqat tenant token.
  2. Filtrni kengaytiradi. Foydalanuvchi matni filtr satriga qo'shilsa — OR bilan begona hujjatlar. Himoya: ruxsat ro'yxati va tur tekshiruvi, egasi filtri — server tokenida.
  3. 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.
  4. Maxfiy maydonni qidiradi. searchableAttributes: ["*"] bo'lsa, hujjatdagi har maydon — telefon, izoh — qidiriladi va javobda qaytadi. Himoya: indeksga faqat kerakli maydonlarni yuboring, displayedAttributes bilan javobni cheklang.
  5. Meilisearch portiga to'g'ridan kiradi. -p 7700:7700 hamma interfeysda ochiq. Himoya: 127.0.0.1 ga 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
js
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:

text
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 1

2026-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
js
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:

text
{
  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 AND bilan 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

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!