کوئری Disjunction max در دسته‌بندی Compound

کوئری Disjunction max در دسته‌بندی Compound

در قسمت قبل که در مورد کوئری boolean صحبت کردیم، دیدیم که رویکرد این کوئری به گونه‌ای است که امتیاز کوئری‌های قرار داده شده در قید‌های should و must را باهم تجمیع کرده و به عنوان امتیاز نهایی هر document درنظر خواهد گرفت. تفاوت این دو قید در این است که کوئری‌های قید should با عملگر منطقی OR و کوئری‌های قید must با عملگر منطقی AND باهم ترکیب می‌شوند! به عبارتی در قید should  الزامی به تطبیق همه‌ی کوئری‌ها نبوده اما در قید must باید تمامی کوئری‌ها همزمان تطبیق داشته باشند.

این رویکردِ کوئری boolean که همان "تطبیق بیشتر، امتیاز بالاتر" می‌باشد، در سناریو‌هایی ممکن است به شیوه‌ای اشتباه مورد استفاده قرار گرفته و ترتیب نتایج جستجو را به شکل نامعقولی تغییر دهد. مثال آن را در قسمت قبل دیدیم و در این قسمت می‌خواهیم آن را اصلاح کنیم.

در این قسمت با کوئری disjunction max آشنا می‌شویم که به اختصار dis_max نیز نامیده می‌شود. این کوئری همانند کوئری boolean برای ترکیب چندین کوئری به کار می‌رود اما رویکرد پیش‌فرض آن به این شکل است که امتیاز بهترین کوئریِ تطبیق پیدا کرده را به عنوان امتیاز نهایی document برمی‌گزیند. به عبارتی رویکرد کوئری dis_max "بهترین تطبیق، بهترین امتیاز" است.

ساختار کلی کوئری dis_max به شکل زیر است:

GET index_1/_search
{
  "query": {
    "dis_max": {
      "queries": [
        …
      ]
    }
  }
}

همانطور که در بالا مشخص است، در کلید query از کلید dis_max استفاده کرده و داخل آن در کلیدی به نام  queries شروع به تعریف آرایه‌ای از کوئری‌ها خواهیم کرد. پارامتر‌های این کوئری عبارتند از:

  • queries: این پارامتر اجباری بوده و مقدار آن آرایه‌ای از کوئری‌های مدنظر خواهد بود. document های ظاهر شده در نتیجه باید با یک یا چند مورد از این کوئری‌ها تطبیق داشته باشند و اگر با بیشتر از یک کوئری منطبق شوند، Elasticsearch مبنای امتیاز را بیشترین امتیاز خواهد گذاشت.
  • tie_breaker: عددی اعشاری بین 0 تا 1 برای افزایش امتیاز document هایی که با بیش از یک کوئری تطبیق داشته باشند. مقدار پیش‌فرض آن 0 است. از این پارامتر طبق فرمول زیر در محاسبه امتیاز نهایی document ها استفاده می‌شود:

max_score + (tie_breaker * (total scores of other queries except the best match))

با توجه به فرمول بالا اگر مقدار tie_breaker همان مقدار پیش‌فرض 0 باشد، عملا تنها امتیازِ بهترین کوئری تطبیق یافته (max_score) برای هر document لحاظ خواهد شد (بهترین تطبیق، بهترین امتیاز)!

هرچه مقدار tie_breaker افزایش یابد، ضریب اثربخشی سایر کوئری‌ها (queries except the best match) در امتیاز document ها بیشتر خواهد شد تا جایی که اگر مقدار tie_breaker برابر 1 باشد، در عمل رویکرد کوئری dis_max مشابه کوئری boolean خواهد شد (تطبیق بیشتر، امتیاز بالاتر)!

برای مثال کوئری زیر را درنظر بگیرید:

