[deploy] Create a Bulk Index Elasticsearch Worker (#7265)

This commit is contained in:
Molly Struve 2020-04-15 09:08:59 -05:00 committed by GitHub
parent ab3a416cfc
commit b10668a454
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 113 additions and 0 deletions

View file

@ -172,6 +172,8 @@ class Article < ApplicationRecord
scope :with_video, -> { published.where.not(video: [nil, ""], video_thumbnail_url: [nil, ""]).where("score > ?", -4) }
scope :eager_load_serialized_data, -> { includes(:user, :organization, :tags) }
algoliasearch per_environment: true, auto_remove: false, enqueue: :trigger_index do
attribute :title
add_index "searchables", id: :index_id, per_environment: true, enqueue: :trigger_index do

View file

@ -19,6 +19,8 @@ class ChatChannelMembership < ApplicationRecord
delegate :channel_type, to: :chat_channel
scope :eager_load_serialized_data, -> { includes(:user, :channel) }
def channel_last_message_at
chat_channel.last_message_at
end

View file

@ -48,6 +48,8 @@ class Comment < ApplicationRecord
before_validation :evaluate_markdown, if: -> { body_markdown }
validate :permissions, if: :commentable
scope :eager_load_serialized_data, -> { includes(:user, :commentable) }
alias touch_by_reaction save
def self.tree_for(commentable, limit = 0)

View file

@ -22,6 +22,7 @@ class Podcast < ApplicationRecord
scope :reachable, -> { where(id: PodcastEpisode.reachable.select(:podcast_id)) }
scope :published, -> { where(published: true) }
scope :available, -> { reachable.published }
scope :eager_load_serialized_data, -> { includes(:user, :podcast, :tags) }
alias_attribute :path, :slug
alias_attribute :profile_image_url, :image_url

View file

@ -20,6 +20,7 @@ class Reaction < ApplicationRecord
scope :positive, -> { where("points > ?", 0) }
scope :readinglist, -> { where(category: "readinglist") }
scope :eager_load_serialized_data, -> { includes(:reactable, :user) }
validates :category, inclusion: { in: CATEGORIES }
validates :reactable_type, inclusion: { in: REACTABLE_TYPES }

View file

@ -36,6 +36,8 @@ class Tag < ActsAsTaggableOn::Tag
after_commit :sync_related_elasticsearch_docs, on: [:update]
after_commit :remove_from_elasticsearch, on: [:destroy]
scope :eager_load_serialized_data, -> {}
include Searchable
SEARCH_SERIALIZER = Search::TagSerializer
SEARCH_CLASS = Search::Tag

View file

@ -159,6 +159,7 @@ class User < ApplicationRecord
scope :top_commenters, lambda { |number = 10|
includes(:counters).order(Arel.sql("user_counters.data -> 'comments_these_7_days' DESC")).limit(number)
}
scope :eager_load_serialized_data, -> { includes(:roles) }
after_save :bust_cache
after_save :subscribe_to_mailchimp_newsletter

View file

@ -9,6 +9,22 @@ module Search
)
end
def bulk_index(data_hashes)
indexing_hashes = data_hashes.map do |data_hash|
model_hash = data_hash.with_indifferent_access
model_hash[:last_indexed_at] = Time.current
{
index: {
_index: self::INDEX_ALIAS,
_id: model_hash[:id],
data: model_hash
}
}
end
process_hashes(indexing_hashes)
end
def find_document(doc_id)
Search::Client.get(id: doc_id, index: self::INDEX_ALIAS)
end
@ -80,6 +96,30 @@ module Search
def index_settings
raise "Search classes must implement their own index settings"
end
def process_hashes(indexing_hashes)
indexing_hashes.in_groups_of(1000, false).flat_map do |hashes|
indexing_chunks(hashes).select(&:any?).map { |chunk| Search::Client.bulk(body: chunk) }
end
end
def indexing_chunks(hashes)
return [] unless hashes.any?
return to_enum(__method__, hashes) unless block_given?
size = 0
chunk = []
hashes.each do |hash|
chunk << hash
size += hash.to_s.bytesize
next unless size > 5.megabytes
yield chunk
size = 0
chunk = []
end
yield chunk
end
end
end
end

View file

@ -0,0 +1,16 @@
module Search
class BulkIndexToElasticsearchWorker
include Sidekiq::Worker
sidekiq_options queue: :high_priority, lock: :until_executing
def perform(object_class, ids)
data_hashes = object_class.constantize.
eager_load_serialized_data.
where(id: ids).
find_each.map(&:serialized_search_hash)
"Search::#{object_class}".constantize.bulk_index(data_hashes)
end
end
end

View file

@ -27,6 +27,30 @@ RSpec.describe Search::Base, type: :service, elasticsearch: true do
end
end
describe "::bulk_index" do
it "indexes a set of data hashes to Elasticsearch" do
id_list = [123, 456, 789]
id_list.each do |document_id|
expect { described_class.find_document(document_id) }.to raise_error(Search::Errors::Transport::NotFound)
end
data_hashes = id_list.map { |id| { id: id, name: "i_am_a_tag" } }
described_class.bulk_index(data_hashes)
id_list.each do |document_id|
doc = described_class.find_document(document_id)
expect(doc.dig("_source", "id")).to eql(document_id)
end
end
it "sets last_indexed_at field" do
Timecop.freeze(Time.current) do
described_class.bulk_index([{ id: document_id, name: "i_am_a_tag" }])
last_indexed_at = described_class.find_document(document_id).dig("_source", "last_indexed_at")
expect(Time.zone.parse(last_indexed_at).to_i).to eq(Time.current.to_i)
end
end
end
describe "::find_document" do
it "fetches a document for a given ID from elasticsearch" do
described_class.index(document_id, id: document_id)

View file

@ -0,0 +1,22 @@
require "rails_helper"
RSpec.describe Search::BulkIndexToElasticsearchWorker, type: :worker, elasticsearch: true do
let(:worker) { subject }
let(:article) { create(:article) }
include_examples "#enqueues_on_correct_queue", "high_priority", ["Reaction", 1]
it "indexes documents for a set of given ids and object class" do
reactions = [create(:reaction, reactable: article), create(:reaction), create(:reaction)]
Sidekiq::Worker.clear_all
reactions.each do |reaction|
expect { reaction.elasticsearch_doc }.to raise_error(Search::Errors::Transport::NotFound)
end
worker.perform("Reaction", reactions.map(&:id))
reactions.each do |reaction|
expect(reaction.elasticsearch_doc.dig("_source", "id")).to eql(reaction.id)
end
end
end