Բոլոր դասերը
Էլեկտրոնային առևտուրՄիջին10 րոպե ընթերցումԻմ ստեկում

Ապրանքների որոնում

LIKE query-ներից մինչև որոնման շարժիչներ՝ տառասխալների հանդուրժում, facet-ներ, հոմանիշներ, ranking և ինդեքսի թարմություն։

  • Algolia
  • Scout
  • Elasticsearch
  • Meilisearch
  • Typo tolerance
Քայլ 1/5
Search engine-ը պահում է inverted index՝ ինչպես գրքի վերջի ցանկը․ ամեն բառ ցույց է տալիս այն փաստաթղթերը, որտեղ հանդիպում է։

30 վայրկյանում

Որոնումը հաճախ վաճառքի ամենաարագ ճանապարհն է․ որոնող գնորդները սովորաբար գիտեն, թե ինչ են ուզում։ Տվյալների բազայի պարզ LIKE query-ն աշխատում է մի քանի հարյուր ապրանքի համար, բայց իրական խանութներին պետք է տառասխալների հանդուրժում, ֆիլտրեր՝ քանակներով, հոմանիշներ և խելացի ranking։ Հենց դա են տալիս full-text որոնումը և առանձին որոնման շարժիչները՝ ձեր տվյալների երկրորդ պատճենի գնով, որը պետք է սինքրոն մնա։

Ինչու եմ օգտագործում

  • Ես օգտագործում եմ առանձին որոնման շարժիչ, որովհետև կենդանիների ապրանքների խանութի համար ապրանքների որոնումը կառուցել եմ Algolia-ի վրա Laravel Scout-ի միջոցով՝ ծածկելով ապրանքները, տարբերակները, կատեգորիաները, բրենդները և հրապարակումները։
  • Հաճախորդները սխալ են գրում, օգտագործում տարբեր բառեր և ակնկալում ակնթարթային արդյունքներ մուտքագրելու ընթացքում։ Տվյալների բազայի պարզ query-ն դա լավ չի կարող անել։
  • Որոնման log-երը ոսկու հանք են․ դրանք ցույց են տալիս, թե ինչ են ուզում մարդիկ և որ որոնումները ոչինչ չեն վերադարձնում։
  • Որոնումը դիպչում է կատալոգին, մնացորդին, ակցիաներին և արագագործությանը, ուստի այն լավ թեստ է՝ արդյոք պլատֆորմի տվյալների հոսքը մաքուր է։

Ինչպես է աշխատում

01Երեք մակարդակ՝ LIKE, full-text, որոնման շարժիչ

Առաջին մակարդակը տվյալների բազայի LIKE query-ն է՝ WHERE name LIKE '%dog food%'։ Այն հեշտ է, բայց սկզբի wildcard-ը չի կարող օգտագործել սովորական ինդեքս, ուստի scan է անում աղյուսակը, և ոչ մի պատկերացում չունի տառասխալների, բառերի հերթականության կամ համապատասխանության մասին։ Երկրորդ մակարդակը տվյալների բազայի full-text որոնումն է՝ MySQL FULLTEXT ինդեքսներ կամ PostgreSQL tsvector՝ GIN ինդեքսներով։ Դրանք տեքստը բաժանում են բառերի, կատարում stemming («running»-ը համընկնում է «run»-ի հետ) և դասավորում ըստ համապատասխանության․ բավական լավ է բլոգների և փոքր կատալոգների համար՝ առանց լրացուցիչ ծառայության։ PostgreSQL-ի pg_trgm ընդլայնումն ավելացնում է fuzzy համընկնում։ Երրորդ մակարդակը առանձին որոնման շարժիչն է, որը կառուցում է inverted index՝ օպտիմալացված ակնթարթային, տառասխալներ հանդուրժող, facet-ներով որոնման համար՝ կարգավորելի ranking-ով։ Մակարդակով բարձրացեք միայն այն ժամանակ, երբ նախորդն այլևս չի սպասարկում օգտատերերին։

sql
-- PostgreSQL full-text search with ranking
ALTER TABLE products ADD COLUMN search tsvector
  GENERATED ALWAYS AS (
    setweight(to_tsvector('english', coalesce(name, '')), 'A') ||
    setweight(to_tsvector('english', coalesce(description, '')), 'B')
  ) STORED;
 
CREATE INDEX products_search_idx ON products USING GIN (search);
 
SELECT id, name, ts_rank(search, query) AS rank
FROM products, websearch_to_tsquery('english', 'dry dog food') AS query
WHERE search @@ query
ORDER BY rank DESC
LIMIT 20;

02Ինչպես է աշխատում որոնման շարժիչը