GET index_1/_search
{
  "query": {
    "dis_max": {
      "queries": [
        {
          "match": {"title": "توسعه‌ی نرم‌افزار"}
       },
        {
          "multi_match": {
             "query": "توسعه‌ی نرم‌افزار هوشمند" 
             "fields": ["title", "body"]
           }
        }
      ],
      "tie_breaker": 0.4
    }
  }
}

در مثال بالا با تعیین مقدار 0.4 برای  tie_breaker این رویکرد را داریم که اگر document ای در هر دو کوئری بالا تطبیق داشته باشد، علاوه بر امتیاز بهترین کوئری، امتیاز کوئری دیگر نیز با ضریب 0.4 به امتیاز نهایی اضافه شود (رویکردی مابین کوئری boolean و پیش‌فرض dis_max).

چه زمانی از کدامیک از کوئری‌های boolean یا dis_max استفاده کنیم؟

در پاسخ به این سوال که چه مواقعی بهتر است از کدام یک از کوئری‌های boolean یا dis_max استفاده شود باید گفت که این مساله بستگی به دو عامل اصلی زیر دارد:

1. میزان همپوشانی کوئری‌های ترکیب شده: در این عامل باید دقت کرد چه شرایطی به صورت مشترک در کوئری‌ها تکرار شده است که اگر آن شرایط در چندین کوئری تطبیق پیدا کند باعث خواهد شد تا امتیازی تکراری برای نتایج محاسبه شود. به مثال زیر دقت کنید:

GET index_1/_search
{
  "query": {
    "dis_max": {
     "tie_breaker": 0,
      "queries": [
        {
         "match_phrase": {
           "title": {
             "query": "استخدام دولوپر",
             "slop": 4,
             "boost": 2
            }
          }
        },
        {
         "multi_match": {
            "fields": [
             "title",
              "body"
            ],
           "query": "استخدام دولوپر"
          }
        }
      ]
    }
  }
}

در مثال بالا اگر دقت کنید شرط تطبیق عبارت (phrase) "استخدام دولوپر" در فیلد title در کوئری اول با شرط تطبیق واژگان (terms) "استخدام" و "دولوپر" در فیلد‌های title یا body در کوئری دوم با یکدیگر همپوشانی دارند. این همپوشانی ممکن است در document هایی که شامل واژگان "استخدام" و "دولوپر" در فیلد title بوده و اتفاقا میزان مجاورت این دو واژه در فیلد title آن‌ها با پارامتر slop در کوئری match_phrase تطبیق داشته باشد، باعث شود تا با هر دو کوئری منطبق شده و امتیاز برای این واژگان دو بار محاسبه شود. از این رو از کوئری dis_max استفاده شده است تا بهترین حالت برای امتیازدهی مدنظر قرار گیرد.

2. انتظار ما از کوئری جستجوی مدنظر: عامل مهم دیگری که در استفاده از کوئری مناسب تاثیرگذار خواهد بود، این است که دقیقا انتظار ما از نتایج کوئری چیست! شاید در مواقعی واقعا نیازمندی بر این اساس باشد که تجمیع امتیاز‌ها نشان دهنده‌ی کیفیت بالاتر است. اما به طور کلی و تجربی می‌توان گفت معمولا زمانی این انتظار از کوئری منطقی است که شروط کوئری‌ها همپوشانی نداشته یا اشتراک آن‌ها در حداقل باشد! و همچنین این نکته را در نظر داشته باشید که در کوئری dis_max می‌توانید یک محیط رقابتی بین کوئری‌ها ایجاد کرده تا برای هر document بهترین کوئری تعیین کننده‌ی امتیاز باشد.

مثال از مقایسه عملکرد کوئری dis_max و boolean

هیچ چیز به اندازه‌ی سعی و خطا با مثال‌های عملی نمی‌تواند تسلط شما را نسبت به مهارت در کوئری نوشتن با Elasticsearch تضمین کند! در جلسات قبل یک فایل برای ایجاد ایندکسی با نام sokanacademy_sample_data و یک API برای ایجاد چندین document در آن در اختیار شما گذاشتم. اگر این داده‌ها را در دسترس ندارید می‌توانید از اینجا آن‌ها را دانلود کنید و دو API قرار گرفته در فایل را در محیط dev tools کیبانا اجرا کنید.

