دليل تقني

هندسة بحث التجارة الإلكترونية: MeiliSearch و OpenSearch وقصص ترحيل حقيقية

كيف تصمم بحث المنتجات للتجارة الإلكترونية. MeiliSearch مقابل OpenSearch مقابل Elasticsearch، تصميم الفهرس، البحث بالفلاتر، استراتيجيات متعددة اللغات، البحث الهجين، والمزامنة الفورية من أنظمة PIM والتجارة.

3 فبراير 202618 دقيقة للقراءةفريق هندسة أورنتس

ليش بحث المنتجات مش بحث نصي عادي

بحث المنتجات يبان بسيط. المستخدم يكتب "حذاء جري أزرق مقاس 42" ويتوقع نتائج مناسبة. لكن التطبيق الفعلي مختلف جذرياً عن بحث المستندات أو بحث الويب. المنتجات عندها خصائص منظمة (مقاس، لون، سعر، علامة تجارية)، تصنيفات هرمية، توفر يتغير بالوقت الفعلي، أسماء وأوصاف مترجمة، وفلاتر المستخدم يتوقع يقدر يفلتر فيها.

محرك بحث المستندات يلاقي مستندات تطابق الاستعلام. محرك بحث المنتجات لازم يلاقي المنتجات، يرتبها حسب الأهمية التجارية (مش بس تطابق النص)، يعرض فلاتر قابلة للتصفية، يتعامل مع الأخطاء الإملائية والمرادفات، يدعم لغات متعددة، ويتحدث بالوقت الفعلي لما يتغير المخزون.

بنينا أنظمة بحث منتجات على MeiliSearch و OpenSearch، في حالة رحّلنا من Elasticsearch 7.4 وفي حالة ثانية بنينا من الصفر. هالمقال يغطي قرارات الهندسة المعمارية، مش تفاصيل الإعدادات. لأنماط البحث المتجهي بالتحديد، شوف دليل هندسة البحث المتجهي. للسياق التجاري الأوسع، شوف دليل منصات التجارة الإلكترونية.

MeiliSearch مقابل OpenSearch مقابل Elasticsearch

المعيارMeiliSearchOpenSearchElasticsearch
اللغةRustJavaJava
تحمل الأخطاء الإملائيةمدمج، ممتازإضافة/مخصصإضافة/مخصص
بحث بالفلاترمدمج، سريعمدمج (aggregations)مدمج (aggregations)
بحث متجهيتجريبيمدمج (k-NN)مدمج (dense_vector)
تعدد اللغاتجيد (tokenizers حسب اللغة)ممتاز (analyzers لكل حقل)ممتاز (analyzers لكل حقل)
الفهرسة الفوريةشبه فورية (< 50ms)شبه فورية (تحديث كل 1 ثانية)شبه فورية (تحديث كل 1 ثانية)
التعقيدمنخفض (ملف تنفيذي واحد، REST API)عالي (cluster، shards، replicas)عالي (cluster، shards، replicas)
استخدام الذاكرةمنخفض (Rust، كفاءة عالية)عالي (JVM heap)عالي (JVM heap)
التكلفة التشغيليةمنخفضة (يشتغل على سيرفرات صغيرة)متوسطة لعاليةمتوسطة لعالية
الترتيبقواعد ترتيب مدمجةترتيب مرنترتيب مرن
الرخصةMITApache 2.0SSPL (مش مفتوحة المصدر)
الأفضل لـكتالوجات صغيرة لمتوسطة (< 500 ألف منتج)كتالوجات كبيرة، استعلامات معقدة، بحث هجيننفس OpenSearch (إذا عندك استثمار موجود)

متى تختار MeiliSearch

  • الكتالوج أقل من 500 ألف منتج
  • تحمل الأخطاء الإملائية مهم جداً (بحث موجه للمستهلك)
  • الفريق عنده خبرة محدودة في بنية البحث التحتية
  • الإعداد السريع أهم من ميزات الاستعلام المتقدمة
  • الميزانية محدودة (يشتغل على سيرفر صغير واحد)

متى تختار OpenSearch

  • الكتالوج أكثر من 100 ألف منتج مع فلاتر معقدة
  • تحتاج بحث هجين (نصي + متجهي / k-NN)
  • مجموعات مستهلكين متعددة تشتغل على نفس الفهرس
  • أصلاً على AWS (OpenSearch Serverless مُدار)
  • تحتاج aggregations متقدمة وتحليلات على بيانات البحث