Որոնման շարժիչը պահում է inverted index․ գրքի վերջում գտնվող ցուցիչի պես այն յուրաքանչյուր բառ կապում է այն փաստաթղթերի ցուցակի հետ, որոնք պարունակում են այն։ Երբ ապրանք եք ինդեքսավորում, շարժիչը տեքստը tokenise է անում (բաժանում բառերի), normalise անում (փոքրատառ, հեռացնում շեշտանշանները) և կարող է կիրառել stemming կամ լեզվական կանոններ։ Query-ի պահին այն որոնում է յուրաքանչյուր բառը, թույլ է տալիս փոքր քանակությամբ տառասխալներ՝ սովորաբար մեկը կարճ բառերի և երկուսը ավելի երկարների համար, չափված որպես edit distance, համընկեցնում է prefix-ները, որպեսզի արդյունքները հայտնվեն մուտքագրելու ընթացքում, և դասավորում է արդյունքները։ Ranking-ը սովորաբար համակցում է տեքստային համապատասխանությունը (քանի բառ է համընկնում, որքան մոտ, որ դաշտում) ձեր կարգավորած բիզնես ազդանշանների հետ, օրինակ՝ հայտնիություն, մնացորդ կամ մարժա։ Facet-ները քանակներ են ըստ ֆիլտրի արժեքի («Բրենդ՝ 12, Չափս՝ 5»), որոնք հաշվարկվում են արդյունքների հետ միասին։

03Շարժիչի ընտրություն

Algolia-ն hosted ծառայություն է՝ հիանալի արագությամբ, տառասխալների հանդուրժմամբ, merchandising գործիքներով և analytics-ով․ վճարում եք ըստ որոնման հարցումների և գրառումների, ինչը կարող է թանկանալ։ Meilisearch-ը և Typesense-ը open-source շարժիչներ են, որոնք կարող եք self-host անել փոքր սերվերի վրա կամ օգտագործել որպես cloud ծառայություններ․ երկուսն էլ արագ են, տառասխալներ հանդուրժող և պարզ կարգավորվող, իդեալական խանութների մեծ մասի համար։ Elasticsearch-ը և OpenSearch-ը հզոր ընդհանուր նշանակության շարժիչներ են հսկա կատալոգների, բարդ query-ների, ագրեգացիաների և log-երի համար, բայց պահանջում են ավելի շատ հիշողություն և շահագործման փորձ։ Laravel-ում Scout-ը տալիս է մեկ API Algolia-ի, Meilisearch-ի, Typesense-ի և database driver-ի վրա, այնպես որ կարող եք սկսել պարզից և հետագայում փոխել շարժիչը։

  • Փոքր կատալոգ, ևս մեկ ծառայության բյուջե չկա՝ տվյալների բազայի full-text։
  • Խանութների մեծ մասը՝ Meilisearch կամ Typesense, self-hosted կամ cloud։
  • Merchandising և analytics առանց ops-ի՝ Algolia։
  • Հսկա տվյալներ, բարդ ագրեգացիաներ կամ արդեն գոյություն ունեցող cluster՝ Elasticsearch / OpenSearch։

04Ինդեքսի նախագծում

Որոնման ինդեքսը ձեր տվյալների բազայի պատճենը չէ․ այն հարթեցված փաստաթուղթ է՝ նախագծված մեկ էկրանի համար։ Խանութի համար որոշեք, թե ինչ է արդյունքը՝ սովորաբար ապրանք, որի մեջ միաձուլված են տարբերակների ատրիբուտները (բոլոր գույներն ու չափսերը), որպեսզի ֆիլտրերն աշխատեն, և ամենացածր հասանելի գինը՝ դասավորման համար։ Ներառեք միայն այն, ինչ պետք է որոնմանը՝ անուն, բրենդ, կատեգորիայի ճանապարհ, ատրիբուտներ, բանալի բառեր, գին, մնացորդի դրոշակ, հայտնիություն և նկարի URL։ Հայտարարեք, թե որ դաշտերն են որոնելի (առաջնահերթության կարգով), որոնք՝ ֆիլտրելի (facet-ներ) և որոնք՝ դասավորելի։ Օգտագործեք հոմանիշներ այն բառերի համար, որոնք հաճախորդները տարբեր կերպ են օգտագործում («kitten» և «cat», «nail polish» և «lacquer»), և stop word-եր՝ աղմուկի համար։ Կատեգորիաների, բրենդների և հոդվածների առանձին ինդեքսները թույլ են տալիս առաջարկների հարուստ dropdown։

php
// Laravel Scout: what goes into the search document
class Product extends Model
{
    use \Laravel\Scout\Searchable;
    public function toSearchableArray(): array
    {
        return [
            'id'         => $this->id,
            'name'       => $this->name,
            'brand'      => $this->brand?->name,
            'categories' => $this->categories->pluck('name')->all(),
            'colours'    => $this->variants->pluck('colour')->filter()->unique()->values()->all(),
            'price_min'  => $this->variants->min('price'),       // integer minor units
            'in_stock'   => $this->variants->contains(fn ($v) => $v->available > 0),
            'popularity' => $this->sales_last_30_days,
        ];
    }
 
    public function shouldBeSearchable(): bool { return $this->is_active; }
}

05Ինդեքսի սինքրոնիզացիա

