پس از اینکه در قسمت قبل با روشهای کار با mapping آشنا شدیم، در این قسمت به انواع datatype ها و پارامترهایی که ضمن تعیین mapping برای هر فیلد میتوان استفاده کرد، خواهیم پرداخت.
ابتدا به معرفی مهمترین datatype ها در Elasticsearch خواهیم پرداخت. datatype یک فیلد در Elasticsearch دو خصوصیت از فیلد را مشخص میکند، اول اینکه چه نوع دادهای در فیلد ذخیره میشود مثلا Boolean یا double و دوم اینکه مقدار فیلد مورد نظر چگونه ایندکس میشود، برای مثال یک دادهی متنی را هم به صورت keyword هم به صورت text میتوان در mapping تعریف کرد اما keyword همانطور که هست ایندکس میشود و text توسط analyzer ها تجزیه شده و ایندکس میشود (تفاوت ایندکس شدن با ذخیره شدن را به یاد دارید. درسته؟!)
هنگامی که بخواهیم یک index را با روش explicit mapping ایجاد کنیم، باید datatype مورد نظر برای فیلدهای آن را مشخص کنیم. در ادامه این قسمت با انواع datatype ها آشنا خواهیم شد. اما قبل از آن یادآوری کنم که datatype یک فیلد به صورت زیر تعیین میشود:
PUT <index-name>
{
"mappings": {
"properties": {
<field-name>: {
"type": <datatype>
}
}
}
}
انواع datatype در Elasticsearch
boolean: این نوع داده فقط مقادیر true یا false و همچنین متنی که قابل تفسیر به true و false باشد، مانند “true”، "false" و "" (معادل false) را قبول میکند.
keyword: این نوع داده در واقع یک خانواده از چند datatype است: keyword و constant_keyword.
- نوع دادهی keyword هیچگاه توسط analyzer تجزیه نمیشود و دقیقا مشابه source_ آن ایندکس میشود. این نوع داده برای دادههای ساختاریافته مانند آدرس ایمیل، شناسه کاربری، تگ و یا کد وضعیت مناسب است.
- نوع دادهی constant_keyword مشابه keyword بوده و مناسب شرایطی است که تمامی document های یک index مقدار یکسانی برای فیلد مورد نظر داشته باشند، برای مثال مقدار فیلدی به نام log_type در ایندکسی به نام error_logs با این فرض که همهی document ها در فیلد log_type مقدار error دارند! این نوع داده در کوئریهای جستجو نسبت به keyword عملکرد بهینهتری خواهد داشت.
numeric: این خانواده شامل type های مختلفی از اعداد از جمله long، integer، short، double، float و ... است که متناسب با اندازه عددی که در فیلد ذخیره میشود، باید datatype مناسب را درنظر گرفت. این نکته را باید درنظر داشت که الزامی ندارد همواره دادهی عددی به صورت نوع دادهی numeric ذخیره شود. تنها مواقعی که نیاز باشد از کوئری range (مورد استفاده برای مقایسهی یک مقدار در یک بازه مشخص) استفاده شود، باید از این datatype ها استفاده شود و در غیر اینصورت keyword گزینهی بهینهتری برای دادههای عددی خواهد بود. برای مثال اگر قصد ذخیرهی status code درخواستهای http را داشته باشیم، در صورتی که بخواهیم مقادیر این فیلد را در یک بازه مثلا بازهی 400 تا 499 فیلتر کنیم، باید از نوع دادهی numeric استفاده کنیم ولی اگر صرفا برای فیلتر کردن بر اساس یک مقدار (نه به صورت بازه) بخواهیم کوئری بزنیم، استفاده از keyword بهتر است.
date: از آنجایی که در JSON نوع دادهی اختصاصی برای تاریخ وجود ندارد، تاریخ معمولا به صورت متن یا گاهی به صورت عددی (seconds-since-the-epoch) ارائه میشود. بنابراین تاریخ و زمان در Elasticsearch باید به یکی از فرمتهای زیر باشد:
- متن با فرمت شناخته شدهی تاریخ و زمان. مثلا "01-01-2015" یا "12:10:30 2015/01/01"
- عددی که بیانگر تاریخ و زمان به اندازهی seconds-since-the-epoch باشد
Elasticsearch در هر حالت تاریخ و زمان را با فرمت UTC و در قالب milliseconds-since-the-epochذخیره خواهد کرد و البته همیشه به صورت متنی و با فرمت مشخص شده در mapping فیلد نمایش داده خواهد شد.
ip: این نوع داده برای مقادیر IPv4 و IPv6 مورد استفاده است.
datatype :text مورد استفاده برای دادههای full-text که قابل خواندن برای انسان باشد برای مثال متن یک ایمیل یا توضیحات و عنوان یک محصول. مقدار این فیلدها توسط analyzer ها تجزیه شده و term های آن برای ایندکس شدن استخراج میشود. این فرایند به Elasticsearch اجازهی پیادهسازی قابلیت full-text search را میدهد. این فیلدها برای sort یا aggregation مناسب نمیباشند. در نظر داشته باشید اگر بخواهیم مقادیر ساختاریافته مانند آدرس ایمیل یا تگهای یک محصول را ایندکس کنیم، بهتر است از نوع دادهی keyword استفاده شود.
object: همانطور که میدانید در JSON این امکان وجود دارد که مقدار یک فیلد، یک object از فیلدهای دیگر باشد. در Elasticsearch نیز این امکان وجود دارد که فیلدها به صورت object تعریف شوند. object در حقیقت شکلی از تعریف یک فیلد به گونهای است که شامل فیلدهای دادهای دیگر باشد. به مثال زیر توجه کنید:
PUT my-index
{
"mappings": {
"properties": {
"manager": {
"properties": {
"age": {"type": "integer"},
"name": {
"properties": {
"first": {"type": "text"},
"last": {"type": "text"}
}
}
}
}
}
}
}
در مثال بالا فیلدهای manager و name خود شامل پارامتر properties هستند و همچنین دقت کنید که کلید type برای آنها استفاده نشده است. با این روش میتوان کلیدهای age در manager و first و last را در manager.name تعریف کرد. به عنوان مثال اگر بخواهیم در index بالا یک document را ایندکس کنیم به شکل زیر میتوان از index API استفاده کرد:
PUT my-index/_doc/1
{
"manager": {
"age": 30,
"name": {
"first": "John",
"last": "Smith"
}
}
}
که Elasticsearch مقدار document بالا را به صورت flat و در قالب key-value به صورت زیر ایندکس میکند:
{
"manager.age": 30,
"manager.name.first": "John",
"manager.name.last": "Smith"
}
نکته 1 : در صورتی که نیاز باشد چند حالت مختلف از مقدار یک فیلد را ایندکس کنیم، میتوان از پارامتر fields در mapping فیلد استفاده کرد و datatype های متفاوتی را برای حالات مختلف فیلد درنظر گرفت. اگر یادتان باشد در قسمت قبل اشاره شد که dynamic mapping در Elasticsearch برای فیلدهای متنی که فرمت تاریخ یا عدد نداشته باشند، به صورت پیشفرض یک فیلد text و یک فیلد keyword درنظر میگیرد. همین کار را خود ما نیز میتوانیم در روش explicit mapping انجام دهیم. برای مثال نمونه زیر را ببینید:
PUT my-index
{
"mappings": {
"properties": {
"city": {
"type": "text",
"fields": {
"raw": {
"type": "keyword"
}
}
}
}
}
}
در مثال بالا برای فیلد city نوع دادهی text تعیین شده است و با استفاده از کلید fields یک sub-field به نام raw تعریف شده و نوع دادهی آن keyword تعیین شده است. در واقع مقداری که در کلید fields قرار میگیرد، دقیقا مشابه تعریف یک فیلد جدید است با این تفاوت که نحوهی دسترسی به sub-field از طریق فیلد اصلی آن انجام میشود (برای مثال city.raw).
نکتهی 2 (مهم): ممکن است کارکرد object و پارامتر fields که در نکتهی قبلی به آن اشاره شد برای شما یکسان به نظر برسد. اما تفاوت اصلی در اینجاست که کارکرد پارامتر field برای ایندکس کردن مقدار یک فیلد به روشهای متفاوت است و کارکرد object تعریف فیلدهای مختلف درون یک فیلد دیگر است. دقت کنید که در مثال مربوط به فیلد city که از پارامتر fieldsاستفاده کردیم، در خروجی دو فیلد به نامهای city و city.raw ایندکس خواهد شد اما در مثال مربوط به object، هیچگاه خود فیلدهای manager و manager.name به تنهایی ایندکس نخواهند شد چراکه مقدار آنها در source_ به صورت یک object بوده و تنها فیلدهای داخلی آنها ایندکس خواهند شد.
نکته 3 : اگر نیاز باشد در یک فیلد چندین مقدار غیر object (متن یا عدد) به صورت آرایه ذخیره شود، نیاز به استفاده از datatype مخصوص نبوده و تنها کافی است datatype مقادیر مورد نظر که در آرایه قرار میگیرند، برای فیلد درنظر گرفته شود. برای مثال فرض کنید بخواهیم فیلد tags را به منظور ذخیرهی آرایهای از تگهای هر محصول در نظر بگیریم:
PUT products
{
"mappings": {
"properties": {
"tags": {
"type": "keyword"
},
…
}
}
}
در نمونه ی بالا، تعیین کردیم مقادیر فیلد tags به صورت keyword ایندکس شوند. حال اگر بخواهیم یک document را ایندکس کنیم، میتوانیم چندین مقدار در فیلد tagsیک document قرار دهیم:
PUT products/_doc/1
{
"description": "a product with some tags …",
"tags": [ "hot sale", " discounted" ]
}
nested: این نوع داده حالت خاصی از نوع دادهی object بوده و برای مواردی مناسب است که بخواهیم آرایهای از object های مستقل را در یک فیلد ایندکس کنیم. برای مثال به API زیر توجه کنید:
PUT groups
{
"group" : "fans",
"users" : [
{
"first" : "John",
"last" : "Smith"
},
{
"first" : "Alice",
"last" : "White"
}
]
}
در مثال بالا قصد این است که دادهی چندین user در فیلد users ایندکس شود اما دادهی هر user به صورت یک object است (بر خلاف آنچه که قبلتر در خصوص ذخیرهی آرایهای از مقادیر غیر object گفته شد). در این موارد باید از نوع دادهی nested در mappings استفاده شود:
PUT groups
{
"mappings": {
"properties": {
"users": {
"type": "nested",
"properties": {
"first": {"type": "text"},
"last": {"type": "text"}
}
}
}
}
}
در مثال بالا دقت شود از پارامتر properties برای تعیین فیلدهای داخلی استفاده شده است (مشابه نوع دادهی object).
بسیار خوب تا اینجا با مهمترین datatype ها آشنا شدیم. در ادامه به برخی از مهمترین پارامترها ضمن تعریف mapping فیلدها نیز اشاره میکنیم (یکی از این پارامترها را قبلتر دیدیم! پارامتر fields).
پارامترهای مهم در mapping
analyzer: یکی از مهمترین پارامترها در mapping یک فیلد، تعیین analyzer آن است که در فصل بعدی به تفصیل به آن خواهیم پرداخت. Elasticsearch شامل چند analyzer پیشفرض است که معمولا برای متنهای انگلیسی مناسب هستند اما برای تجزیهی متن فارسی بهتر است از analyzer های custom که متناسب با نیازمان باشد استفاده کنیم. این پارامتر فقط برای فیلدهای از نوع text قابل استفاده است!
search_analyzer: این پارامتر analyzer مورد استفاده در زمان کوئری search را برای یک فیلد تعیین میکند. به طور خلاصه زمانی که یک کوئری search اجرا میشود، مقدار مورد جستجو در فیلد از نوع text توسط analyzer ها تجزیه میشود. این پارامتر این امکان را میدهد تا analyzer موردنظر برای این مورد را مشخص کنیم. (نگران نباشید این بخش در مورد ماژول mapping بوده و مباحث analyzer ها در فصل آینده به طور کامل بررسی خواهد شد!)
enabled: این پارامتر تعیین میکند فیلد مورد نظر تنها در source_ ذخیره شده و هیچگاه مقدار آن ایندکس نشود! به صورت پیشفرض مقدار آن true است که باعث میشود مقادیر فیلدها علاوه بر ذخیره شدن، ایندکس نیز شوند. توجه شود که این پارامتر فقط برای فیلدهای از نوع object قابل استفاده است البته که در زمان ثبت document میتوان هر مقداری (مثلا متن یا عدد یا object) برای آن تعیین کرد. به مثالهای زیر توجه کنید:
PUT user_sessions
{
"mappings": {
"properties": {
"user_id": {
"type": "keyword"
},
"session_data": {
"type": "object",
"enabled": false
}
}
}
}
فیلد session_data از نوع object تعیین شده و مقدار پارامتر enabled آن false است. بنابراین دادههای فیلد session_data فقط در source_ ذخیره شده و قابل جستجو در search API نمیباشد (این شرایط مانند آن است که از Elasticsearch صرفا به عنوان یک منبع داده (data source) معمولی استفاده شود!)
ضمن ایندکس کردن document میتوان هر مقداری برای این فیلد درنظر گرفت. به مثالهای زیر توجه کنید:
PUT user_sessions/_doc/session_1
{
"user_id": "kimchy",
"session_data": {
"arbitrary_object": {
"some_array": [ "foo", "bar", { "baz": 2 } ]
}
}
}
PUT user_sessions/_doc/session_2
{
"user_id": "jpountz",
"session_data": "some text"
}
fields: همانطور که قبلتر اشاره شد، گاهی اوقات لازم است تا حالات مختلفی از مقادیر یک فیلد را ایندکس کنیم. پارامتر fields این امکان را میدهد تا بتوان برای یک فیلد، sub-field هایی با datatype و پارامترهای مختلف تعیین کرد:
PUT my-index
{
"mappings": {
"properties": {
"city": {
"type": "text",
"fields": {
"raw": {
"type": "keyword"
}
}
}
}
}
}
format: این پارامتر تنها مخصوص فیلد از نوع date بوده و برای تعیین فرمت نمایش تاریخ استفاده میشود. Elasticsearch برای این پارامتر مقادیر built-in نیز دارد که میتوان متناسب با نیاز از آنها نیز استفاده کرد.
normalizer: این پارامتر تنها برای فیلدهای keyword قابل استفاده بوده و کارکرد آن مشابه analyzer برای فیلدهای text است با این تفاوت که هیچگاه مقدار فیلد به term های مجزا تجزیه نشده و فقط امکان اعمال برخی فیلترها روی مقدار فیلد را فراهم میکند. در فصل آینده با این پارامتر بیشتر آشنا خواهیم شد.
در این قسمت ابتدا با انواع datatype ها و کاربرد هریک از آنها آشنا شدیم و نکات مهمی را در خصوص تفاوت کارکرد نوع دادهی object و nested و پارامتر fields بررسی کردیم. همچنین دیدیم برای ذخیره آرایهای از مقادیر که object نباشند، به راحتی کافیست تا نوع دادهی مقداری که در آرایه ذخیره میشود را برای فیلد تعیین کرد و در زمان ایندکس شدن میتوان لیستی از مقادیر را در فیلد ثبت کرد (مثال فیلد tags).
در نهایت در مورد برخی پارامترهای مهم در mappings صحبت شد که موارد مربوط به analyzer ها در فصل بعدی به طور کامل بررسی خواهد شد.