متى تختار Elasticsearch

  • أصلاً تشغل Elasticsearch وما في سبب للترحيل
  • تحتاج ميزات خاصة بـ Elastic (ML inference، الأمان)
  • عقد دعم مؤسسي مطلوب

لأغلب مشاريع التجارة الجديدة، ننصح بـ MeiliSearch للبساطة أو OpenSearch للقوة. رخصة SSPL حقت Elasticsearch تخليها أقل جاذبية للنشر الجديد.

تصميم الفهرس

أكثر غلطة شائعة: تفهرس مخطط قاعدة البيانات مباشرة. جداول المنتجات مُطبّعة (normalized). فهارس البحث لازم تكون غير مُطبّعة (denormalized).

// قاعدة البيانات: مُطبّعة (علائقية)
// products table: id, name, category_id, brand_id
// categories table: id, name, parent_id
// variants table: id, product_id, sku, price, size, color
// translations table: id, product_id, locale, name, description

// فهرس البحث: غير مُطبّع (مستند مسطح)
interface ProductSearchDocument {
    id: string;
    name: string;                    // اللغة الحالية
    description: string;             // اللغة الحالية
    slug: string;
    sku: string[];                   // كل SKUs المتغيرات
    brand: string;                   // غير مطبع من جدول العلامة التجارية
    categories: string[];            // التسلسل الهرمي الكامل: ["Shoes", "Running", "Trail"]
    categoryIds: string[];           // لفلترة الفلاتر
    price: number;                   // أقل سعر متغير (للترتيب)
    priceRange: { min: number; max: number };
    sizes: string[];                 // كل المقاسات المتاحة
    colors: string[];                // كل الألوان المتاحة
    inStock: boolean;                // أي متغير متوفر
    imageUrl: string;                // الصورة الرئيسية
    rating: number;                  // متوسط تقييم المراجعات
    reviewCount: number;
    tags: string[];                  // وسوم قابلة للبحث
    createdAt: number;               // لترتيب "الأحدث أولاً"
    popularity: number;              // عدد المبيعات أو المشاهدات
}

قواعد إلغاء التطبيع:

  • سطّح كل العلاقات في المستند (اسم العلامة التجارية، مش معرف العلامة التجارية)
  • ضمّن التسلسل الهرمي الكامل للتصنيف كمصفوفة (يسمح بالتنقل بين مستويات الفلاتر)
  • ضمّن كل خصائص المتغيرات (المقاسات، الألوان) كمصفوفات على المنتج
  • استخدم أقل سعر للترتيب، ونطاق السعر للعرض
  • ضمّن الحقول المحسوبة (التقييم، عدد المراجعات، الشعبية) للترتيب
  • مستند واحد لكل منتج لكل لغة (مش مستند واحد فيه كل اللغات)

فهرس واحد لكل لغة

للتجارة متعددة اللغات، أنشئ فهرس واحد لكل لغة:

products_en
products_de
products_fr
products_ar

كل فهرس يستخدم analyzers و tokenizers و stop words خاصة باللغة. بحث ألماني عن "Laufschuhe" يستخدم stemming ألماني. بحث عربي يستخدم تحليل مورفولوجي عربي. خلط اللغات في فهرس واحد يجبرك على تنازلات في التحليل تضعف الجودة لكل اللغات.

// MeiliSearch: فهرس واحد لكل لغة
await meili.createIndex('products_de', { primaryKey: 'id' });
await meili.index('products_de').updateSettings({
    searchableAttributes: ['name', 'description', 'brand', 'tags', 'categories'],
    filterableAttributes: ['categories', 'brand', 'sizes', 'colors', 'price', 'inStock'],
    sortableAttributes: ['price', 'createdAt', 'popularity', 'rating'],
});

هندسة البحث بالفلاتر

الفلاتر هي الفلاتر اللي على يسار كل صفحة بحث تجارة إلكترونية. تبان بسيطة لكن تحتاج تصميم دقيق.

أنواع الفلاتر

النوعمثالالتطبيق
فلتر مصطلحالعلامة: Nike (42)، Adidas (38)Term aggregation على حقل brand
فلتر نطاقالسعر: 0-50 (15)، 50-100 (28)، 100+ (12)Range aggregation على حقل price
فلتر هرميالتصنيف: أحذية > جري > تريلTerm aggregation متعدد المستويات على التسلسل الهرمي
فلتر بوليانمتوفر: نعم (89)، لا (11)Term aggregation على حقل inStock
فلتر لونعينات ألوان مع الأعدادTerm aggregation على حقل مصفوفة colors
فلتر مقاسالمقاس: 40 (5)، 41 (8)، 42 (12)Term aggregation على حقل مصفوفة sizes