Ինդեքսը ձեր տվյալների երկրորդ պատճենն է, ուստի այն կարող է հնանալ։ Թարմացրեք այն, երբ տվյալները փոխվում են․ Scout-ը դա անում է ավտոմատ մոդելի save-ի և delete-ի ժամանակ, բայց աշխատանքն ուղարկեք queue, որպեսզի ապրանքը պահելը երբեք չսպասի որոնման ծառայությանը։ Զգույշ եղեք այն փոփոխությունների հետ, որոնք շրջանցում են մոդելի event-ները՝ զանգվածային SQL update-ներ, ERP import-ներ, մնացորդի փոփոխություններ այլ աղյուսակում կամ բրենդի վերանվանում, որն ազդում է հազարավոր ապրանքների վրա, և ազդված գրառումների համար բացահայտ գործարկեք reindex։ Պարբերաբար պլանավորեք ամբողջական reindex, իդեալում՝ նոր ինդեքսի մեջ, ապա փոխարինեք այն alias-ով, որպեսզի օգտատերերը երբեք կիսակառույց ինդեքս չտեսնեն։ Մնացորդն ու գինը հաճախ են փոխվում․ մտածեք միայն այդ դաշտերը batch-երով թարմացնելու մասին՝ ամբողջ փաստաթղթերի փոխարեն։

  • Ինդեքսի թարմացումները queue արեք․ զանգվածային import-ների համար batch արեք դրանք։
  • Վերակառուցեք նոր ինդեքսի մեջ և ատոմար փոխարինեք։
  • Alert ուղարկեք, երբ ինդեքսավորված փաստաթղթերի քանակը շատ է տարբերվում տվյալների բազայից։

06Ranking, analytics և production-ի խորհուրդներ

Լավ ranking-ը կարգավորվում է, ոչ թե գուշակվում։ Սկսեք շարժիչի default-ներից, որոնելի դաշտերը դրեք ըստ կարգի (անունը՝ նկարագրությունից առաջ) և ավելացրեք custom ranking ազդանշան, օրինակ՝ հայտնիություն կամ վաճառքներ։ Պահեստում չեղած ապրանքները իջեցրեք ներքև՝ թաքցնելու փոխարեն, եթե բիզնեսն այլ բան չի նախընտրում։ Log արեք որոնումները՝ query-ն, արդյունքների քանակը և click-երը։ Զրո արդյունքով որոնումների ցուցակը ցույց է տալիս բացակայող հոմանիշներ, ապրանքներ և ուղղագրական խնդիրներ, իսկ հայտնի որոնումները կարող են սնել առաջարկները։ Պաշտպանեք որոնման endpoint-ը rate limiting-ով և երբեք մի բացահայտեք admin API key-երը բրաուզերին․ օգտագործեք search-only key-եր, իդեալում՝ սահմանափակ scope-ով կամ ժամկետով։ Client-ում search-as-you-type-ը debounce արեք մոտ 150–300 ms-ով և cache արեք հայտնի query-ները։

Օգտագործեք, երբ

  • Հազարավոր ապրանքներից բաղկացած կատալոգներ կամ ցանկացած խանութ, որտեղ հաճախորդներն ավելի շատ որոնում են, քան թերթում։
  • Ձեզ պետք է տառասխալների հանդուրժում, ակնթարթային արդյունքներ, facet-ներ՝ քանակներով և merchandising-ի վերահսկողություն։
  • Կոնտենտն ընդգրկում է մի քանի տեսակ՝ ապրանքներ, կատեգորիաներ, բրենդներ, հոդվածներ, մեկ որոնման դաշտում։

Մի՛ օգտագործեք, երբ

  • Մի քանի տասնյակ կամ հարյուր ապրանք՝ տվյալների բազայի full-text որոնումը կամ նույնիսկ LIKE-ը բավարար է։
  • Երբ թիմը չի կարող շահագործել կամ վճարել ևս մեկ ծառայության համար, և տվյալների բազան բավարարում է ընթացիկ կարիքները։
  • Մի օգտագործեք որոնման շարժիչը որպես ձեր հիմնական տվյալների բազա կամ ճշմարտության աղբյուր։

Այլընտրանքներ

  • Database full-text (MySQL FULLTEXT, PostgreSQL tsvector + pg_trgm)

    Լրացուցիչ ծառայություն չկա, transactional համաձայնեցվածություն․ ավելի թույլ տառասխալների հանդուրժում և facet-ներ։

  • Meilisearch / Typesense

    Open-source, արագ, տառասխալներ հանդուրժող, հեշտ է self-host անել․ հիանալի default խանութների համար։

  • Algolia

    Hosted, շատ արագ, ուժեղ merchandising և analytics․ գնագոյացում՝ ըստ օգտագործման։

  • Elasticsearch / OpenSearch

    Ամենահզորն ու ճկունը․ ավելի ծանր է շահագործել և կարգավորել։

Լավագույն պրակտիկաներ

  1. Սկսեք ամենապարզ աշխատող մակարդակից և բարձրացեք միայն այն ժամանակ, երբ օգտատերերին դա պետք է։
  2. Ինդեքսի փաստաթղթերը նախագծեք արդյունքների էկրանի համար՝ հարթեցված, նվազագույն, բացահայտ որոնելի, ֆիլտրելի և դասավորելի դաշտերով։
  3. Ինդեքսի թարմացումները queue արեք և մշակեք մոդելի event-ները շրջանցող զանգվածային փոփոխությունները։
  4. Ինդեքսները վերակառուցեք background-ում և ատոմար փոխարինեք։
  5. Սպասարկեք հոմանիշները և պարբերաբար վերանայեք զրո արդյունքով որոնումները։
  6. Բրաուզերում օգտագործեք search-only API key-եր և rate-limit արեք որոնման endpoint-ները։
  7. Տեքստային համապատասխանությունը համակցեք բիզնես ազդանշանների հետ, ինչպիսիք են հայտնիությունն ու մնացորդը։

