نحوه پیاده سازی elasticsearch در پروژه روبی آن ریلز - قسمت اول

این هفته من درگیر تحویل پروژه ای بودم که با فریمورک ریلز اون رو توسعه میدن. یکی از نیازهایی که کارفرمای پروژه داشت این بود که بتونه داخل پروژش امکان جستجو هم داشته باشه . برای همین شروع کردم به خوندن و پیاده سازی این نوشته حاصل تجربه من از این پیاده سازییه .قبل از هرچیزی باید بگم که من داخل پروژم مدلی دارم به اسم Content که شامل محتوای متنی ، ویدیویی و صوتی، همه این محتواها فیلد های body ، title ،description و type دارن ، کاربر قرار که بیاد و با نوشتن کلمه دلخواهش بتونه محتوای موردنظرش رو پیدا کنه . برای شروع اولین چیزی که لازم داریم استفاده از gem هاست . با یک بررسی ساده بین gem های موجود انتخاب من سه gem زیر بودن :#Gemfile gem 'elasticsearch-rails' gem 'elasticsearch-model' group :test do gem 'elasticsearch-extensions' endElasticsearch-rails : که شامل ویژگی های مختلفی برای اپلیکشین روبی آن ریلز Elasticsearch-model : ابزاری برای ادغام الستیک سرچ با مدل های روبی آن ریلزElasticsearch-extentions : و این مورد هم برای تست استفاده میشه خب توی این مرحله باید دستور bundle install رو داخل ترمینال بزنیم تا gem هامون نصب بشن .توی این مرحله باید انتخاب کنین که برای کدوم یکی از مدل های داخل اپلیکیشن میخواین قابلیت سرچ رو قرار بدین از اونجایی که ممکن در آینده نیازهای مختلفی داشته باشین برای راحتی کار در پوشه Concern داخل مدل ها فایلی رو به اسم searchable.rb میسازیم که بعدا برای هر مدلی که لازم بود بتونیم ازش استفاده کنیم .# models/concerns/searchable.rb require &quotelasticsearch/model&quot module Searchable extend ActiveSupport::Concern included do include Elasticsearch::Model after_commit :index_document, if: :persisted? after_commit on: [:destroy] do __elasticsearch__.delete_document end end private def index_document __elasticsearch__.index_document end endدر توضیح اسنیپ کدی که بالا نوشتم باید بگم که دستور Elasticsearch::Model مدلی که درونش قرار سرویس سرچ داشته باشیم رو قادر میکنه تا از ‌متد های gem ما که قبلا اونها رو نصب کردیم استفاده کنه .قسمت after_commit این امکان رو میده تا هرگونه تغییری که شامل حذف یا اضافه و یا تغییر در محتوای قابل جستجوی ما داخل مدل رو به صورت خودکار index یا delete در داخل الستیک سرچ هم انجام بده . در این مرحله باید concern رو که نوشتیم به مدل مورد نظرمون اضافه کنیم . # /models/Content.rb class Content < ApplicationRecord include Searchable # we are assuming this fields as string fields validates :body, :title, :description, :type, presence: true settings index: { number_of_shards: 1 } do mappings dynamic: 'false' do indexes :body indexes :title indexes :description indexes :type end end end تا اینجای کار فایل searchable رو که قبلتر نوشته بودیم رو به مدلمون اضافه کردیم ، مقادیرش رو validate میکنیم و میرسیم به یه قسمت جدید Settings . این قسمت مثل فایل تنظیمات برای کار ما عمل میکنه و به الستیک میگه که چه فایل هایی رو باید index گذاری کنه . تا اینجا Elastic رو داخل اپ ریلز پیاده کردیم حالا باید به این نکته فکر کنیم که ممکنه داخل دیتابیس داده ای باشه ، از اونجایی که Elastic بصورت پیش فرض هیچ اطلاعاتی رو Index نمیکنه پس باید یک فایل Importer بنویسیم که اطلاعات رو بصورت دستی داده های از قبل موجود رو داخل Elastic وارد کنه .# poros/elasticsearch_data_importer.rb module ElasticsearchDataImporter def self.import [Content].each do |model_to_search| model_to_search.__elasticsearch__.create_index!(force: true) model_to_search.find_in_batches do |records| bulk_index(records, model_to_search) end end end def self.prepare_records(records) records.map do |record| { index: { _id: record.id, data: record.__elasticsearch__.as_indexed_json } } end end def self.bulk_index(records, model) model.__elasticsearch__.client.bulk( index: model.__elasticsearch__.index_name, type: model.__elasticsearch__.document_type, body: prepare_records(records) ) end end توی تیکه کد بالا اول گفتم که به ازای مدلی که داریم محتوای داخل مدل رو Index بکنه و در مرحله بعد هم تمام رکورد ها رو یکجا وارد دیتابیس الستیک بکنه . برای اجرای این کد باید دستور زیر رو داخل ترمینال وارد کنین . # You need to have ElasticSearch running on port 9200 to make this # work. ElasticsearchDataImporter.importتا اینجا بیشتر کارهارو انجام دادیم و الان لازم که خود سرچ رو بنویسیم . قبل از اینکه بخوایم بریم سراغ Controller بنظرم خوبه که یه تستی از کاری که تا اینجا کردیم رو هم انجام بدیم .همونطور که قبل تر گفتم من داخل پروژم یک مدل دارم به اسم Content که فیلد‌های type ، body ، description و title داریم که قرار روی اونها جستجو بشه .خب با زدن دستور rails console وارد محیط کنسول ریلز میشیم تا یک رکورد جدید بسازیم و بتونیم روی اون تست کنیم . Content.create(title: 'covid-19', type: Article, body: 'covid-19 is a diseases', description:'diseases is dangerous')از طریق دستور زیر داخل اطلاعاتی که الستیک اونها رو Index کرده جستجو میکنیم . # This should return how many records were found Content.__elasticsearch__.search('covid-19').results.totalتا اینجا میتونیم مطمئن باشیم که همه چی درسته اما چیزی که واقعا بهش احتیاج داریم Controller که بتونیم مدلی رو که قرار روش جستجو بشه رو تعریف کنیم .# controllers/content_controller.rb class ContentController < ApplicationController def search search_result = Elasticsearch::Model.search(params[:query].to_s, [Content] ).records.records render json: search_result, status: :found end end # routes.rb resources :content, only: [] do get 'search', on: :collection end کنترلر Content جایی که میخواستیم کابر بتونه جستجو انجام بده پس متد search رو براش تعریف کردیم و متد Elasticsearch::Model.search قرار میدیم تا کاربر بتونه از این طریق جستجوی مورد نظرش رو بر روی اطلاعات داخل مدل انجام بده . تا اینجای کار همه چی به خوبی پیاده شده اما به این صورت که کابر باید دقیقا کلمه covid-19 کامل وارد کنه تا نتیجه درست رو ببینه اما اگر کابر کلمه رو درست وارد نکرد چی ؟برای حل این مشکل نیاز داریم تا جستجو رو بهینه کنیم پس :# services/autocompleter.rb class Autocompleter MODELS_TO_SEARCH = [Content].freeze attr_accessor :query def initialize(query) @query = query end def self.call(query) new(query).call end def call results.map do |result| { hint: build_hint(result), record_type: result.class.name, record_id: result.id } end end private def results Elasticsearch::Model.search(search_query, MODELS_TO_SEARCH).records end def build_hint(record) HintBuilder.call(record) end def search_query { &quotsize&quot: 50, &quotquery&quot: { &quotfunction_score&quot: { &quotquery&quot: { &quotbool&quot: { &quotmust&quot: [multi_match] } }, &quotfunctions&quot: priorities } } } end def multi_match { &quotmulti_match&quot: { &quotquery&quot: @query, &quotfields&quot: %w[body title description body], &quotfuzziness&quot: 'auto' } } end def priorities [ { &quotfilter&quot: { &quotterm&quot: { &quot_type&quot: 'text' } }, &quotweight&quot: 5000 } ] end endبا کدی که بالا نوشتم نتیجه کار رو بهینه تر کردیم اما بعضی از مفاهیمی رو که استفاده کردم رو در زیر توضیح میدم : size : . تعداد نتایجی که در هربار جستجو قرار نمایش داده بشه به کاربر function_score : به شما اجازه میده تا امتیاز محتوایی که بدست اومده رو مشخص کنین اینکار با کمک وزن که پایین تر راجع بهش میگم به بهبود نتایج جستجو در آینده کمک میکنه . multi-match : به شما اجازه میده در صورتی که بخواین داخل چنتا فیلد از مدلتون رو جستجو کنین به الستیک اعلام کنین که این فیلد ها رو برام ایندکس کن چرا که میخوام داخل اونها جستجو کنم .query : . مقداری که کاربر از ورودی میفرسته تا داخل الستیک جستجو انجام بشه fuzziness : وظیفه فازینس اینه که مشکلات نوشتاری حل بکنه مقداری که براش تعریف کردم برای اینه که تا دو حرف رو بتونه بصورت پایه پوشش بده . functions : به ما کمک میکنه تا الویت هایی که برای این جستجو لازم داریم رو مشخص کنیم .HintBuilder : مسئول ساخت نتیجه جستجو در داخل فایل های ایندکس شدس که از طریق یه فایل دیگه به کاربر نمایش داده میشه .# services/hint_builder.rb class HintBuilder attr_accessor :record def initialize(record) @record = record end def self.call(record) new(record).call end def call ContentResultBuilder.new(@record).autocomplete_hint end end # services/result_builder_base.rb class ResultBuilderBase def initialize(record) @record = record end private attr_reader :record end # services/Content_result_builder.rb class ContentResultBuilder < ResultBuilderBase def autocomplete_hint &quot#{record.title}, #{record.body}, #{record.description}, #{record.type}&quot end endHintBuilder نتیجه جستجو رو با کمک ContentResultBuilder که از ResutBuilderBase ارث میبره و title ، body ، description و type رو بر میگردونه . باید اضافه کنم که من اینجا صرفا نتیجه جستجو کامل نوشتم که بدونین میشه اضافه کرد و کاملا دلبخواه و باتوجه به نیازتون میتونین موارد رو کم یا زیاد کنین . به اینجا که رسیدیم باید دوباره متد سرچ رو که داخل کنترلر نوشتیم رو اپدیت کنیم به صورتی که پایین تر مینویسم : # controllers/content_controller.rb def search search_result = Autocompleter.call(params[:query].to_s) render json: search_result, status: :found endبا همه کارهایی که تا اینجا کردیم سرچ به خوبی پیاده سازی شد و جستجوگر ما قادر که غلط های تایپی رو هم تا دو حرف تشخیص بده . خب تا همین جا کافیه برای قسمت اول برای قسمت دوم راجع به بهینه نویسی متد سرچ و همچنین پیاده سازی ساختار پیشنهاد بصورت حداقلی داخل الستیک مینویسم .باید اضافه کنم که این ساختار صرفا نتیجه تجربه من از پیاده سازی و قطعا راه صدرصدی نیست ، ممکنه که شما بتونین تا اینجای راه رو هم بهینه تر بنویسین .