تفاعل الفلاتر

لما المستخدم يختار فلتر، الفلاتر الثانية لازم تتحدث لتعكس النتائج المُفلترة. هذا اسمه "تنقيح الفلاتر" وهو أكثر جزء معقد في واجهة البحث.

// MeiliSearch: أعداد الفلاتر مع فلاتر نشطة
const results = await meili.index('products_de').search('laufschuhe', {
    filter: ['brand = "Nike"', 'inStock = true'],
    facets: ['categories', 'brand', 'sizes', 'colors', 'price'],
});

// results.facetDistribution:
// {
//   categories: { "Running": 42, "Trail": 18, "Road": 24 },
//   brand: { "Nike": 42 },  // بس Nike (لأنه مُفلتر)
//   sizes: { "40": 5, "41": 8, "42": 12, "43": 10, "44": 7 },
//   colors: { "Black": 20, "White": 15, "Blue": 7 },
// }

قرار تجربة المستخدم الرئيسي: لما فلتر العلامة التجارية نشط، هل يعرض فلتر العلامة بس العلامة المختارة (مع العدد) أو كل العلامات (بأعداد تعكس الاستعلام الحالي بدون فلتر العلامة)؟ الطريقة الثانية ("فلترة منفصلة") تخلي المستخدمين يقارنون الأعداد بين العلامات. MeiliSearch يدعم هذا بشكل أصلي. OpenSearch يحتاج استعلامات aggregation منفصلة لكل فلتر منفصل.

المزامنة الفورية من الأنظمة المصدرية

فهارس البحث لازم تبقى متزامنة مع مصدر الحقيقة (PIM، قاعدة بيانات التجارة، ERP). هندسة المزامنة تعتمد على النظام المصدري.

المزامنة المبنية على الأحداث (الموصى بها)

النظام المصدري يرسل أحداث عند تغيير البيانات. عامل يستهلك الأحداث ويحدث فهرس البحث.

// Vendure: مزامنة على أحداث المنتجات
@Injectable()
export class SearchIndexSubscriber {
    constructor(
        private eventBus: EventBus,
        private searchService: SearchIndexService,
    ) {
        this.eventBus.ofType(ProductEvent).subscribe(async event => {
            if (event.type === 'updated' || event.type === 'created') {
                await this.searchService.indexProduct(event.ctx, event.product.id);
            }
            if (event.type === 'deleted') {
                await this.searchService.removeProduct(event.product.id);
            }
        });

        this.eventBus.ofType(ProductVariantEvent).subscribe(async event => {
            // تغيير المتغير يأثر على مستند البحث للمنتج الأب
            await this.searchService.indexProduct(event.ctx, event.productVariant.productId);
        });
    }
}

إعادة فهرسة مجدولة كاملة

حتى مع المزامنة المبنية على الأحداث، شغّل إعادة فهرسة كاملة مجدولة كشبكة أمان. الأحداث ممكن تضيع (توقف الوسيط، تعطل العامل). إعادة فهرسة ليلية تلتقط أي شي المزامنة المبنية على الأحداث فاتته.

// وظيفة إعادة فهرسة ليلية كاملة
async function fullReindex(locale: string) {
    const batchSize = 500;
    let offset = 0;
    let products = [];

    do {
        products = await productService.findAll({ take: batchSize, skip: offset });
        const documents = products.map(p => buildSearchDocument(p, locale));
        await meili.index(`products_${locale}`).addDocuments(documents);
        offset += batchSize;
    } while (products.length === batchSize);
}

التعامل مع الحذف

حذف المنتجات موضوع حساس. إذا حذفت منتج من قاعدة البيانات، المزامنة المبنية على الأحداث تشيله من الفهرس. لكن إذا ضاع الحدث، المنتج المحذوف يبقى في نتائج البحث.

حلين:

  1. تتبع أوقات الحذف وفلترة بـ "ما محذوف" في الاستعلامات
  2. إعادة الفهرسة الكاملة تستبدل الفهرس بالكامل بشكل ذري (تبديل الاسم المستعار)