Տարածված սխալներ

  • LIKE '%term%' օգտագործել մեծ աղյուսակի վրա և զարմանալ, թե ինչու է որոնումը դանդաղ։
  • Տվյալների բազայի ամբողջ տողերը, ներառյալ private դաշտերը, ինդեքսավորել հանրային ինդեքսում։
  • Մոռանալ reindex անել զանգվածային import-ներից հետո, այնպես որ որոնումը ցույց է տալիս հին գներ կամ ջնջված ապրանքներ։
  • Admin API key-ը բացահայտել frontend-ի կոդում։
  • Երբեք չնայել որոնման log-երին՝ բաց թողնելով կոնվերսիայի ամենահեշտ հաղթանակները։

Խորը վերլուծություն

07Relevance-ի ներքին կառուցվածքը. BM25 և ranking rule-եր

Ընդհանուր նշանակության engine-ները, ինչպիսիք են Elasticsearch-ը և OpenSearch-ը, default-ով document-ները գնահատում են BM25-ով։ Պարզ բառերով՝ document-ն ավելի բարձր score է ստանում, երբ հաճախ է պարունակում query-ի բառերը (նվազող արդյունավետությամբ, այնպես որ բառը քսան անգամ կրկնելը շատ չի օգնում), երբ բառերը հազվադեպ են ամբողջ index-ում («merino»-ն ավելի շատ է կշռում, քան «the»-ն), և երբ դաշտը կարճ է (հինգ բառանոց վերնագրում համընկնումը գերազանցում է երկար նկարագրության մեջ եղածին)։ Field boost-երը բազմապատկում են այդ score-ը։ Instant search-ի համար ստեղծված engine-ները, ինչպիսիք են Meilisearch-ը, Typesense-ը և Algolia-ն, փոխարենը օգտագործում են ranking rule-երի կամ tie-breaker-ների դասավորված ցուցակ. համընկած բառերի քանակ, typo-ներ, մոտիկություն, ատրիբուտների հերթականություն, ճշգրտություն, ապա ձեր custom ranking-ը։ Հասկանալը, թե որ մոդելն է օգտագործում ձեր engine-ը, ասում է, թե ինչ կարգավորել. boost-եր և ֆունկցիաներ BM25 engine-ներում, rule-երի հերթականություն և custom ատրիբուտներ մյուսներում։

08Zero-downtime reindexing alias-ներով և swap-երով

Index-ի կարգավորումների փոփոխությունը՝ նոր searchable ատրիբուտներ, այլ tokeniser, նոր լեզու, հաճախ պահանջում է ամբողջ index-ի վերակառուցում։ Երբեք դա տեղում մի արեք, այլապես օգտատերերը րոպեներով կտեսնեն կիսալցված արդյունքներ։ Կառուցեք նոր index live-ի կողքին, լցրեք այն batch-երով, ստուգեք document-ների քանակը և գործարկեք մի քանի smoke query, ապա traffic-ը փոխեք մեկ ատոմար քայլով։ Elasticsearch-ը և OpenSearch-ը դա անում են alias-ներով. application-ը միշտ query է անում «products» alias-ը, և alias-ի update-ը այն products_v1-ից տեղափոխում է products_v2։ Typesense-ն ունի collection alias-ներ, որոնք աշխատում են նույն կերպ։ Meilisearch-ն առաջարկում է index swap գործողություն, որը ատոմար կերպով փոխանակում է երկու index-ների բովանդակությունը։ Հին index-ը պահեք կարճ ժամանակ, որպեսզի կարողանաք ակնթարթորեն վերադառնալ, եթե ինչ-որ բան սխալ է թվում։

bash
# Meilisearch: swap the freshly built index with the live one
curl -X POST 'http://localhost:7700/swap-indexes' \
  -H 'Authorization: Bearer MEILI_ADMIN_KEY' \
  -H 'Content-Type: application/json' \
  --data '[{ "indexes": ["products", "products_new"] }]'
 
# Elasticsearch / OpenSearch: move an alias atomically
curl -X POST 'http://localhost:9200/_aliases' -H 'Content-Type: application/json' -d '{
  "actions": [
    { "remove": { "index": "products_v1", "alias": "products" } },
    { "add":    { "index": "products_v2", "alias": "products" } }
  ]
}'

09Անվտանգ search key-եր և ֆիլտրում ըստ օգտատիրոջ

