Add AlgoliaSearchable for User (#20869)

* Add AlgoliaSearchable::SearchableUser

* Update spec

* Refactor

* Refactor

* Add more test

* Refactor

* Update seeder to clear Algolia index if it exist

* All Algolia directly in update_score

* Fix broken spec

* Remove new queue

* Account for more user statuses

* Remove unncessary test for now
This commit is contained in:
Mac Siri 2024-04-19 10:03:04 -04:00 committed by GitHub
parent 2129ae145b
commit d587ba962d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 176 additions and 0 deletions

View file

@ -26,6 +26,10 @@ class Seeder
if klass.none?
message = ["Creating", count, plural].compact.join(" ")
if klass.respond_to?(:algolia_search) && Settings::General.algolia_search_enabled?
puts " Algolia search enabled, clearing index for #{klass}..."
klass.clear_index!
end
puts " #{@counter}. #{message}."
yield
else

View file

@ -0,0 +1,14 @@
module AlgoliaSearchable
extend ActiveSupport::Concern
DEFAULT_ALGOLIA_SETTINGS = {
per_environment: true,
disable_indexing: -> { Settings::General.algolia_search_enabled? == false },
enqueue: :trigger_sidekiq_worker
}.freeze
included do
include AlgoliaSearch
public_send :include, "AlgoliaSearchable::Searchable#{name}".constantize
end
end

View file

@ -0,0 +1,24 @@
module AlgoliaSearchable
module SearchableUser
extend ActiveSupport::Concern
included do
algoliasearch(**DEFAULT_ALGOLIA_SETTINGS, unless: :bad_actor?) do
attribute :name, :username
attribute :profile_image do
profile_image_90
end
end
end
class_methods do
def trigger_sidekiq_worker(record, delete)
AlgoliaSearch::SearchIndexWorker.perform_async(record.class.name, record.id, delete)
end
end
def bad_actor?
score.negative? || banished? || spam_or_suspended?
end
end
end

View file

@ -5,6 +5,7 @@ class User < ApplicationRecord
include CloudinaryHelper
include Images::Profile.for(:profile_image_url)
include AlgoliaSearchable
# NOTE: we are using an inline module to keep profile related things together.
concerning :Profiles do
@ -315,6 +316,7 @@ class User < ApplicationRecord
calculated_score = (badge_achievements_count * 10) + user_reaction_points
calculated_score -= 500 if spam?
update_column(:score, calculated_score)
AlgoliaSearch::SearchIndexWorker.perform_async(self.class.name, id, false)
end
def path

View file

@ -0,0 +1,19 @@
module AlgoliaSearch
class SearchIndexWorker
include Sidekiq::Worker
sidekiq_options queue: :medium_priority, retry: 5, tags: ["algolia"]
def perform(klass, id, remove)
return unless Settings::General.algolia_search_enabled?
record = klass.constantize
if remove
index = AlgoliaSearch.client.init_index(record.index_name)
index.delete_object(id)
else
record.find(id).index!
end
end
end
end

View file

@ -1022,4 +1022,49 @@ RSpec.describe User do
end
end
end
context "when indexing with Algolia", :algolia do
it "indexes the user on create" do
allow(AlgoliaSearch::SearchIndexWorker).to receive(:perform_async)
create(:user)
expect(AlgoliaSearch::SearchIndexWorker).to have_received(:perform_async).with("User", kind_of(Integer), false)
end
it "updates user index if user's name has changed" do
user = create(:user)
allow(AlgoliaSearch::SearchIndexWorker).to receive(:perform_async)
user.update(name: "New Name")
expect(AlgoliaSearch::SearchIndexWorker).to have_received(:perform_async).with("User", user.id, false)
end
describe "#bad_actor?" do
it "returns false to a regular user" do
user = build(:user)
expect(user.bad_actor?).to be(false)
end
it "returns true if the user has negative score" do
user = build(:user, score: -500)
expect(user.bad_actor?).to be(true)
end
it "returns true if the user has spam role" do
user = build(:user)
user.add_role(:spam)
expect(user.bad_actor?).to be(true)
end
it "return true if user is suspended" do
user = build(:user)
user.add_role(:suspended)
expect(user.bad_actor?).to be(true)
end
it "return true if user is banished" do
user = build(:user)
allow(user).to receive(:banished?).and_return(true)
expect(user.bad_actor?).to be(true)
end
end
end
end

View file

@ -128,6 +128,12 @@ RSpec.configure do |config|
end
end
config.before(:each, :algolia) do
allow(Settings::General).to receive_messages(
algolia_application_id: "on", algolia_search_only_api_key: "on", algolia_api_key: "on",
)
end
config.before(:suite) do
# Set the TZ ENV variable with the current random timezone from zonebie
# which we can then use to properly set the browser time for Capybara specs

View file

@ -0,0 +1,37 @@
# from https://github.com/algolia/algoliasearch-client-ruby/blob/master/test/algolia/integration/mocks/mock_requester.rb
class AlgoliaMockRequester
attr_accessor :requests
def initialize
@connection = nil
@requests = []
end
def send_request(host, method, path, body, headers, timeout, connect_timeout)
request = {
host: host,
method: method,
path: path,
body: body,
headers: headers,
timeout: timeout,
connect_timeout: connect_timeout
}
@requests.push(request)
Algolia::Http::Response.new(
status: 200,
body: '{"hits": [], "status": "published"}',
headers: {},
)
end
def get_connection(host)
@connection = host
end
def build_url(host)
host.protocol + host.url
end
end

View file

@ -0,0 +1,25 @@
require "rails_helper"
# rubocop:disable RSpec/AnyInstance
RSpec.describe AlgoliaSearch::SearchIndexWorker, :algolia, type: :worker do
let(:user) { create(:user) }
before do
mock_requester = AlgoliaMockRequester.new
algolia_config = Algolia::Search::Config.new(AlgoliaSearch.configuration)
mock_client = Algolia::Search::Client.new(algolia_config, http_requester: mock_requester)
AlgoliaSearch.instance_variable_set(:@client, mock_client)
end
it "remove the record from Algolia if record is deleted" do
expect_any_instance_of(Algolia::Search::Index).to receive(:delete_object).with(user.id)
described_class.new.perform(user.class.name, user.id, true)
end
it "index the record in Algolia if record is created" do
allow(User).to receive(:find).with(user.id).and_return(user)
allow(user).to receive(:index!)
described_class.new.perform(user.class.name, user.id, false)
expect(user).to have_received(:index!)
end
end
# rubocop:enable RSpec/AnyInstance