حال دو کوئری زیر را اجرا کرده و نتایج آن‌ها را از نظر ترتیب و امتیاز مقایسه کنید:

GET sokanacademy_sample_data/_search
{
  "query": {
    "bool": {
      "should": [
        {
         "match": {
           "title": {
             "query": "لینکدین توسعه‌دهنده backend"
            }
          }
        },
        {
         "match": {
           "description": {
             "query": "لینکدین توسعه‌دهنده backend"
            }
          }
        }
      ]
    }
  }
}

GET sokanacademy_sample_data/_search
{
  "query": {
    "dis_max": {
     "tie_breaker": 0, 
      "queries": [
        {
         "match": {
           "title": {
             "query": "لینکدین توسعه‌دهنده backend"
            }
          }
        },
        {
         "match": {
           "description": {
             "query": "لینکدین توسعه‌دهنده backend"
            }
          }
        }
      ]
    }
  }
}

در نتایج کوئری‌ها اگر به کلید max_score داخل کلید hits توجه کنید، خواهید دید که برای کوئری boolean مقدار 16.20129 و برای کوئری dis_max مقدار 8.761531 محاسبه شده است. این اعداد خود گویای آن هستند که در کوئری boolean امتیاز بیشتری به بهترین نتیجه تخصیص داده شده است!

اکنون به ترتیب نتایج بازگشت داده شده و امتیاز هریک از آن‌ها توجه کنید. نتایج دوم و سوم در دو کوئری جابجا شده‌اند:

_ نتایج در کوئری boolean:

_ نتایج در کوئری dis_max:

اگر به امتیاز‌ها دقت کنید مشخص است که در کوئری boolean به مقاله‌ی با عنوان "لینکدین: ۲۵ مهارت برتر در سال ۲۰۱۴"، 10 امتیاز تعلق گرفته در حالیکه به همین مقاله در کوئری dis_max، امتیاز 5 تعلق گرفته است. مشخص است که به دلیل تکرار واژه‌ی "لینکدین" در فیلد‌های title و  description ، کوئری boolean دو بار امتیاز برای این واژه محاسبه کرده است و در کوئری dis_max امتیاز بالاتر بین فیلد‌های title و description به عنوان امتیاز نهایی درنظر گرفته شده است! (در حقیقت در کوئری dis_max یک محیط رقابتی بین فیلد‌های title و description برقرار است)

در بخش آخر این قسمت آموزشی از شما می‌خواهم با توجه به مثال‌های بالا و ترکیب آن‌ها، کوئری‌ای ایجاد کنید که با منطق رقابتی بین فیلد‌های title و description واژگان مثال زده شده ("لینکدین توسعه‌دهنده backend") را جستجو کند و علاوه بر آن، شرط عدم تطبیق واژگان "طراح طراحی ui ux گرافیک" در فیلد‌های title و description نیز به این کوئری اضافه شود.

سعی کنید خودتان این کوئری را بنویسید و بعد از آن، نتیجه را با پاسخ زیر مقایسه کنید:

GET sokanacademy_sample_data/_search
{
  "query": {
    "bool": {
      "should": [
        {
         "dis_max": {
           "tie_breaker": 0,
           "queries": [
              {
               "match": {
                 "title": {
                   "query": "لینکدین توسعه‌دهنده backend"
                  }
                }
              },
              {
               "match": {
                 "description": {
                   "query": "لینکدین توسعه‌دهنده backend"
                  }
                }
              }
            ]
          }
        }
      ],
     "must_not": [
        {
          "multi_match": {
           "query": "طراح طراحی ui ux گرافیک",
           "fields": [
             "title",
             "description"
            ]
          }
        }
      ]
    }
  }
}