Browser-ի կողմից որոնումը արագ է, որովհետև client-ը engine-ը կանչում է անմիջապես, բայց դա նշանակում է, որ key-ը տեսանելի է բոլորին։ Օգտագործեք search-only key, երբեք՝ admin key-ը։ Երբ արդյունքները պետք է տարբեր լինեն ըստ օգտատիրոջ՝ B2B գնացուցակներ, private կատալոգներ, tenant-ի սեփական տվյալներ, մի հենվեք client-ի վրա ֆիլտր ավելացնելու համար, որովհետև այն կարող է պարզապես հեռացնել այն։ Փոխարենը սերվերը գեներացնում է սահմանափակված key, որն իր մեջ պարունակում է ֆիլտրը և ժամկետը. Algolia-ն դրանք անվանում է secured API key-եր, Typesense-ը՝ scoped API key-եր, իսկ Meilisearch-ը՝ tenant token-ներ, որոնք ստորագրված JWT-ներ են։ Engine-ը ներդրված ֆիլտրը կիրառում է յուրաքանչյուր query-ի վրա։ Index արեք նաև միայն հանրային դաշտերը. index-ում եղած ամեն ինչ կարող է կարդալ համառ օգտատերը։ Rate limit դրեք ըստ key-ի կամ IP-ի և սահմանեք կարճ ժամկետ, որպեսզի արտահոսած key-երը արագ դադարեն աշխատել։

ts
// Meilisearch tenant token: the filter is enforced by the engine
import { generateTenantToken } from "meilisearch/token";
 
export async function searchTokenFor(customer: { priceListId: number }) {
  return generateTenantToken({
    apiKey: process.env.MEILI_SEARCH_KEY!,       // a search-only key
    apiKeyUid: process.env.MEILI_SEARCH_KEY_UID!,
    searchRules: { products: { filter: `price_list = ${customer.priceListId}` } },
    expiresAt: new Date(Date.now() + 60 * 60 * 1000), // 1 hour
  });
}

10Բազմալեզու որոնում և լեզվական եզրային դեպքեր

Որոնման որակը կախված է նրանից, թե ինչպես է տեքստը բաժանվում և normalize արվում, և դա կախված է լեզվից։ Անգլերենի stemming-ը «boots»-ը map է անում «boot»-ին, ռուսերենն ու հայերենը ունեն հարուստ վերջավորություններ, որոնք պահանջում են լեզվին համապատասխան մշակում կամ prefix matching, իսկ չինարենին և ճապոներենին պետք է segmentation, որովհետև բառերը բացատներով բաժանված չեն։ Հանդիպում են նաև խառը գրեր. հաճախորդները brand-երի անունները գրում են լատինատառ կամ տառադարձում են տեղական բառերը։ Գործնական մոտեցում. յուրաքանչյուր լեզու պահեք իր դաշտում կամ index-ում, engine-ի լեզվի կամ locale-ի կարգավորումները կարգավորեք բացահայտ՝ ավտոմատ հայտնաբերման վրա հենվելու փոխարեն, ավելացրեք synonym-ներ տառադարձությունների միջև («krem»-ը և cream բառի կիրիլյան գրությունը) և normalize արեք շեշտերը։ Թեստավորեք log-երից վերցված իրական query-ներով յուրաքանչյուր լեզվով, որովհետև կարգավորումը, որը լավ է աշխատում անգլերենով, կարող է լուռ ձախողվել մեկ այլ լեզվով։

  • Մեկ searchable դաշտ յուրաքանչյուր լեզվի համար, օրինակ՝ name_en և name_ru։
  • Նախ query արեք օգտատիրոջ interface-ի լեզվի index-ը։
  • SKU-ները և barcode-ները դիտարկեք որպես exact-match դաշտեր՝ անջատված typo tolerance-ով։

Իրական իրավիճակներ

01

Ապրանքի կոդերով որոնումները անհեթեթություն են վերադարձնում

Խնդիր

Աշխատակիցները և B2B հաճախորդները որոնում են SKU-ով կամ barcode-ով, օրինակ՝ AB-1042, բայց արդյունքներում նման անուններով ապրանքներ են, իսկ ճշգրիտ ապրանքը հաճախ երկրորդ էջում է։

Ինչպես լուծել

Typo tolerance-ը և tokenisation-ը աշխատում են ձեր դեմ. engine-ը կոդը բաժանում է մասերի և թույլ է տալիս typo, ուստի AB-1043-ը նույնպես համընկնում է։ Կոդերը index արեք առանձին դաշտում, նշեք այն որպես exact կամ անջատեք typo tolerance-ը դրա վրա (engine-ների մեծ մասն աջակցում է typo-ների անջատումը ըստ ատրիբուտի) և դրեք այն searchable ատրիբուտների ցուցակում առաջինը, որպեսզի կոդի ճշգրիտ համընկնումը հաղթի։ Application-ում regex-ով հայտնաբերեք կոդի տեսք ունեցող query-ները և նախ գործարկեք exact filter query, իսկ հետո fallback արեք սովորական որոնմանը։ Ավելացրեք և՛ հում կոդը, և՛ դրա normalize արված տարբերակը՝ առանց գծիկների և բացատների, որպեսզի աշխատեն և՛ AB1042-ը, և՛ AB-1042-ը։

02

Որոնումը ցույց է տալիս երեկ ջնջված ապրանքներ

Խնդիր

Հաճախորդները սեղմում են որոնման արդյունքներ, որոնք տանում են 404 էջեր։ Կատալոգը մաքրվել էր bulk SQL script-ով, որը 3,000 ապրանքի համար is_active-ը դարձրել էր false։

Ինչպես լուծել

