Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add AlgoliaSearchable for User #20869

Merged
merged 13 commits into from Apr 19, 2024
4 changes: 4 additions & 0 deletions app/lib/seeder.rb
Expand Up @@ -26,6 +26,10 @@

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

Check warning on line 32 in app/lib/seeder.rb

View check run for this annotation

Codecov / codecov/patch

app/lib/seeder.rb#L29-L32

Added lines #L29 - L32 were not covered by tests
puts " #{@counter}. #{message}."
yield
else
Expand Down
14 changes: 14 additions & 0 deletions app/models/concerns/algolia_searchable.rb
@@ -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
24 changes: 24 additions & 0 deletions app/models/concerns/algolia_searchable/searchable_user.rb
@@ -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
2 changes: 2 additions & 0 deletions app/models/user.rb
Expand Up @@ -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
Expand Down Expand Up @@ -315,6 +316,7 @@ def calculate_score
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
Expand Down
19 changes: 19 additions & 0 deletions app/workers/algolia_search/search_index_worker.rb
@@ -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
45 changes: 45 additions & 0 deletions spec/models/user_spec.rb
Expand Up @@ -1022,4 +1022,49 @@ def provider_username(service_name)
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
6 changes: 6 additions & 0 deletions spec/rails_helper.rb
Expand Up @@ -128,6 +128,12 @@
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
Expand Down
37 changes: 37 additions & 0 deletions spec/support/algolia_mock_requester.rb
@@ -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
25 changes: 25 additions & 0 deletions spec/workers/algolia_search/search_index_worker_spec.rb
@@ -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