// إعادة فهرسة ذرية بتبديل الاسم المستعار (OpenSearch/Elasticsearch)
async function atomicReindex(locale: string) {
    const newIndex = `products_${locale}_${Date.now()}`;
    await opensearch.indices.create({ index: newIndex, body: indexSettings });

    // فهرسة كل المنتجات في الفهرس الجديد
    await bulkIndex(newIndex, locale);

    // تبديل الاسم المستعار بشكل ذري
    await opensearch.indices.updateAliases({
        body: {
            actions: [
                { remove: { index: `products_${locale}_*`, alias: `products_${locale}` } },
                { add: { index: newIndex, alias: `products_${locale}` } },
            ],
        },
    });

    // حذف الفهارس القديمة
    await cleanupOldIndices(`products_${locale}_*`, keepLast: 2);
}

لكيف نتعامل مع خطوط أنابيب مزامنة البيانات على نطاق واسع، إضافة Vendure Data Hub حقتنا تطبق كل هالأنماط مع 7 أنظمة بحث مختلفة كأهداف.

ضبط الصلة

صلة البحث الافتراضية غلط للتجارة. صلة النص (قد إيش الاستعلام يطابق المستند) إشارة واحدة. الصلة التجارية (قد إيش المستخدم احتمال يشتري) بنفس الأهمية.

إشارات الترتيب

الإشارةالوزنالمصدر
تطابق النص (العنوان)عاليمحرك البحث
تطابق النص (الوصف)متوسطمحرك البحث
متوفرحرج (تعزيز أو فلتر)نظام المخزون
الشعبية (عدد المبيعات)متوسطبيانات الطلبات
تقييم المراجعاتمنخفض-متوسطالمراجعات
الحداثة (منتجات جديدة)منخفضتاريخ إنشاء المنتج
الهامش (داخلي)اختياريقواعد العمل
// MeiliSearch: قواعد ترتيب مخصصة
await meili.index('products_de').updateSettings({
    rankingRules: [
        'words',           // 1. جودة تطابق النص
        'typo',            // 2. تحمل الأخطاء الإملائية
        'proximity',       // 3. قرب الكلمات
        'attribute',       // 4. أي حقل طابق (العنوان > الوصف)
        'sort',            // 5. الترتيب المطلوب من المستخدم
        'exactness',       // 6. تطابق دقيق مقابل جزئي
        'popularity:desc', // 7. المنتجات الشائعة ترتب أعلى
        'rating:desc',     // 8. المنتجات الأعلى تقييماً ترتب أعلى
    ],
});

تعزيز المنتجات المتوفرة

المنتجات غير المتوفرة لازم تظهر أقل في النتائج، مش تختفي نهائياً. المستخدمين ممكن يبون يشوفون منتجات قادمة أو يشتركون بإشعارات عودة التوفر.

// OpenSearch: تعزيز المنتجات المتوفرة
const query = {
    bool: {
        must: [{ match: { searchText: userQuery } }],
        should: [
            { term: { inStock: { value: true, boost: 5.0 } } }, // تعزيز قوي للمتوفر
        ],
        filter: [
            { term: { tenant_id: tenantId } },
        ],
    },
};

البحث الهجين للتجارة

دمج البحث النصي مع البحث المتجهي يحسن النتائج للاستعلامات باللغة الطبيعية مع الحفاظ على قدرة التطابق الدقيق لأكواد SKU وأكواد المنتجات.

// OpenSearch: بحث هجين (نصي + متجهي)
const results = await opensearch.search({
    index: 'products_en',
    body: {
        query: {
            bool: {
                should: [
                    // بحث نصي (يتعامل مع SKUs، أسماء المنتجات الدقيقة)
                    { multi_match: { query: userQuery, fields: ['name^3', 'description', 'sku^5', 'tags'], type: 'best_fields' } },
                    // بحث متجهي (يتعامل مع اللغة الطبيعية، التشابه الدلالي)
                    { knn: { embedding: { vector: queryEmbedding, k: 20 } } },
                ],
            },
        },
        // الفلاتر
        aggs: {
            brands: { terms: { field: 'brand.keyword', size: 20 } },
            categories: { terms: { field: 'categories.keyword', size: 30 } },
            price_ranges: { range: { field: 'price', ranges: [{ to: 50 }, { from: 50, to: 100 }, { from: 100 }] } },
        },
    },
});

استعلامات SKU ("ABC-12345") تمر بمسار البحث النصي بدقة عالية. استعلامات اللغة الطبيعية ("أحذية مريحة للمشي الطويل") تمر بمسار البحث المتجهي مع فهم دلالي. كلاهما يساهم في الترتيب النهائي.