Bulk update-ը շրջանցել է model event-ները, ուստի search index-ը դրա մասին երբեք չի իմացել։ Անմիջական խնդիրը լուծեք՝ reindex անելով համապատասխան ապրանքները, կամ ամբողջական import արեք նոր index և swap արեք այն։ Ապա փակեք բացը. bulk փոփոխությունները կատարեք կոդի միջոցով, որը dispatch է անում reindex job-եր համապատասխան ID-ների համար, կամ chunk-երով գործարկեք Scout-ի searchable կամ unsearchable մեթոդները query builder-ների վրա։ Ավելացրեք գիշերային drift ստուգում, որը համեմատում է database-ում և index-ում ակտիվ ապրանքների քանակն ու ID-ները և alert է տալիս փոքր շեմից վեր։ Որպես պաշտպանության վերջին գիծ՝ ապրանքի էջը պետք է ոչ ակտիվ ապրանքները redirect անի իրենց կատեգորիա՝ մերկ 404 վերադարձնելու փոխարեն։

03

Algolia-ի ծախսերը աճում են վաճառքից ավելի արագ

Խնդիր

Mobile app-ում search-as-you-type գործարկելուց հետո որոնման ամսական հաշիվը կրկնապատկվեց, մինչդեռ պատվերներն աճեցին ընդամենը 10%-ով։

Ինչպես լուծել

Յուրաքանչյուր keystroke վճարովի որոնում է։ Մուտքագրումը debounce արեք մոտ 200 ms, պահանջեք առնվազն երկու նիշ և չեղարկեք ընթացքի մեջ գտնվող հարցումները, երբ օգտատերը շարունակում է գրել։ Նույնական query-ները կարճ ժամանակով cache արեք client-ում և ձեր edge-ում, իսկ հանրաճանաչ empty-state առաջարկները մատուցեք նախապես հաշվարկված ցուցակից՝ query-ի փոխարեն։ Ստուգեք չնախատեսված traffic-ը. bot-եր, prefetching և re-render-ներից կրկնվող հարցումներ։ Եթե ծախսերը արժեքի համեմատ մնում են բարձր, գնահատեք self-hosted Meilisearch կամ Typesense. երբ Laravel Scout-ը interface-ի հետևում է, migration-ը հիմնականում կոնֆիգուրացիա է գումարած ranking-ի վերակարգավորում, և որակը կարող եք համեմատել log արված query-ներով։

Արագ ուղեցույց

Բոլոր գրառումների import index (Scout)
php artisan scout:import "App\Models\Product"
Բոլոր գրառումների հեռացում index-ից (Scout)
php artisan scout:flush "App\Models\Product"
Index-ի կարգավորումների push config-ից (Scout)
php artisan scout:sync-index-settings
Scout update-ները queue-ով
SCOUT_QUEUE=true # in .env; index writes go through the queue
Reindex bulk փոփոխությունից հետո (Scout)
Product::where('brand_id', $id)->searchable();
Index-ների swap (Meilisearch)
POST /swap-indexes [{ "indexes": ["products", "products_new"] }]
Alias-ի ուղղում նոր collection-ի (Typesense)
PUT /aliases/products { "collection_name": "products_v2" }
Fuzzy match PostgreSQL-ում
CREATE EXTENSION pg_trgm; CREATE INDEX ON products USING GIN (name gin_trgm_ops);
Որոնման դաշտի debounce
setTimeout(() => search(q), 200) // clear the previous timer on each keystroke

Իմ իրական նախագծերում

  • Կենդանիների ապրանքների պլատֆորմում կառուցեցի որոնում Algolia-ի վրա Laravel Scout-ի միջոցով՝ ինդեքսավորելով ապրանքները, տարբերակները, կատեգորիաները, բրենդները և հրապարակումները, առաջարկներով, հայտնի որոնումներով, հոմանիշների sync-ով, որոնման log-երով և rate limiting-ով։
  • Նույն պլատֆորմի analytics-ը ներառում է որոնման հաշվետվություններ, իսկ ranking-ի և ասոցիացիաների job-երը սնում են առաջարկությունները, «կրկին գնել» և «վերջերս դիտված» բլոկները։

Հարցեր և պատասխաններ

Q1Ինչո՞ւ պարզապես LIKE չօգտագործել որոնման համար։

Սկզբի wildcard-ով LIKE-ը չի կարող օգտագործել սովորական B-tree ինդեքս, ուստի տվյալների բազան scan է անում յուրաքանչյուր տող, ինչը դանդաղում է կատալոգի աճին զուգընթաց։ Ավելի կարևոր է, որ այն ոչ մի պատկերացում չունի լեզվի մասին՝ ոչ տառասխալների հանդուրժում, ոչ stemming, ոչ համապատասխանության ranking, ոչ հոմանիշներ, ոչ facet-ներ։ Այն նորմալ է փոքր աղյուսակի վրա ադմինի ֆիլտրի համար։ Հաճախորդներին ուղղված որոնման համար հաջորդ քայլը տվյալների բազայի full-text որոնումն է, իսկ առանձին շարժիչը՝ երբ ձեզ պետք են ակնթարթային, տառասխալներ հանդուրժող, facet-ներով արդյունքներ։

Q2Ի՞նչ է inverted index-ը։

