diff --git a/app/services/search/classified_listing.rb b/app/services/search/classified_listing.rb new file mode 100644 index 000000000..9d4b3dd4c --- /dev/null +++ b/app/services/search/classified_listing.rb @@ -0,0 +1,129 @@ +module Search + class ClassifiedListing + INDEX_NAME = "classified_listings_#{Rails.env}".freeze + INDEX_ALIAS = "classified_listings_#{Rails.env}_alias".freeze + + class << self + def index(classified_listing_id, serialized_data) + SearchClient.index( + id: classified_listing_id, + index: INDEX_ALIAS, + body: serialized_data, + ) + end + + def find_document(classified_listing_id) + SearchClient.get(id: classified_listing_id, index: INDEX_ALIAS) + end + + def create_index(index_name: INDEX_NAME) + SearchClient.indices.create(index: index_name, body: settings) + end + + def delete_index(index_name: INDEX_NAME) + SearchClient.indices.delete(index: index_name) + end + + def add_alias(index_name: INDEX_NAME, index_alias: INDEX_ALIAS) + SearchClient.indices.put_alias(index: index_name, name: index_alias) + end + + def update_mappings(index_alias: INDEX_ALIAS) + SearchClient.indices.put_mapping(index: index_alias, body: mappings) + end + + private + + def settings + { settings: { index: index_settings } } + end + + def index_settings + if Rails.env.production? + { + number_of_shards: 2, + number_of_replicas: 1 + } + else + { + number_of_shards: 1, + number_of_replicas: 0 + } + end + end + + def mappings + { + dynamic: "strict", + properties: { + id: { + type: "keyword" + }, + author: { + dynamic: "strict", + properties: { + username: { + type: "keyword" + }, + name: { + type: "keyword" + }, + profile_image_90: { + type: "keyword" + } + } + }, + body_markdown: { + type: "text" + }, + bumped_at: { + type: "date" + }, + category: { + type: "keyword" + }, + contact_via_connect: { + type: "boolean" + }, + expires_at: { + type: "date" + }, + location: { + type: "text", + fields: { + raw: { + type: "keyword" + } + } + }, + processed_html: { + type: "keyword" + }, + slug: { + type: "text", + fields: { + raw: { + type: "keyword" + } + } + }, + tags: { + type: "keyword" + }, + title: { + type: "text", + fields: { + raw: { + type: "keyword" + } + } + }, + user_id: { + type: "keyword" + } + } + } + end + end + end +end diff --git a/app/services/search/cluster.rb b/app/services/search/cluster.rb index 9f73e019f..f64298b4e 100644 --- a/app/services/search/cluster.rb +++ b/app/services/search/cluster.rb @@ -1,6 +1,9 @@ module Search class Cluster - SEARCH_CLASSES = [Search::Tag].freeze + SEARCH_CLASSES = [ + Search::Tag, + Search::ClassifiedListing, + ].freeze class << self def recreate_indexes diff --git a/spec/services/search/classified_listing_spec.rb b/spec/services/search/classified_listing_spec.rb new file mode 100644 index 000000000..2792883ab --- /dev/null +++ b/spec/services/search/classified_listing_spec.rb @@ -0,0 +1,88 @@ +require "rails_helper" + +RSpec.describe Search::ClassifiedListing, type: :service, elasticsearch: true do + describe "::index" do + it "indexes a classified_listing to elasticsearch" do + classified_listing = FactoryBot.create(:classified_listing) + expect { described_class.find_document(classified_listing.id) }.to raise_error(Elasticsearch::Transport::Transport::Errors::NotFound) + described_class.index(classified_listing.id, id: classified_listing.id) + expect(described_class.find_document(classified_listing.id)).not_to be_nil + end + end + + describe "::find_document" do + it "fetches a document for a given ID from elasticsearch" do + classified_listing = FactoryBot.create(:classified_listing) + described_class.index(classified_listing.id, id: classified_listing.id) + expect { described_class.find_document(classified_listing.id) }.not_to raise_error + end + end + + describe "::create_index" do + it "creates an elasticsearch index with INDEX_NAME" do + described_class.delete_index + expect(SearchClient.indices.exists(index: described_class::INDEX_NAME)).to eq(false) + described_class.create_index + expect(SearchClient.indices.exists(index: described_class::INDEX_NAME)).to eq(true) + end + + it "creates an elasticsearch index with name argument" do + other_name = "random" + expect(SearchClient.indices.exists(index: other_name)).to eq(false) + described_class.create_index(index_name: other_name) + expect(SearchClient.indices.exists(index: other_name)).to eq(true) + + # Have to cleanup index since it wont automatically be handled by our cluster class bc of the unexpected name + described_class.delete_index(index_name: other_name) + end + end + + describe "::delete_index" do + it "deletes an elasticsearch index with INDEX_NAME" do + expect(SearchClient.indices.exists(index: described_class::INDEX_NAME)).to eq(true) + described_class.delete_index + expect(SearchClient.indices.exists(index: described_class::INDEX_NAME)).to eq(false) + end + + it "deletes an elasticsearch index with name argument" do + other_name = "random" + described_class.create_index(index_name: other_name) + expect(SearchClient.indices.exists(index: other_name)).to eq(true) + + described_class.delete_index(index_name: other_name) + expect(SearchClient.indices.exists(index: other_name)).to eq(false) + end + end + + describe "::add_alias" do + it "adds alias INDEX_ALIAS to elasticsearch index with INDEX_NAME" do + SearchClient.indices.delete_alias(index: described_class::INDEX_NAME, name: described_class::INDEX_ALIAS) + expect(SearchClient.indices.exists(index: described_class::INDEX_ALIAS)).to eq(false) + described_class.add_alias + expect(SearchClient.indices.exists(index: described_class::INDEX_ALIAS)).to eq(true) + end + + it "adds custom alias to elasticsearch index with INDEX_NAME" do + other_alias = "random" + expect(SearchClient.indices.exists(index: other_alias)).to eq(false) + described_class.add_alias(index_name: described_class::INDEX_NAME, index_alias: other_alias) + expect(SearchClient.indices.exists(index: other_alias)).to eq(true) + end + end + + describe "::update_mappings" do + it "updates index mappings for classified_listing index", :aggregate_failures do + other_name = "random" + described_class.create_index(index_name: other_name) + initial_mapping = SearchClient.indices.get_mapping(index: other_name).dig(other_name, "mappings") + expect(initial_mapping).to be_empty + + described_class.update_mappings(index_alias: other_name) + mapping = SearchClient.indices.get_mapping(index: other_name).dig(other_name, "mappings") + expect(mapping.deep_stringify_keys).to include(described_class.send("mappings").deep_stringify_keys) + + # Have to cleanup index since it wont automatically be handled by our cluster class bc of the unexpected name + described_class.delete_index(index_name: other_name) + end + end +end