لمزيد من التفاصيل عن آليات البحث المتجهي الداخلية، شوف دليل هندسة البحث المتجهي.

الأخطاء الشائعة

  1. فهرسة بيانات مُطبّعة. مستندات البحث حقتك لازم تكون غير مُطبّعة. سطّح كل العلاقات في المستند. لا ترجع لمعرفات تحتاج بحث ثاني.

  2. فهرس واحد لكل اللغات. أنشئ فهرس واحد لكل لغة. الفهارس المختلطة ما تقدر تستخدم analyzers خاصة باللغة، وجودة البحث تتراجع لكل اللغات.

  3. ما في تصميم للفلاتر. الفلاتر مش شي ثانوي. خطط أي خصائص قابلة للفلترة، كيف التصنيفات الهرمية تشتغل، وكيف أعداد الفلاتر تتحدث لما الفلاتر تتطبق.

  4. المزامنة بس عبر إعادة فهرسة مجدولة. المزامنة المبنية على الأحداث تعطيك تحديثات شبه فورية. إعادة الفهرسة المجدولة شبكة أمان، مش الآلية الأساسية.

  5. ما في ضبط للصلة. صلة النص الافتراضية غلط للتجارة. عزّز المنتجات المتوفرة، ادمج الشعبية والتقييمات، واعطي تطابقات العنوان وزن أعلى من تطابقات الوصف.

  6. تجاهل المنتجات غير المتوفرة. لا تشيلهم من الفهرس. خفّض ترتيبهم. المستخدمين ممكن يبون إشعارات عودة التوفر أو يتصفحون المنتجات القادمة.

  7. ما في إعادة فهرسة ذرية. إذا عملية إعادة الفهرسة تفشل بالنص، عندك فهرس محدث جزئياً. استخدم تبديل الأسماء المستعارة للتبديل الذري.

  8. التعامل مع البحث كميزة، مش بنية تحتية. البحث خدمة أساسية. يحتاج كلستر خاص فيه، مراقبة خاصة فيه، واستراتيجية توسع خاصة فيه. لا تشغله على نفس السيرفر حق قاعدة البيانات.

النقاط الرئيسية

  • بحث المنتجات مش بحث نصي. الخصائص المنظمة، الفلاتر، الصلة التجارية، المخزون الفوري، والدعم متعدد اللغات يخلوه مختلف جذرياً.

  • أزل التطبيع للبحث، واحتفظ بالتطبيع للتخزين. مستند البحث تمثيل مسطح ومكتفي ذاتياً لكل شي مطلوب لعرض نتيجة بحث. بدون joins، بدون lookups.

  • فهرس واحد لكل لغة. الـ analyzers و tokenizers و stop words الخاصة باللغة تنتج نتائج أفضل بشكل كبير من فهرس مختلط اللغات.

  • مزامنة مبنية على الأحداث مع إعادة فهرسة مجدولة كشبكة أمان. تحديثات فورية للعمليات العادية. إعادة فهرسة ليلية كاملة تلتقط أي شي الأحداث فاتته.

  • ضبط الصلة قرار تجاري. جودة تطابق النص، حالة التوفر، الشعبية، التقييمات، والهامش كلها إشارات ترتيب. الصلة الافتراضية غلط للتجارة.

  • MeiliSearch للبساطة، OpenSearch للقوة. MeiliSearch مثالي لكتالوجات أقل من 500 ألف مع تحمل أخطاء إملائية ممتاز. OpenSearch يتعامل مع aggregations معقدة، بحث هجين، ونشر على نطاق واسع.

نبني بنية البحث التحتية كجزء من ممارسات هندسة البيانات والتجارة الإلكترونية حقتنا. إذا كنت تبني أو ترحّل بحث منتجات، تواصل مع فريقنا أو اطلب عرض سعر. إضافة Vendure Data Hub حقتنا تشمل أنظمة بحث لـ MeiliSearch و OpenSearch و Elasticsearch و Algolia و Typesense.

المواضيع المغطاة

بحث التجارة الإلكترونيةMeiliSearch للتجارةهندسة بحث المنتجاتبحث بالفلاتربحث هجين للتجارةترحيل ElasticsearchOpenSearch للتجارةفهرسة البحث

جاهز لبناء أنظمة ذكاء اصطناعي جاهزة للإنتاج؟

فريقنا متخصص في بناء أنظمة ذكاء اصطناعي جاهزة للإنتاج. خلينا نحكي كيف نقدر نساعد.

ابدأ محادثة