Այն որոնման շարժիչների հիմնական տվյալների կառուցվածքն է։ Փաստաթղթերը պահելու և scan անելու փոխարեն շարժիչը պահում է map՝ յուրաքանչյուր տերմինից դեպի այն պարունակող փաստաթղթերի ցուցակ, հաճախ՝ դիրքերով և հաճախականություններով։ Query-ն որոնում է յուրաքանչյուր տերմինի ցուցակը և հատում կամ միաձուլում դրանք, ինչը չափազանց արագ է։ Տեքստը tokenise և normalise է արվում մինչև ինդեքսավորումը, այնպես որ բառի տարբեր ձևերը կապվում են նույն տերմինին։ Տառասխալների հանդուրժումն ու prefix որոնումն աշխատում են տերմինների բառարանի վրա։

Q3Ինչպե՞ս եք որոնման ինդեքսը սինքրոն պահում տվյալների բազայի հետ։

Incremental թարմացումներ յուրաքանչյուր create-ի, update-ի և delete-ի ժամանակ՝ ուղարկված queue-ի միջոցով, որպեսզի հիմնական հարցումը մնա արագ։ Laravel Scout-ը դա անում է մոդելի event-ների միջոցով։ Event-ները շրջանցող փոփոխությունները՝ զանգվածային SQL, ERP import-ներ, կապված մոդելների վերանվանումներ, պետք է բացահայտ գործարկեն reindex։ Դրա վրա պլանավորված ամբողջական reindex-ը background-ում կառուցում է թարմ ինդեքս և ատոմար փոխարինում այն alias-ի միջոցով։ Մոնիտորինգը համեմատում է փաստաթղթերի քանակները տվյալների բազայի հետ՝ շեղումները բռնելու համար։

Q4Algolia, Meilisearch, Typesense, թե՞ Elasticsearch։

Algolia՝ երբ հաճախորդն ուզում է hosted ծառայություն ուժեղ merchandising-ով և analytics-ով և ընդունում է ըստ օգտագործման գնագոյացումը։ Meilisearch կամ Typesense՝ խանութների մեծ մասի համար․ open source, արագ, տառասխալներ հանդուրժող, հեշտ կարգավորվող և էժան՝ self-host անելու կամ դրանց cloud-ում օգտագործելու համար։ Elasticsearch կամ OpenSearch՝ երբ տվյալները շատ մեծ են, query-ներն ու ագրեգացիաները բարդ են, կամ ընկերությունն արդեն cluster է շահագործում։ Laravel Scout-ի նման աբստրակցիայով առաջին երեքի միջև անցումը հիմնականում կոնֆիգուրացիա է։

Q5Ի՞նչ են facet-ները և ինչպե՞ս են դրանք աշխատում։

Facet-ները որոնման արդյունքների կողքին գտնվող ֆիլտրերն են՝ քանակներով, օրինակ՝ «Բրենդ՝ Acme (12)» կամ «Չափս՝ Large (5)»։ Դուք հայտարարում եք, թե որ դաշտերն են ֆիլտրելի, և շարժիչը հաշվարկում է յուրաքանչյուր արժեքի քանակը ընթացիկ արդյունքների բազմության մեջ։ Արժեք ընտրելը ֆիլտրում է արդյունքները և թարմացնում մյուս քանակները։ Խանութի համար տարբերակների ատրիբուտները սովորաբար միաձուլվում են ապրանքի փաստաթղթի մեջ, որպեսզի ապրանքը հայտնվի իր բոլոր գույների և չափսերի տակ։ Disjunctive facet-ները մեկն ընտրելուց հետո տեսանելի են պահում մյուս արժեքների քանակները։

Q6Ինչպե՞ս եք ժամանակի ընթացքում բարելավում որոնման որակը։

Տվյալներով։ Log արեք յուրաքանչյուր query, դրա արդյունքների քանակը և click-երը։ Զրո արդյունքով որոնումները վերանայեք ամեն շաբաթ և ուղղեք դրանք հոմանիշներով, բացակայող ապրանքներով կամ ավելի լավ ատրիբուտներով։ Նայեք այն query-ներին, որոնք արդյունքներ ունեն, բայց click չունեն․ դրանք վատ ranking-ի նշան են։ Կարգավորեք որոնելի դաշտերի հերթականությունը, ավելացրեք բիզնես ranking ազդանշաններ, օրինակ՝ հայտնիություն, և պահեստում չեղած ապրանքները իջեցրեք ներքև։ Հայտնի որոնումները կարող են սնել առաջարկները։ Փոքր, կանոնավոր կարգավորումը գերազանցում է մեծ, միանգամյա կոնֆիգուրացիային։

Սենիոր մակարդակի հարցեր

S1Ինչպե՞ս կնախագծեիք որոնում միլիոնավոր SKU-ներով և յուրաքանչյուր հաճախորդի համար տարբեր գներով կատալոգի համար։

