WisarWisar
Dasturlash kitobi/8-QISM — NestJS27 daqiqa

8.22-bob: Kuchli qidiruv — Elasticsearch, Meilisearch

8-QISM — NestJS (chuqur) · 22-mavzu · Amaliy real mavzu


1. Kirish va motivatsiya

Endi yana bir har real loyihada kerak bo'ladigan mavzu — kuchli qidiruv (full-text search). Har e-commerce (mahsulot qidirish), kontent platforma (maqola), bozor (e'lon), ish topish sayti — sifatli, tez, xato-bardosh qidiruvga muhtoj. 6.3 (Mongo text index) va 6.7 (SQL) da DB qidiruvini ko'rdik, lekin DB qidiruvi (LIKE '%matn%') zaif: sekin (indeks ishlamaydi), xatoni kechirmaydi ("telefn" hech narsa), ahamiyat bo'yicha saralamaydi, til xususiyatlarini bilmaydi. Maxsus qidiruv tizimi (Elasticsearch, Meilisearch, Typesense) bu muammolarni hal qiladi.

Tasavvur qiling: mijoz "ayfon 15" deb qidiradi, lekin siz bazada "iPhone 15" deb saqlagansiz, mijoz "telefn" deb xato yozadi — DB LIKE hech narsa topmaydi (mijoz ketadi). Kuchli qidiruv tizimi: xato-bardosh ("telefn" "telefon" topadi), ahamiyatli (eng mos natija birinchi), tez (millisekund — millionlab yozuv), fasetli (filtr — narx, kategoriya), ajratib ko'rsatish (highlight — mos so'z belgilanadi). Bu — sifatli mahsulot tajribasi (qidiruv yomon bo'lsa — sotuv tushadi).

Bu bob: nega maxsus qidiruv (DB qidiruvi muammosi), qidiruv tizimlari (Elasticsearch — kuchli/murakkab, Meilisearch/Typesense — oddiy/tez — qachon qaysi), asosiy tushunchalar (index, document, relevance, typo tolerance, faceting), NestJS integratsiya, indekslash (DBqidiruv sinxron), va xato-bardosh/fasetli qidiruv. Bu bob 6.3 (text index), 8.13 (Mongo), 11 (frontend qidiruv UI) bilan bog'liq. Sifatli qidiruv — mahsulot muvaffaqiyatining muhim qismi.

O'xshatish: DB qidiruvi (LIKE) — kutubxonada har kitobni qo'lda varaqlash (sekin, aniq nomni bilishingiz kerak). Maxsus qidiruv tizimi — professional kutubxonachi: u barcha kitobni oldindan kataloglagan (index), siz noaniq aytsangiz ham tushunadi ("Dostayevskiy" "Dostoyevskiy" — typo tolerance), eng mosini birinchi beradi (relevance), mavzu/yil bo'yicha filtrlaydi (facets), va bir soniyada javob beradi (million kitob orasidan). Qidiruv tizimi — ma'lumotni qidirishga maxsus optimallashtirilgan (DB — saqlashga).

Nega muhim?

  • DB qidiruvi zaif — sekin, xatoni kechirmaydi, saralamaydi.
  • Mijoz tajribasi — qidiruv yomon bo'lsa, sotuv tushadi.
  • Har platformada — e-commerce, kontent, e'lon, ish.
  • Tez + ahamiyatli — millisekund, eng mos natija.

2. Nazariya — chuqur tushuntirish

2.1. DB qidiruvi muammosi (nega maxsus tizim)

text
  DB LIKE qidiruvi (SELECT ... WHERE nom LIKE '%matn%'):
   Sekin (indeks ishlamaydi '%...%' da — to'liq skanlash)
   Xatoni kechirmaydi ("telefn"  0 natija)
   Ahamiyat yo'q (saralash tartibsiz)
   Til bilmaydi (so'z shakllari, sinonim)
   Ko'p maydon bo'yicha qidiruv qiyin

  MAXSUS QIDIRUV (Elasticsearch/Meilisearch):
   Tez (teskari indeks — inverted index)
   Typo tolerance ("telefn"  "telefon")
   Relevance (ahamiyat bo'yicha saralash)
   Faceting (filtr), highlighting, sinonim

DB qidiruvi muammosi: LIKE '%matn%' — sekin (indekssiz to'liq skanlash), xatoni kechirmaydi, ahamiyatsiz, til bilmaydi. Maxsus qidiruv tizimi — bu uchun maxsus qurilgan: inverted index (teskari indeks — so'zhujjatlar, tez), typo tolerance, relevance (BM25 algoritm), faceting. Kichik/oddiy qidiruv — DB yetadi (6.3 — over-engineering'dan qoch); katta/sifatli qidiruv — maxsus tizim.

2.2. Qidiruv tizimlari — qachon qaysi

text
  ┌──────────────┬─────────────────────────────────────────────┐
  │ Meilisearch  │ ODDIY, tez, "out of box" typo tolerance.    │
  │ / Typesense  │ Zero-config, instant search. Kichik-o'rta.  │
  │              │  e-commerce, kontent (oson, tez sozlash)   │
  ├──────────────┼─────────────────────────────────────────────┤
  │ Elasticsearch│ KUCHLI, murakkab. To'liq nazorat (DSL),     │
  │ / OpenSearch │ analytics, log, juda katta hajm.            │
  │              │  katta, murakkab, log/analytics            │
  ├──────────────┼─────────────────────────────────────────────┤
  │ DB (PG/Mongo)│ Oddiy qidiruv (kam ma'lumot — 6.3, 6.7)     │
  │ PG full-text │ PostgreSQL tsvector (o'rtacha — DB ichida)  │
  └──────────────┴─────────────────────────────────────────────┘

Qidiruv tizimlari tanlovi: Meilisearch/Typesense — oddiy, tez, "out of box" (typo tolerance default, zero-config — kichik/o'rta e-commerce/kontent uchun ideal — 10-100x oson). Elasticsearch/OpenSearch — kuchli, murakkab (to'liq nazorat — query DSL, analytics, log, juda katta hajm — lekin og'ir sozlash). DB (PostgreSQL tsvector — 6.7) — o'rtacha (DB ichida, qo'shimcha tizim kerak emas). Ko'p loyiha uchun Meilisearch — eng yaxshi balans (oson + sifatli).

2.3. Asosiy tushunchalar

text
  INDEX — qidiruv "jadvali" (mahsulotlar, maqolalar — DB jadval kabi)
  DOCUMENT — bitta yozuv (bir mahsulot — JSON)
  INVERTED INDEX — teskari indeks: so'z  qaysi hujjatlarda (tezlik siri)
  RELEVANCE — ahamiyat (qaysi natija ko'proq mos — saralash)
  TYPO TOLERANCE — xato-bardosh ("telefn"  "telefon")
  FACETING — filtr/guruh (narx oralig'i, kategoriya — ko'rsatkichlar bilan)
  HIGHLIGHTING — mos so'zni ajratib ko'rsatish (<mark>)
  RANKING RULES — saralash qoidalari (so'z mosligi, typo, yaqinlik)

Asosiy tushunchalar: Index (qidiruv jadvali), document (yozuv — JSON), inverted index (so'zhujjatlar — tezlik asosi), relevance (ahamiyat — saralash), typo tolerance (xato-bardosh), faceting (filtr + ko'rsatkichlar), highlighting (mos so'z ajratish). Bular — qidiruv tizimining til lug'ati (DB'dan farqli tushunchalar). Bularni bilish — har qidiruv tizimini tushunishga yetadi.

2.4. Meilisearch integratsiya (NestJS)

typescript
import { MeiliSearch } from "meilisearch";

@Injectable()
export class SearchService implements OnModuleInit {
  private client = new MeiliSearch({
    host: this.config.get("MEILI_HOST"),
    apiKey: this.config.get("MEILI_MASTER_KEY"),       // (8.14, 14)
  });

  async onModuleInit() {                               // (8.1)
    const index = this.client.index("mahsulotlar");
    await index.updateSettings({                       // sozlash
      searchableAttributes: ["nom", "tavsif", "kategoriya"],   // qidiriladigan maydonlar
      filterableAttributes: ["kategoriya", "narx", "faol"],    // filtr (faceting — 2.6)
      sortableAttributes: ["narx", "reyting"],                 // saralash
    });
  }
}

Meilisearch integratsiya: MeiliSearch klient (host + apiKey — 8.14). index("nom") — qidiruv jadvali. updateSettings: searchableAttributes (qaysi maydonlar qidiriladi), filterableAttributes (filtr — 2.6), sortableAttributes (saralash). Sozlamadan keyin — typo tolerance, relevance avtomatik (zero-config). Bu — Meilisearch'ning oddiyligi (kam kod, ko'p imkoniyat).

2.5. Indekslash (DB qidiruv — sinxronizatsiya)

typescript
// Ma'lumot DB'da, lekin qidiruv tizimida ham bo'lishi kerak (sinxron)
async indeksla(mahsulot: Product) {
  await this.client.index("mahsulotlar").addDocuments([{
    id: mahsulot.id,
    nom: mahsulot.nom,
    tavsif: mahsulot.tavsif,
    kategoriya: mahsulot.kategoriya,
    narx: mahsulot.narx,
  }]);
}

// DB o'zgarganda — qidiruvni yangilash (hook/event — 8.13, 8.16)
// yarat/yangila  addDocuments; ochir  deleteDocument

Indekslash (eng muhim amaliy masala): ma'lumot DB'da (asosiy manba — 8.3), lekin qidiruv tizimida ham bo'lishi kerak (nusxa — qidiruv uchun). Sinxronizatsiya: DB o'zgarganda (yarat/yangila/o'chir) qidiruv indeksini yangilash (addDocuments/deleteDocument). Usul: entity hook 8.13-bob, domain event (8.16 — 9.4), yoki davriy reindex (cron — 8.18). Boshlang'ich to'liq indekslash (mavjud ma'lumot) + keyingi o'zgarishlar. DBqidiruv mos turishi — kritik.

2.6. Qidiruv + filtr (faceting)

typescript
async qidir(query: string, filter?: { kategoriya?: string; minNarx?: number; maxNarx?: number }) {
  const filterlar: string[] = ["faol = true"];
  if (filter?.kategoriya) filterlar.push(`kategoriya = "${filter.kategoriya}"`);
  if (filter?.minNarx) filterlar.push(`narx >= ${filter.minNarx}`);
  if (filter?.maxNarx) filterlar.push(`narx <= ${filter.maxNarx}`);

  return this.client.index("mahsulotlar").search(query, {
    filter: filterlar,                                 // faceting (2.3)
    facets: ["kategoriya"],                            // har kategoriyada nechta (ko'rsatkich)
    limit: 20,
    attributesToHighlight: ["nom"],                    // mos so'z ajratish
    sort: ["narx:asc"],
  });
}
// natija: { hits: [...], estimatedTotalHits, facetDistribution: { kategoriya: { telefon: 45, ... } } }

Qidiruv + faceting: search(query, { filter, facets, sort, limit }). filter — natijani cheklash (kategoriya, narx oralig'i). facets — har qiymatda nechta (e-commerce filtr paneli — "Telefon (45)", "Noutbuk (12)"). highlighting — mos so'z <mark>. filterableAttributes oldindan sozlangan bo'lishi kerak 2.4-bob. Bu — e-commerce qidiruvning to'liq tajribasi (qidiruv + filtr + ko'rsatkich).

2.7. Elasticsearch (kuchli — query DSL)

typescript
import { Client } from "@elastic/elasticsearch";

const es = new Client({ node: "http://localhost:9200" });

// Elasticsearch — kuchli, lekin murakkab (query DSL)
await es.search({
  index: "mahsulotlar",
  query: {
    bool: {                                           // murakkab mantiq
      must: [{ multi_match: { query: "telefon", fields: ["nom^2", "tavsif"] } }],   // nom muhimroq (^2)
      filter: [{ range: { narx: { gte: 100000, lte: 5000000 } } }],
    },
  },
  highlight: { fields: { nom: {} } },
});

Elasticsearch — kuchli, lekin murakkab (query DSL — boy, lekin batafsil). bool (must/should/filter — murakkab mantiq), multi_match (ko'p maydon, ^2 — vazn), range (filtr), function_score (maxsus relevance). Elasticsearch — to'liq nazorat (murakkab relevance, analytics, log — ELK stack), lekin og'ir (sozlash, resurs, klaster). Juda katta/murakkab loyiha uchun. Ko'p loyiha uchun Meilisearch yetadi 2.2-bob.

2.8. Relevance va ranking (ahamiyat)

text
  RELEVANCE — qaysi natija "eng mos" (saralash):
  - So'z mosligi (qancha qidiruv so'zi mavjud)
  - Typo soni (kam xato — yuqori)
  - So'zlar yaqinligi (proximity — yonma-yon)
  - Maydon ahamiyati (nom > tavsif — vazn)
  - Maxsus (reyting, sotuvlar — biznes vazn)

  Meilisearch: default ranking rules (avtomatik — yaxshi)
  Elasticsearch: BM25 + function_score (qo'lda sozlash)

Relevance (ahamiyat — saralash siri): natijalar eng mosdan tartibida. Omillar: so'z mosligi, typo soni, so'zlar yaqinligi, maydon vazni (nom muhimroq tavsifdan), biznes vazn (reyting, sotuv — mashhurni yuqoriga). Meilisearch — default qoidalar (avtomatik, yaxshi); Elasticsearch — qo'lda (BM25 + function_score — to'liq nazorat). To'g'ri relevance — sifatli qidiruv (mijoz birinchi natijada topadi).

2.9. Indekslash strategiyalari (sinxron)

text
  DB  QIDIRUV sinxronlash usullari:

  1. Real-time (hook/event): DB o'zgarganda darrov indeks (8.13, 8.16)
      Doim mos;  qo'shimcha kod, xato bo'lsa nomutanosib

  2. Navbat (async): o'zgarish  navbatga  worker indekslaydi (8.22 BullMQ)
      Ishonchli (retry), so'rovni bloklamaydi

  3. Davriy reindex (cron): vaqti-vaqti to'liq qayta indeks 8.18-bob
      Oddiy, nomutanosiblikni tuzatadi;  kechikish

   Eng yaxshi: navbat (real-time) + davriy reindex (zaxira)

Indekslash strategiyalari: real-time (hook/event — darrov), navbat (async — worker — ishonchli, retry — 8.22), davriy reindex (cron — to'liq qayta — zaxira/tuzatish — 8.18). Eng yaxshi — navbat (o'zgarishni qidiruvga ishonchli yetkazadi — so'rov bloklamaydi) + davriy reindex (nomutanosiblikni tuzatadi). DB — asosiy manba (source of truth); qidiruv — nusxa (qayta tiklanadigan).

2.10. Best practices (qidiruv)

text
   DB qidiruvi yetsa — uni ishlat (kichik — over-engineering'dan qoch — 2.1)
   Meilisearch (oddiy) yoki Elasticsearch (murakkab/katta — 2.2)
   searchable/filterable/sortable to'g'ri sozlash 2.4-bob
   Indekslash navbat orqali (ishonchli — 2.9) + davriy reindex
   DB = source of truth; qidiruv = nusxa (qayta tiklanadigan — 2.9)
   Faceting (filtr + ko'rsatkich — e-commerce — 2.6)
   Highlighting (mos so'z — UX); relevance vazn (nom>tavsif — 2.8)
   API key xavfsizlik (.env — 2.4, 14); pagination (katta natija)

2.11. @nestjs/elasticsearch moduli (rasmiy integratsiya)

Yuqorida 2.7-bob Elasticsearch'ni to'g'ridan-to'g'ri @elastic/elasticsearch klienti bilan ishlatdik. NestJS'da esa rasmiy o'rov — @nestjs/elasticsearch moduli bor: u Client'ni DI konteyneriga 8.1-bob ulaydi, konfiguratsiyani ConfigService 8.14-bob bilan async yuklaydi, va ElasticsearchService'ni inyeksiya qilib beradi. Bu — NestJS uslubidagi toza yo'l (klientni qo'lda yaratmaysiz, modul boshqaradi).

bash
npm install @nestjs/elasticsearch @elastic/elasticsearch
typescript
// search.module.ts — ElasticsearchModule async ro'yxatdan o'tkazish (8.14)
import { Module } from "@nestjs/common";
import { ElasticsearchModule } from "@nestjs/elasticsearch";
import { ConfigModule, ConfigService } from "@nestjs/config";

@Module({
  imports: [
    ElasticsearchModule.registerAsync({                // async — .env dan (8.14)
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        node: config.get("ES_NODE"),                   // http://localhost:9200
        auth: {
          username: config.get("ES_USER"),             // xavfsizlik (14)
          password: config.get("ES_PASSWORD"),
        },
        maxRetries: 3,                                 // ishonchlilik (qayta urinish)
        requestTimeout: 5000,
      }),
    }),
  ],
  providers: [ElasticSearchService],
  exports: [ElasticSearchService],
})
export class SearchModule {}
typescript
// search.service.ts — ElasticsearchService inyeksiya (modul yaratgan klient)
import { Injectable } from "@nestjs/common";
import { ElasticsearchService } from "@nestjs/elasticsearch";

@Injectable()
export class ElasticSearchService {
  constructor(private readonly es: ElasticsearchService) {}   // DI — modul beradi (8.1)

  async index(mahsulot: any) {
    await this.es.index({
      index: "mahsulotlar",
      id: mahsulot.id,                                 // hujjat id (yangilashda ustiga yozadi)
      document: {                                      // hujjat tanasi
        nom: mahsulot.nom, tavsif: mahsulot.tavsif,
        kategoriya: mahsulot.kategoriya, narx: mahsulot.narx,
      },
    });
  }
}

@nestjs/elasticsearch: ElasticsearchModule.registerAsync (yoki register) — klientni DI'ga ulaydi (ConfigService bilan .env dan node/auth). ElasticsearchService'ni konstruktorda inyeksiya qilasiz (private es: ElasticsearchService) — u @elastic/elasticsearch Client'ni o'rab, es.index, es.search, es.indices metodlarini beradi. Bu — NestJS uslubi (klientni qo'lda new qilmaysiz, modul hayot siklini boshqaradi). Meilisearch uchun rasmiy Nest moduli yo'q — klientni provider'da qo'lda yaratasiz 2.4-bob.

2.12. Elasticsearch mapping (indeks sxemasi)

Elasticsearch'da mapping — indeksning "sxemasi": har maydon qanday turi (text, keyword, integer, float, boolean) va qanday analiz qilinishini belgilaydi. Bu Meilisearch'dan asosiy farq: Meilisearch turlarni avtomatik aniqlaydi (zero-config), Elasticsearch'da esa siz oldindan mapping bering — aks holsa noto'g'ri tur tanlanadi (masalan, filtr uchun keyword kerak, to'liq matnli qidiruv uchun text).

typescript
// Indeksni mapping bilan yaratish (odatda deploy/migration'da bir marta)
async indeksYarat() {
  const bor = await this.es.indices.exists({ index: "mahsulotlar" });
  if (bor) return;                                     // bir marta (mavjud bo'lsa o'tkazib yubor)

  await this.es.indices.create({
    index: "mahsulotlar",
    settings: {
      analysis: {
        analyzer: {
          uz_analyzer: {                               // maxsus analizator (til)
            type: "custom",
            tokenizer: "standard",
            filter: ["lowercase", "asciifolding"],     // kichik harf + diakritika olib tashlash
          },
        },
      },
    },
    mappings: {
      properties: {
        nom:       { type: "text", analyzer: "uz_analyzer" },  // to'liq matnli qidiruv
        tavsif:    { type: "text", analyzer: "uz_analyzer" },
        kategoriya:{ type: "keyword" },                // aniq mos (filtr/facet — analiz qilinmaydi)
        brend:     { type: "keyword" },
        narx:      { type: "float" },                  // range filtr
        reyting:   { type: "float" },
        sotuvlar:  { type: "integer" },
        faol:      { type: "boolean" },
        yaratilgan:{ type: "date" },
      },
    },
  });
}

Mapping — Elasticsearch indeksining sxemasi (maydon turi + analiz). Muhim farq: text — to'liq matnli qidiruv uchun (so'zlarga bo'linadi — analiz qilinadi, nom/tavsif); keyword — aniq mos uchun (analiz qilinmaydi — filtr, facet, saralash, kategoriya/brend). Sonli maydonlar (float/integer) — range filtr. Analyzer — matnni tokenlarga bo'lish qoidasi (lowercase, asciifolding — "cafe"="café"). Noto'g'ri mapping = noto'g'ri qidiruv (masalan, kategoriya'ni text qilsangiz filtr sinadi). Meilisearch bu bosqichni avtomatlashtiradi 2.4-bob — Elasticsearch'ning murakkabligi shu yerda ko'rinadi.

2.13. Query DSL — match, bool, fuzzy, wildcard

Elasticsearch'ning kuchi — Query DSL (JSON tilida boy so'rov). Asosiy so'rov turlari:

text
  match        — to'liq matnli qidiruv (analiz qilinadi, so'zlarga bo'linadi)
  term         — aniq mos (analiz qilinmaydi — keyword maydonga)
  multi_match  — ko'p maydon bo'yicha (nom^3 — vazn)
  bool         — birlashtirish: must (AND), should (OR), must_not (NOT), filter
  fuzzy        — typo-bardosh (Levenshtein masofa — "telefn""telefon")
  wildcard     — shablon (tel* — prefiks; analiz qilinmaydi, sekin — ehtiyot)
  prefix       — prefiks (avtomatik to'ldirish uchun — "tel"  "telefon")
  range        — oraliq (narx: gte/lte — sonli/sana filtr)
typescript
// Har xil so'rov turlari — namunalar
// 1) match — to'liq matnli (eng ko'p ishlatiladigan)
{ match: { nom: "simsiz quloqchin" } }                 // "simsiz" YOKI "quloqchin" (analiz)

// 2) match — operator AND (barcha so'z bo'lishi shart)
{ match: { nom: { query: "simsiz quloqchin", operator: "and" } } }

// 3) term — aniq mos (keyword maydonga — filtr)
{ term: { kategoriya: "telefon" } }

// 4) fuzzy — typo-bardosh (Levenshtein)
{ fuzzy: { nom: { value: "telefn", fuzziness: "AUTO" } } }   // "telefon" topadi

// 5) wildcard — shablon (ehtiyot: sekin, keyword maydonga)
{ wildcard: { brend: "sam*" } }                        // "samsung" topadi

// 6) prefix — avtomatik to'ldirish
{ prefix: { nom: "quloq" } }                           // "quloqchin"

// 7) bool — murakkab mantiq (AND/OR/NOT + filtr)
{
  bool: {
    must:     [{ match: { nom: "quloqchin" } }],        // BO'LISHI SHART (skorga ta'sir)
    should:   [{ match: { brend: "sony" } }],           // BO'LSA — skor oshadi (OR, ixtiyoriy)
    must_not: [{ term: { faol: false } }],              // BO'LMASLIGI SHART
    filter:   [{ range: { narx: { lte: 500000 } } }],   // shart (skorga ta'sir qilmaydi — tez)
  },
}

Query DSL — Elasticsearch'ning JSON so'rov tili. match — to'liq matnli (analiz qilinadi — asosiy qidiruv); term — aniq mos (analiz qilinmaydi — filtr, keyword); fuzzy (yoki match'da fuzziness: "AUTO") — typo-bardosh; wildcard/prefix — shablon (wildcard sekin — ehtiyot, faqat kerak bo'lsa); bool — mantiqiy birlashtirish: must (AND — skorga ta'sir), should (OR — skorni oshiradi), must_not (NOT), filter (shart — lekin skorga ta'sir qilmaydi, keshlanadi — tez). Amaliy qoida: matn qidiruvini must+match'ga, filtrlarni (narx/kategoriya) filter'ga qo'ying — filter keshlanadi va skor hisoblamaydi (tezroq). Meilisearch bularni bitta oddiy search() API ostida yashiradi 2.6-bob — DSL yo'q, lekin nazorat ham kamroq.

2.14. PostgreSQL full-text search (DB ichida — oraliq yechim)

Maxsus tizim (Meilisearch/ES) o'rnatishdan oldin — agar ma'lumot allaqachon PostgreSQL'da bo'lsa va qidiruv talablari o'rtacha bo'lsa — PostgreSQL'ning o'z to'liq matnli qidiruvi (tsvector/tsquery — 6.7) yetarli bo'lishi mumkin. Qo'shimcha tizim, sinxronizatsiya, infratuzilma kerak emas (DB ichida).

sql
-- tsvector ustuni + GIN indeks (tez to'liq matnli qidiruv)
ALTER TABLE mahsulotlar ADD COLUMN qidiruv_vektori tsvector
  GENERATED ALWAYS AS (
    to_tsvector('simple', coalesce(nom,'') || ' ' || coalesce(tavsif,''))
  ) STORED;                                            -- avto-hisoblanadi (generated column)

CREATE INDEX idx_qidiruv ON mahsulotlar USING GIN (qidiruv_vektori);  -- GIN — tez
typescript
// NestJS + TypeORM — raw query (tsquery + ts_rank saralash — 6.7)
async pgQidir(query: string) {
  return this.repo.query(
    `SELECT id, nom, ts_rank(qidiruv_vektori, plainto_tsquery('simple', $1)) AS skor
       FROM mahsulotlar
      WHERE qidiruv_vektori @@ plainto_tsquery('simple', $1)   -- @@ mos kelish
      ORDER BY skor DESC                                       -- ahamiyat bo'yicha (relevance)
      LIMIT 20`,
    [query],
  );
}

PostgreSQL full-text (tsvector): DB ichida to'liq matnli qidiruv — to_tsvector (matnvektor), plainto_tsquery (qidiruvso'rov), @@ (mos kelish operatori), ts_rank (ahamiyat — relevance), GIN indeks (tezlik). Kuchli tomoni: qo'shimcha tizim/sinxronizatsiya yo'q (DB = yagona manba), tranzaksiyada 8.5-bob izchil. Cheklovi: typo tolerance yo'q (pg_trgm bilan qisman — trigram o'xshashlik), faceting cheklangan, o'lchovlanish DB'ga bog'liq. Tanlov: DB'da qoldiring tsvector; sifatli/xato-bardosh qidiruv kerak Meilisearch; juda katta/analytics Elasticsearch 2.2-bob.


3. Sintaksis — tez ma'lumotnoma

typescript
// Meilisearch (2.4)
const client = new MeiliSearch({ host, apiKey });
index.updateSettings({ searchableAttributes, filterableAttributes, sortableAttributes });
index.addDocuments([...]);  index.deleteDocument(id);   // indekslash (2.5)
index.search(query, { filter, facets, sort, limit, attributesToHighlight });   // qidiruv (2.6)

// Elasticsearch — @nestjs/elasticsearch (2.11, 2.7)
ElasticsearchModule.registerAsync({ useFactory: () => ({ node, auth }) });
constructor(private es: ElasticsearchService) {}       // DI inyeksiya
es.indices.create({ index, mappings: { properties } });  // mapping (2.12)
es.index({ index, id, document });                     // hujjat qo'shish
es.search({ index, query: { bool: { must, should, filter } }, highlight });   // qidiruv

// Query DSL turlari (2.13)
{ match: { nom: "matn" } }                             // to'liq matnli
{ term: { kategoriya: "telefon" } }                    // aniq mos (keyword)
{ fuzzy: { nom: { value: "telefn", fuzziness: "AUTO" } } }   // typo
{ wildcard: { brend: "sam*" } }                        // shablon
{ range: { narx: { gte: 100, lte: 5000 } } }           // oraliq

// PostgreSQL full-text (2.14)
WHERE qidiruv_vektori @@ plainto_tsquery('simple', $1) ORDER BY ts_rank(...) DESC

4. Batafsil kod namunalari

Misol 1 — Search service (Meilisearch — 2.4, 2.5)

typescript
@Injectable()
export class SearchService implements OnModuleInit {
  private client: MeiliSearch;
  private readonly INDEX = "mahsulotlar";

  constructor(private config: ConfigService) {
    this.client = new MeiliSearch({
      host: config.get("MEILI_HOST"),
      apiKey: config.get("MEILI_MASTER_KEY"),
    });
  }

  async onModuleInit() {
    const index = this.client.index(this.INDEX);
    await index.updateSettings({
      searchableAttributes: ["nom", "tavsif", "kategoriya", "brend"],
      filterableAttributes: ["kategoriya", "narx", "brend", "faol", "reyting"],
      sortableAttributes: ["narx", "reyting", "sotuvlar"],
      rankingRules: ["words", "typo", "proximity", "attribute", "sort", "exactness"],
      typoTolerance: { enabled: true },               // xato-bardosh (2.3)
    });
  }

  async indeksla(mahsulot: any) {
    await this.client.index(this.INDEX).addDocuments([{
      id: mahsulot.id, nom: mahsulot.nom, tavsif: mahsulot.tavsif,
      kategoriya: mahsulot.kategoriya, brend: mahsulot.brend,
      narx: mahsulot.narx, reyting: mahsulot.reyting, faol: mahsulot.faol,
    }]);
  }

  async ochir(id: string) {
    await this.client.index(this.INDEX).deleteDocument(id);
  }
}

Misol 2 — Qidiruv + filtr + facet (2.6)

typescript
async qidir(dto: SearchDto) {
  const filterlar: string[] = ["faol = true"];
  if (dto.kategoriya) filterlar.push(`kategoriya = "${dto.kategoriya}"`);
  if (dto.brend) filterlar.push(`brend = "${dto.brend}"`);
  if (dto.minNarx != null) filterlar.push(`narx >= ${dto.minNarx}`);
  if (dto.maxNarx != null) filterlar.push(`narx <= ${dto.maxNarx}`);

  const natija = await this.client.index("mahsulotlar").search(dto.q, {
    filter: filterlar,
    facets: ["kategoriya", "brend"],                  // filtr paneli ko'rsatkichlari
    sort: dto.sort ? [dto.sort] : undefined,          // "narx:asc"
    limit: dto.limit || 20,
    offset: ((dto.sahifa || 1) - 1) * (dto.limit || 20),
    attributesToHighlight: ["nom", "tavsif"],
    highlightPreTag: "<mark>", highlightPostTag: "</mark>",
  });

  return {
    natijalar: natija.hits,
    jami: natija.estimatedTotalHits,
    filtrlar: natija.facetDistribution,               // { kategoriya: { telefon: 45 }, brend: {...} }
  };
}

Misol 3 — Indekslash navbat orqali (2.9, 8.22)

typescript
// DB o'zgarganda  navbatga (ishonchli sinxron)
@Injectable()
export class ProductsService {
  constructor(
    @InjectRepository(Product) private repo: Repository<Product>,
    @InjectQueue("search-index") private indexQueue: Queue,
  ) {}

  async yarat(dto: CreateProductDto) {
    const mahsulot = await this.repo.save(dto);
    await this.indexQueue.add("index", { id: mahsulot.id });   // navbatga (2.9)
    return mahsulot;
  }
  async yangila(id: string, dto: UpdateProductDto) {
    await this.repo.update(id, dto);
    await this.indexQueue.add("index", { id });
  }
  async ochir(id: string) {
    await this.repo.delete(id);
    await this.indexQueue.add("remove", { id });
  }
}

@Processor("search-index")
export class SearchIndexProcessor extends WorkerHost {
  constructor(private searchService: SearchService, private repo: ProductRepository) { super(); }
  async process(job: Job) {
    if (job.name === "index") {
      const mahsulot = await this.repo.findById(job.data.id);
      if (mahsulot) await this.searchService.indeksla(mahsulot);
    } else if (job.name === "remove") {
      await this.searchService.ochir(job.data.id);
    }
    // Xato  BullMQ retry 8.22-bob — ishonchli
  }
}

Misol 4 — Davriy to'liq reindex (cron — 2.9, 8.18)

typescript
@Injectable()
export class ReindexService {
  // Har kecha to'liq reindex (nomutanosiblikni tuzatish — 2.9)
  @Cron(CronExpression.EVERY_DAY_AT_3AM)
  async toliqReindex() {
    this.logger.log("To'liq reindex boshlandi");
    const index = this.searchService.getIndex();

    // Batch'lab (xotira — streaming kabi — 8.21: 2.7)
    let offset = 0;
    const batch = 1000;
    while (true) {
      const mahsulotlar = await this.repo.find({ skip: offset, take: batch });
      if (mahsulotlar.length === 0) break;
      await index.addDocuments(mahsulotlar.map((m) => ({ id: m.id, nom: m.nom, /* ... */ })));
      offset += batch;
    }
    this.logger.log(`Reindex tugadi: ${offset} hujjat`);
  }
}

Misol 5 — Search controller (2.6)

typescript
@Controller("search")
export class SearchController {
  constructor(private searchService: SearchService) {}

  @Get()
  async qidir(@Query() dto: SearchDto) {              // ?q=telefon&kategoriya=...&minNarx=...
    return this.searchService.qidir(dto);
  }

  @Get("suggest")                                     // avtomatik to'ldirish (instant)
  async takliflar(@Query("q") q: string) {
    const natija = await this.searchService.qidir({ q, limit: 5 } as any);
    return natija.natijalar.map((h: any) => h.nom);   // tez takliflar
  }
}

class SearchDto {
  @IsString() @IsOptional() q: string = "";
  @IsOptional() kategoriya?: string;
  @IsOptional() brend?: string;
  @Type(() => Number) @IsOptional() minNarx?: number;
  @Type(() => Number) @IsOptional() maxNarx?: number;
  @IsOptional() sort?: string;
  @Type(() => Number) @IsOptional() sahifa?: number = 1;
  @Type(() => Number) @IsOptional() limit?: number = 20;
}

Misol 6 — Elasticsearch (murakkab relevance — 2.7, 2.8)

typescript
@Injectable()
export class ElasticSearchService {
  private es = new Client({ node: this.config.get("ES_NODE") });

  async qidir(query: string, filter: any) {
    const result = await this.es.search({
      index: "mahsulotlar",
      query: {
        bool: {
          must: [{
            multi_match: {
              query,
              fields: ["nom^3", "brend^2", "tavsif"],   // nom 3x, brend 2x muhim (2.8)
              fuzziness: "AUTO",                         // typo tolerance
            },
          }],
          filter: [
            ...(filter.kategoriya ? [{ term: { kategoriya: filter.kategoriya } }] : []),
            ...(filter.minNarx ? [{ range: { narx: { gte: filter.minNarx } } }] : []),
          ],
        },
      },
      // Maxsus relevance (mashhurlikni hisobga olish)
      sort: [{ _score: "desc" }, { sotuvlar: "desc" }],
      highlight: { fields: { nom: {}, tavsif: {} } },
      size: 20,
    });
    return result.hits.hits;
  }
}

Misol 7 — Boshlang'ich indekslash (migration — 2.5)

typescript
// Mavjud DB ma'lumotini birinchi marta indekslash (deploy'da bir marta)
@Injectable()
export class SearchSeedService {
  async boshlangichIndeks() {
    const jami = await this.repo.count();
    this.logger.log(`${jami} mahsulot indekslanmoqda...`);

    const batch = 1000;
    for (let offset = 0; offset < jami; offset += batch) {
      const mahsulotlar = await this.repo.find({ skip: offset, take: batch });
      await this.searchService.indekslaBatch(mahsulotlar);   // ommaviy (tez)
      this.logger.log(`${offset + mahsulotlar.length}/${jami}`);
    }
    this.logger.log("Boshlang'ich indekslash tugadi");
  }
}
// CLI buyruq / deploy script orqali bir marta ishga tushiriladi

Misol 8 — Sinonim va sozlash (2.8)

typescript
// Sinonimlar (o'zbek-rus aralash qidiruv — O'zbekiston konteksti)
await this.client.index("mahsulotlar").updateSettings({
  synonyms: {
    "telefon": ["smartfon", "mobil", "ayfon"],
    "noutbuk": ["laptop", "kompyuter"],
    "muzlatgich": ["xolodilnik"],                     // rus-o'zbek
  },
  stopWords: ["va", "yoki", "uchun", "bilan"],        // ahamiyatsiz so'zlar
});
//  "ayfon" qidiruvi "telefon"larni ham topadi (sinonim)

Misol 9 — Pagination va natija (katta — 2.6)

typescript
// Katta natija — sahifalash (offset yoki keyset)
async qidirSahifa(q: string, sahifa: number, limit = 20) {
  const natija = await this.client.index("mahsulotlar").search(q, {
    limit,
    offset: (sahifa - 1) * limit,
  });
  return {
    natijalar: natija.hits,
    jami: natija.estimatedTotalHits,
    sahifa,
    jamiSahifa: Math.ceil(natija.estimatedTotalHits / limit),
    qidiruvVaqti: natija.processingTimeMs,            // millisekund (tezlik)
  };
}

Misol 10 — Search modul (to'liq — arxitektura)

text
  src/search/
  ├── search.module.ts          (MeiliSearch klient, BullMQ navbat)
  ├── search.service.ts         (qidiruv + indekslash — Misol 1, 2)
  ├── search.controller.ts      (qidiruv API — Misol 5)
  ├── search-index.processor.ts (navbat worker — Misol 3)
  ├── reindex.service.ts        (davriy + boshlang'ich — Misol 4, 7)
  └── dto/search.dto.ts

  Oqim:
  ProductsService 8.3-bob yarat/yangila/ochir  navbat  SearchIndexProcessor  Meilisearch
  Frontend (11)  GET /search?q=...  SearchService.qidir  Meilisearch  natija + facet
  Cron 8.18-bob  davriy reindex (zaxira)

   DB = source of truth; Meilisearch = qidiruv nusxasi (qayta tiklanadigan)

Misol 11 — Elasticsearch to'liq oqim (@nestjs/elasticsearch — 2.11, 2.12, 2.13)

typescript
// Modul 2.11-bob klientni bergan holda — mapping + indekslash + query DSL qidiruv
@Injectable()
export class ElasticSearchService implements OnModuleInit {
  private readonly INDEX = "mahsulotlar";
  private readonly logger = new Logger(ElasticSearchService.name);

  constructor(private readonly es: ElasticsearchService) {}   // DI (2.11)

  async onModuleInit() {
    // Indeks + mapping (bir marta — 2.12)
    const bor = await this.es.indices.exists({ index: this.INDEX });
    if (!bor) {
      await this.es.indices.create({
        index: this.INDEX,
        mappings: {
          properties: {
            nom:        { type: "text" },
            tavsif:     { type: "text" },
            kategoriya: { type: "keyword" },            // filtr/facet — aniq mos
            brend:      { type: "keyword" },
            narx:       { type: "float" },
            sotuvlar:   { type: "integer" },
            faol:       { type: "boolean" },
          },
        },
      });
      this.logger.log("Indeks yaratildi (mapping bilan)");
    }
  }

  // Indekslash (id — ustiga yozadi, ya'ni yangilash ham shu)
  async indeksla(m: any) {
    await this.es.index({ index: this.INDEX, id: m.id, document: m, refresh: true });
  }
  async ochir(id: string) {
    await this.es.delete({ index: this.INDEX, id }).catch(() => undefined);  // yo'q bo'lsa e'tiborsiz
  }

  // Qidiruv — bool + fuzzy + filter + facet (aggregation) + highlight (2.13)
  async qidir(dto: { q: string; kategoriya?: string; minNarx?: number; maxNarx?: number }) {
    const filter: any[] = [{ term: { faol: true } }];
    if (dto.kategoriya) filter.push({ term: { kategoriya: dto.kategoriya } });
    if (dto.minNarx != null || dto.maxNarx != null) {
      filter.push({ range: { narx: { gte: dto.minNarx ?? 0, lte: dto.maxNarx ?? 1e12 } } });
    }

    const res = await this.es.search({
      index: this.INDEX,
      query: {
        bool: {
          must: [{
            multi_match: {
              query: dto.q,
              fields: ["nom^3", "brend^2", "tavsif"],   // vazn (2.8)
              fuzziness: "AUTO",                         // typo-bardosh (2.13)
            },
          }],
          filter,                                        // skorsiz, keshlanadi (tez — 2.13)
        },
      },
      aggs: {                                            // facet = aggregation (2.6 ES ekvivalenti)
        kategoriyalar: { terms: { field: "kategoriya" } },
      },
      highlight: { fields: { nom: {}, tavsif: {} }, pre_tags: ["<mark>"], post_tags: ["</mark>"] },
      from: 0, size: 20,
    });

    return {
      natijalar: res.hits.hits.map((h) => ({ ...(h._source as object), _skor: h._score, _highlight: h.highlight })),
      jami: typeof res.hits.total === "number" ? res.hits.total : res.hits.total?.value,
      facet: (res.aggregations?.kategoriyalar as any)?.buckets,   // [{ key, doc_count }]
    };
  }
}

Elasticsearch'da facet = aggregation (aggs + terms): Meilisearch'dagi facetDistribution 2.6-bob ekvivalenti. highlight — mos so'z <mark>, from/size — pagination. Bu misol 2.11–2.13'ni bitta amaliy servisda birlashtiradi (mapping indekslash murakkab qidiruv).

Misol 12 — Tanlash jadvali (ES vs Meilisearch vs PostgreSQL — 2.2, 2.14)

text
  ┌────────────────┬──────────────┬──────────────┬────────────────┐
  │ Xususiyat      │ Meilisearch  │ Elasticsearch│ PG full-text   │
  ├────────────────┼──────────────┼──────────────┼────────────────┤
  │ Sozlash        │ Oson (zero)  │ Murakkab     │ O'rtacha (DB)  │
  │ Typo tolerance │ Default    │ fuzziness    │ Yo'q (pg_trgm) │
  │ Relevance      │ Avto (yaxshi)│ To'liq (DSL) │ ts_rank        │
  │ Faceting       │ Oson         │ aggregation  │ Cheklangan     │
  │ Instant search │  (tez)     │ Sozlash bilan│               │
  │ Analytics/log  │             │  (ELK)     │               │
  │ Hajm/masshtab  │ O'rta-katta  │ Juda katta   │ DB'ga bog'liq  │
  │ Qo'shimcha     │ Alohida      │ Alohida+og'ir│ Yo'q (DB ichi) │
  │ tizim          │ tizim        │ (klaster)    │                │
  ├────────────────┼──────────────┼──────────────┼────────────────┤
  │ QACHON         │ Ko'p loyiha  │ Katta/murak. │ Ma'lumot PG'da │
  │                │ (default)    │ /analytics   │ o'rta talab    │
  └────────────────┴──────────────┴──────────────┴────────────────┘

Tanlov qoidasi: ma'lumot allaqachon PostgreSQL'da va qidiruv talabi o'rtacha PG full-text (2.14 — qo'shimcha tizim yo'q). Sifatli, xato-bardosh, instant qidiruv (e-commerce, kontent) Meilisearch (default tanlov — oson + kuchli). Juda katta hajm, murakkab relevance, log/analytics (ELK) Elasticsearch. "Elasticsearch = qidiruv" degan odat noto'g'ri — ko'p loyiha uchun u ortiqcha (over-engineering — 2.2, 5.3-holat).


5. To'g'ri va noto'g'ri holatlar

1) Katta qidiruvga DB LIKE

text
 LIKE '%matn%' (sekin, xatoni kechirmaydi — 2.1)
 Meilisearch/Elasticsearch (tez, typo tolerance)

2) Indekslash sinxron emas

text
 DB o'zgardi, qidiruv eski (nomutanosib — 2.9)
 navbat + davriy reindex

3) Kichik loyihaga Elasticsearch

text
 100 yozuvga ES klaster (over-engineering — 2.2)
 DB / Meilisearch (oddiy)

4) Qidiruvni source of truth qilish

text
 faqat qidiruvda saqlash (yo'qolsa — ma'lumot ketadi — 2.9)
 DB asosiy, qidiruv nusxa

5) API key ochiq

text
 master key frontendda (14)
 backend'da; frontend uchun search-only key

6. Keng tarqalgan xatolar va yechimlari

Xato 1 — Qidiruv natija bermaydi

Sababi: searchableAttributes sozlanmagan, yoki indekslanmagan (2.4, 2.5). Yechimi: settings; addDocuments.

Xato 2 — Filtr ishlamaydi

Sababi: filterableAttributes yo'q 2.4-bob. Yechimi: updateSettings filterable.

Xato 3 — DB va qidiruv mos emas

Sababi: sinxron yo'q 2.9-bob. Yechimi: navbat + reindex.

Xato 4 — Qidiruv sekin (ES)

Sababi: mapping/shard noto'g'ri. Yechimi: mapping; Meilisearch (oddiyroq).

Xato 5 — Typo tolerance ishlamaydi

Sababi: o'chirilgan yoki qisqa so'z. Yechimi: typoTolerance enabled; min so'z uzunligi.

Xato 6 — Reindex xotira

Sababi: hammasi birda (8.21: 2.7). Yechimi: batch (Misol 4, 7).


7. Integratsiya — bu mavzu stack'ning qayerida uchraydi

  • DB qidiruv (6.3, 6.7): text index, tsvector — PG full-text (oraliq yechim — 2.14).
  • Navbat 8.22-bob: indekslash.
  • Cron 8.18-bob: davriy reindex.
  • DB 8.3-bob: source of truth.
  • Config 8.14-bob: API key.
  • Frontend (11): qidiruv UI (instant search).
  • Xavfsizlik (14): API key, search-only key.
  • Event 8.16-bob: indekslash trigger.

8. Eng yaxshi amaliyotlar (best practices)

  • DB qidiruvi yetsa — uni ishlat (kichik — 2.1); Meilisearch (oddiy)/ES (murakkab) 2.2-bob.
  • searchable/filterable/sortable sozlash 2.4-bob.
  • Indekslash navbat (ishonchli — 2.9) + davriy reindex.
  • DB = source of truth; qidiruv = nusxa 2.9-bob.
  • Faceting (filtr + ko'rsatkich — 2.6); highlighting (UX).
  • Relevance vazn (nom>tavsif — 2.8); sinonim (O'zbekiston — Misol 8).
  • API key xavfsizlik (.env, search-only — 14).
  • Pagination (katta natija — 2.6); batch reindex (xotira — Misol 4).
  • Boshlang'ich indekslash (mavjud ma'lumot — Misol 7).

9. Amaliy loyiha: "Kuchli Qidiruv Tizimi"

Qidiruvni amalda mustahkamlash.

Maqsad

E-commerce'ga kuchli qidiruv: Meilisearch, indekslash (navbat), filtr/facet, sinonim, davriy reindex.

Talablar (requirements)

  1. Search service: Meilisearch + settings (Misol 1, 2.4).
  2. Indekslash: navbat orqali (yarat/yangila/ochir — Misol 3, 2.9).
  3. Qidiruv: filtr + facet + highlight (Misol 2, 2.6).
  4. Davriy reindex: cron + batch (Misol 4, 2.9).
  5. Boshlang'ich indeks: mavjud ma'lumot (Misol 7, 2.5).
  6. Controller: qidiruv + suggest (Misol 5).
  7. Sinonim: o'zbek-rus (Misol 8, 2.8).
  8. Pagination: katta natija (Misol 9, 2.6).
  9. Relevance: maydon vazni 2.8-bob.
  10. Xavfsizlik: API key, search-only 2.10-bob.

Maslahatlar (hint)

  • searchable/filterable sozlash (2.4, 1-xato, 2-xato).
  • Indekslash navbat (2.9, 3-xato).
  • DB source of truth (2.9, 4-holat).
  • Batch reindex (Misol 4, 6-xato).
  • Sinonim O'zbekiston (Misol 8).

"Tayyor" mezonlari (acceptance criteria)

  • Search service (settings).
  • Indekslash (navbat).
  • Qidiruv (filtr/facet/highlight).
  • Davriy reindex.
  • Boshlang'ich indeks.
  • Controller (suggest).
  • Sinonim.
  • Pagination.
  • Relevance vazn.
  • Xavfsizlik.

Yechim kodi ataylab berilmagan — bu loyihani o'zingiz yozib ko'ring.


10. Xulosa va keyingi bobga ko'prik

Bu bobda kuchli qidiruvni o'rgandik:

  • DB qidiruvi muammosi 2.1-bob; tizimlar (Meilisearch/Elasticsearch — qachon qaysi — 2.2); tushunchalar (index, relevance, typo tolerance, faceting — 2.3).
  • Meilisearch (integratsiya, sozlash — 2.4); indekslash (DBqidiruv sinxron — 2.5, 2.9); qidiruv + facet 2.6-bob.
  • Elasticsearch (query DSL — 2.7; @nestjs/elasticsearch — 2.11; mapping — 2.12; match/bool/fuzzy/wildcard — 2.13); relevance (ahamiyat, vazn — 2.8); strategiyalar (navbat + reindex — 2.9).
  • Tanlash (Meilisearch vs Elasticsearch vs PostgreSQL full-text — 2.14, Misol 12): ko'p loyiha uchun Meilisearch, ma'lumot PG'da bo'lsa tsvector, juda katta/analytics uchun Elasticsearch.

Keyingi bob — 8.23: Push bildirishnoma — Firebase FCM. Qidiruvni bildik; endi yana bir real mavzu — push bildirishnoma (brauzer/mobil telefonga real-time xabar — FCM) — ni o'rganamiz. Buyurtma holati, aksiya, eslatma — push orqali.


Foydalanilgan rasmiy/ishonchli manbalar

  • OSSAlt / Meilisearch — Meilisearch vs Typesense vs Elasticsearch 2026
  • meilisearch.com/docs (settings, search, faceting, typo tolerance)
  • elastic.co — Elasticsearch query DSL (bool, multi_match, function_score)

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
8.22-bob: Kuchli qidiruv — Elasticsearch, Meilisearch — Wisar