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)
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, sinonimDB 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
┌──────────────┬─────────────────────────────────────────────┐
│ 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
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)
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:
MeiliSearchklient (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)
// 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 deleteDocumentIndekslash (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)
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)
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)
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)
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)
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).
npm install @nestjs/elasticsearch @elastic/elasticsearch// 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 {}// 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(yokiregister) — klientni DI'ga ulaydi (ConfigServicebilan.envdan node/auth).ElasticsearchService'ni konstruktorda inyeksiya qilasiz (private es: ElasticsearchService) — u@elastic/elasticsearchClient'ni o'rab,es.index,es.search,es.indicesmetodlarini beradi. Bu — NestJS uslubi (klientni qo'ldanewqilmaysiz, 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).
// 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) —rangefiltr. Analyzer — matnni tokenlarga bo'lish qoidasi (lowercase,asciifolding— "cafe"="café"). Noto'g'ri mapping = noto'g'ri qidiruv (masalan,kategoriya'nitextqilsangiz 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:
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)// 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(yokimatch'dafuzziness: "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 qidiruvinimust+match'ga, filtrlarni (narx/kategoriya)filter'ga qo'ying —filterkeshlanadi va skor hisoblamaydi (tezroq). Meilisearch bularni bitta oddiysearch()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).
-- 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// 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_trgmbilan 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
// 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(...) DESC4. Batafsil kod namunalari
Misol 1 — Search service (Meilisearch — 2.4, 2.5)
@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)
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)
// 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)
@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)
@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)
@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)
// 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 tushiriladiMisol 8 — Sinonim va sozlash (2.8)
// 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)
// 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)
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)
// 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'dagifacetDistribution2.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)
┌────────────────┬──────────────┬──────────────┬────────────────┐
│ 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
LIKE '%matn%' (sekin, xatoni kechirmaydi — 2.1)
Meilisearch/Elasticsearch (tez, typo tolerance)2) Indekslash sinxron emas
DB o'zgardi, qidiruv eski (nomutanosib — 2.9)
navbat + davriy reindex3) Kichik loyihaga Elasticsearch
100 yozuvga ES klaster (over-engineering — 2.2)
DB / Meilisearch (oddiy)4) Qidiruvni source of truth qilish
faqat qidiruvda saqlash (yo'qolsa — ma'lumot ketadi — 2.9)
DB asosiy, qidiruv nusxa5) API key ochiq
master key frontendda (14)
backend'da; frontend uchun search-only key6. 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)
- Search service: Meilisearch + settings (Misol 1, 2.4).
- Indekslash: navbat orqali (yarat/yangila/ochir — Misol 3, 2.9).
- Qidiruv: filtr + facet + highlight (Misol 2, 2.6).
- Davriy reindex: cron + batch (Misol 4, 2.9).
- Boshlang'ich indeks: mavjud ma'lumot (Misol 7, 2.5).
- Controller: qidiruv + suggest (Misol 5).
- Sinonim: o'zbek-rus (Misol 8, 2.8).
- Pagination: katta natija (Misol 9, 2.6).
- Relevance: maydon vazni 2.8-bob.
- 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!