Ես կառանձնացնեի այն, ինչը դանդաղ է փոխվում, այն ամենից, ինչը փոխվում է ըստ հաճախորդի։ Ապրանքի բովանդակությունը, կատեգորիաները և ատրիբուտները index են մտնում որպես document-ներ՝ մեկը յուրաքանչյուր ապրանքի համար, տարբերակների ատրիբուտները միավորված facet-երի համար։ Յուրաքանչյուր հաճախորդի գները չպետք է բազմապատկեն document-ները. փոխարենը index արեք գնացուցակների ID-ները կամ գնային tier-երը, որոնք կարելի է ֆիլտրել, և հաճախորդի ֆիլտրը կիրառեք secured key-ի կամ tenant token-ի միջոցով։ Ճշգրիտ գները կարելի է վերցնել pricing service-ից արդյունքների տեսանելի էջի համար։ Մնացորդի և գների update-ները batch-ային partial update-ներ են։ Ես shard կանեի ըստ engine-ի ուղեցույցի, կչափեի p95 latency-ն բեռի տակ և կպահեի ամբողջական rebuild-ի ընթացակարգ alias-ներով։ Trade-off-ը յուրաքանչյուր էջի համար լրացուցիչ pricing կանչն է՝ շատ ավելի փոքր և էժան index-ի դիմաց։

S2Ինչպե՞ս եք օբյեկտիվորեն գնահատում որոնման որակը փոփոխությունից առաջ և հետո։

Որոնման մասին կարծիքները անվստահելի են, ուստի ես օգտագործում եմ տվյալներ։ Offline. log-երից կառուցեք իրական query-ների judgement set՝ այն ապրանքներով, որոնք պետք է բարձր դասակարգվեն, և հաշվեք այնպիսի metric-ներ, ինչպիսիք են precision-ը առաջին մի քանի արդյունքներում կամ NDCG-ն, որը պարգևատրում է վերևում գտնվող համապատասխան արդյունքները. գործարկեք այն կոնֆիգուրացիայի յուրաքանչյուր փոփոխությունից առաջ և հետո։ Online. հետևեք zero-result rate-ին, click-through rate-ին, առաջին click-ի դիրքին, որոնումից դուրս գալուն և որոնումից conversion-ին, իդեալական դեպքում՝ կոնֆիգուրացիաների միջև A/B test-ում։ Համատեղեք երկուսը, որովհետև offline set-երը էժան են բռնում regression-ները, իսկ online metric-ները չափում են իրական վարքագիծը։ Trade-off-ը ջանքն է. judgement set-ը կառուցելու համար պետք են մի քանի օր, բայց այն կանխում է այնպիսի tuning, որը բարելավում է մեկ query և լուռ կոտրում հիսուն ուրիշը։

S3Ե՞րբ կպահեիք որոնումը PostgreSQL-ի ներսում՝ engine ավելացնելու փոխարեն։

Երբ կատալոգը համեստ է, թիմը փոքր է, և transaction-ային թարմությունը կարևոր է։ PostgreSQL full-text search-ը tsvector-ով, GIN index-ներով և ranking-ով, գումարած pg_trgm-ը fuzzy matching-ի համար, լավ է ծածկում տասնյակ կամ հարյուրավոր հազարավոր ապրանքներ՝ առանց sync pipeline-ի, առանց տվյալների երկրորդ պատճենի և permission-ներով, որոնք կիրառվում են նույն query-ներով։ Դրա թույլ կողմերն են instant, typo-tolerant prefix search-ը, հարուստ faceting-ը մեծ մասշտաբով, բազմալեզու tuning-ը և merchandising գործիքները։ Ես անցնում եմ առանձին engine-ի, երբ այդ սահմանները երևում են իրական metric-ներում՝ դանդաղ facet query-ներ, վատ zero-result rate-եր, marketing-ին պետք է ranking-ի վերահսկում, և ոչ թե default-ով։ Trade-off-ը օպերացիոն է. առանձին engine-ը ավելացնում է ենթակառուցվածք և sync-ի բարդություն, բայց տալիս է որոնման շատ ավելի լավ UX։

Բառարան

Inverted index
Map յուրաքանչյուր բառից դեպի այն պարունակող document-ները՝ search engine-ների հիմնական կառուցվածքը։
Tokenisation
Տեքստի բաժանում որոնելի միավորների, սովորաբար՝ բառերի։
Stemming
Բառերի բերում ընդհանուր արմատի, որպեսզի «running»-ը և «run»-ը համընկնեն։
BM25
Relevance-ի ստանդարտ բանաձև, որը կշռում է բառի հաճախականությունը, բառի հազվադեպությունը և դաշտի երկարությունը։
Edit distance
Երկու բառերի միջև մեկ նիշի փոփոխությունների քանակը. օգտագործվում է typo-ները չափելու համար։
Facet
Ֆիլտր՝ յուրաքանչյուր արժեքի համար արդյունքների քանակով, օրինակ՝ brand կամ չափս։
Synonym
Բառեր, որոնք կարգավորված են միմյանց հետ համընկնելու համար, օրինակ՝ «kitten» և «cat»։
Index alias
Կայուն անուն, որը ցույց է տալիս իրական index-ի վրա և կարող է ատոմար կերպով փոխվել։
Tenant token
Ստորագրված, ժամկետով search key՝ ներդրված ֆիլտրերով, որոնք կիրառում է engine-ը։

Պրոդուկտի գաղափար ունե՞ք։Եկեք ստեղծենք այն։

Պատմեք ձեր գաղափարի, պրոդուկտի կամ թիմի մասին։ Սովորաբար պատասխանում եմ մեկ աշխատանքային օրվա ընթացքում։