[deploy] Refactor:Remove Reaction Search Code (#10377)
This commit is contained in:
parent
779fad25b9
commit
e9cb7b2fd9
22 changed files with 14 additions and 647 deletions
|
|
@ -8,7 +8,6 @@ class Article < ApplicationRecord
|
|||
|
||||
SEARCH_SERIALIZER = Search::ArticleSerializer
|
||||
SEARCH_CLASS = Search::FeedContent
|
||||
DATA_SYNC_CLASS = DataSync::Elasticsearch::Article
|
||||
|
||||
acts_as_taggable_on :tags
|
||||
resourcify
|
||||
|
|
@ -91,7 +90,6 @@ class Article < ApplicationRecord
|
|||
}
|
||||
after_commit :async_score_calc, :update_main_image_background_hex, :touch_collection, on: %i[create update]
|
||||
after_commit :index_to_elasticsearch, on: %i[create update]
|
||||
after_commit :sync_related_elasticsearch_docs, on: %i[create update]
|
||||
after_commit :remove_from_elasticsearch, on: [:destroy]
|
||||
|
||||
serialize :cached_user
|
||||
|
|
|
|||
|
|
@ -1,14 +1,10 @@
|
|||
class Reaction < ApplicationRecord
|
||||
include Searchable
|
||||
BASE_POINTS = {
|
||||
"vomit" => -50.0,
|
||||
"thumbsup" => 5.0,
|
||||
"thumbsdown" => -10.0
|
||||
}.freeze
|
||||
|
||||
SEARCH_SERIALIZER = Search::ReactionSerializer
|
||||
SEARCH_CLASS = Search::Reaction
|
||||
|
||||
CATEGORIES = %w[like readinglist unicorn thinking hands thumbsup thumbsdown vomit].freeze
|
||||
PUBLIC_CATEGORIES = %w[like readinglist unicorn thinking hands].freeze
|
||||
REACTABLE_TYPES = %w[Comment Article User].freeze
|
||||
|
|
@ -41,8 +37,6 @@ class Reaction < ApplicationRecord
|
|||
after_create_commit :record_field_test_event
|
||||
after_commit :async_bust
|
||||
after_commit :bust_reactable_cache, :update_reactable, on: %i[create update]
|
||||
after_commit :index_to_elasticsearch, if: :indexable?, on: %i[create update]
|
||||
after_commit :remove_from_elasticsearch, if: :indexable?, on: [:destroy]
|
||||
|
||||
class << self
|
||||
def count_for_article(id)
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
module Search
|
||||
class ReactionSerializer < ApplicationSerializer
|
||||
attributes :id, :created_at, :category, :status, :user_id
|
||||
|
||||
attribute :reactable do |reaction|
|
||||
reactable = reaction.reactable
|
||||
tags = if reactable.respond_to?(:tags)
|
||||
reactable.tags.map do |tag|
|
||||
{ name: tag.name, keywords_for_search: tag.keywords_for_search }
|
||||
end
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
{
|
||||
id: reactable.id,
|
||||
body_text: reactable.respond_to?(:body_text) ? reactable.body_text : reactable.body_markdown,
|
||||
class_name: reactable.class.name,
|
||||
path: reactable.path,
|
||||
published_date_string: reactable.readable_publish_date,
|
||||
reading_time: reactable.respond_to?(:reading_time) ? reactable.reading_time : 1,
|
||||
tags: tags,
|
||||
title: reactable.title,
|
||||
user: NestedUserSerializer.new(reactable.user).serializable_hash.dig(
|
||||
:data, :attributes
|
||||
)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
module DataSync
|
||||
module Elasticsearch
|
||||
class Article < Base
|
||||
RELATED_DOCS = %i[
|
||||
reactions
|
||||
].freeze
|
||||
|
||||
SHARED_FIELDS = %i[
|
||||
body_markdown
|
||||
path
|
||||
published
|
||||
reading_time
|
||||
tag_list
|
||||
title
|
||||
].freeze
|
||||
|
||||
private
|
||||
|
||||
def sync_related_documents
|
||||
RELATED_DOCS.each do |relation_name|
|
||||
if updated_record.published
|
||||
__send__(relation_name).find_each(&:index_to_elasticsearch)
|
||||
elsif updated_fields.key?(:published)
|
||||
__send__(relation_name).find_each(&:remove_from_elasticsearch)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def sync_needed?
|
||||
updated_fields.slice(*SHARED_FIELDS).any? && reactions.any?
|
||||
end
|
||||
|
||||
def reactions
|
||||
updated_record.reactions.readinglist
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -4,7 +4,6 @@ module DataSync
|
|||
RELATED_DOCS = %i[
|
||||
articles
|
||||
podcast_episodes
|
||||
reactions
|
||||
].freeze
|
||||
|
||||
SHARED_FIELDS = %i[
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ module DataSync
|
|||
class User < Base
|
||||
RELATED_DOCS = %i[
|
||||
articles
|
||||
reactions
|
||||
podcast_episodes
|
||||
chat_channel_memberships
|
||||
comments
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ module Search
|
|||
Search::ChatChannelMembership,
|
||||
Search::Listing,
|
||||
Search::FeedContent,
|
||||
Search::Reaction,
|
||||
Search::Tag,
|
||||
Search::User,
|
||||
].freeze
|
||||
|
|
|
|||
|
|
@ -1,92 +0,0 @@
|
|||
module Search
|
||||
module QueryBuilders
|
||||
class Reaction < QueryBase
|
||||
QUERY_KEYS = {
|
||||
search_fields: [
|
||||
"reactable.tags.keywords_for_search",
|
||||
"reactable.tags.name^3",
|
||||
"reactable.body_text^2",
|
||||
"reactable.title^6",
|
||||
"reactable.user.name",
|
||||
"reactable.user.username",
|
||||
]
|
||||
}.freeze
|
||||
|
||||
# Search keys from our controllers may not match what we have stored in Elasticsearch so we map them here,
|
||||
# this allows us to change our Elasticsearch docs without worrying about the frontend
|
||||
TERM_KEYS = {
|
||||
category: "category",
|
||||
tag_names: "reactable.tags.name",
|
||||
user_id: "user_id",
|
||||
status: "status"
|
||||
}.freeze
|
||||
|
||||
DEFAULT_PARAMS = {
|
||||
sort_by: "created_at",
|
||||
sort_direction: "desc",
|
||||
size: 0
|
||||
}.freeze
|
||||
|
||||
SOURCE = %i[
|
||||
id
|
||||
reactable.path
|
||||
reactable.published_date_string
|
||||
reactable.reading_time
|
||||
reactable.tags
|
||||
reactable.title
|
||||
reactable.user
|
||||
].freeze
|
||||
|
||||
attr_accessor :params, :body
|
||||
|
||||
def initialize(params:)
|
||||
super()
|
||||
|
||||
@params = params.deep_symbolize_keys
|
||||
|
||||
# Default to only readinglist reactions
|
||||
@params[:category] = "readinglist"
|
||||
|
||||
build_body
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_queries
|
||||
@body[:query] = { bool: {} }
|
||||
@body[:query][:bool][:filter] = terms_keys if terms_keys_present?
|
||||
@body[:query][:bool][:must] = query_conditions if query_keys_present?
|
||||
end
|
||||
|
||||
def terms_keys_present?
|
||||
TERM_KEYS.detect { |key, _| @params[key].present? }
|
||||
end
|
||||
|
||||
def terms_keys
|
||||
TERM_KEYS.flat_map do |term_key, search_key|
|
||||
next unless @params.key? term_key
|
||||
|
||||
values = Array.wrap(@params[term_key])
|
||||
|
||||
if params[:tag_boolean_mode] == "all" && term_key == :tag_names
|
||||
values.map { |val| { term: { search_key => val } } }
|
||||
else
|
||||
{ terms: { search_key => values } }
|
||||
end
|
||||
end.compact
|
||||
end
|
||||
|
||||
def query_hash(key, fields)
|
||||
{
|
||||
simple_query_string: {
|
||||
query: key,
|
||||
fields: fields,
|
||||
lenient: true,
|
||||
analyze_wildcard: true,
|
||||
minimum_should_match: 2
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
module Search
|
||||
class Reaction < Base
|
||||
INDEX_NAME = "reactions_#{Rails.env}".freeze
|
||||
INDEX_ALIAS = "reactions_#{Rails.env}_alias".freeze
|
||||
MAPPINGS = JSON.parse(File.read("config/elasticsearch/mappings/reactions.json"), symbolize_names: true).freeze
|
||||
DEFAULT_PAGE = 0
|
||||
DEFAULT_PER_PAGE = 80
|
||||
|
||||
class << self
|
||||
def search_documents(params:)
|
||||
set_query_size(params)
|
||||
query_hash = "Search::QueryBuilders::#{name.demodulize}".safe_constantize.new(params: params).as_hash
|
||||
|
||||
results = search(body: query_hash)
|
||||
hits = results.dig("hits", "hits").map { |hit| prepare_doc(hit) }
|
||||
{
|
||||
"reactions" => paginate_hits(hits, params),
|
||||
"total" => results.dig("hits", "total", "value")
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def index_settings
|
||||
if Rails.env.production?
|
||||
{
|
||||
number_of_shards: 10,
|
||||
number_of_replicas: 1
|
||||
}
|
||||
else
|
||||
{
|
||||
number_of_shards: 1,
|
||||
number_of_replicas: 0
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def dynamic_index_settings
|
||||
if Rails.env.production?
|
||||
{ refresh_interval: "2s" }
|
||||
else
|
||||
{ refresh_interval: "1s" }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
module DataUpdateScripts
|
||||
class ReindexReadingListReactions
|
||||
def run
|
||||
Reaction.readinglist.select(:id).in_batches do |batch|
|
||||
Search::BulkIndexWorker.set(queue: :default).perform_async(
|
||||
"Reaction", batch.ids
|
||||
)
|
||||
end
|
||||
# Reactions are no longer indexed in Elasticsearch
|
||||
# Reaction.readinglist.select(:id).in_batches do |batch|
|
||||
# Search::BulkIndexWorker.set(queue: :default).perform_async(
|
||||
# "Reaction", batch.ids
|
||||
# )
|
||||
# end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -15,16 +15,12 @@ RSpec.describe ActsAsTaggableOn::Tag, type: :lib do
|
|||
podcast_episode = create(:podcast_episode)
|
||||
tag = article.tags.first
|
||||
podcast_episode.tags << tag
|
||||
reaction = create(:reaction, reactable: article, category: "readinglist")
|
||||
new_keywords = "keyword1, keyword2, keyword3"
|
||||
sidekiq_perform_enqueued_jobs
|
||||
|
||||
tag.update(keywords_for_search: new_keywords)
|
||||
sidekiq_perform_enqueued_jobs
|
||||
expect(collect_keywords(article)).to include(new_keywords)
|
||||
expect(
|
||||
reaction.elasticsearch_doc.dig("_source", "reactable", "tags").flat_map { |t| t["keywords_for_search"] },
|
||||
).to include(new_keywords)
|
||||
expect(collect_keywords(podcast_episode)).to include(new_keywords)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
require "rails_helper"
|
||||
require Rails.root.join("lib/data_update_scripts/20200901194251_reindex_reading_list_reactions.rb")
|
||||
|
||||
describe DataUpdateScripts::ReindexReadingListReactions, elasticsearch: "Reaction" do
|
||||
it "indexes feed content(articles, comments, podcast episodes) to Elasticsearch" do
|
||||
reactions = create_list(:reaction, 3, category: "readinglist")
|
||||
Sidekiq::Worker.clear_all
|
||||
|
||||
reactions.each do |reaction|
|
||||
expect { reaction.elasticsearch_doc }.to raise_error(Search::Errors::Transport::NotFound)
|
||||
end
|
||||
|
||||
sidekiq_perform_enqueued_jobs { described_class.new.run }
|
||||
reactions.each do |reaction|
|
||||
expect(reaction.elasticsearch_doc).not_to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -68,49 +68,6 @@ RSpec.describe Article, type: :model do
|
|||
article.destroy
|
||||
end
|
||||
end
|
||||
|
||||
it "on update syncs elasticsearch data" do
|
||||
allow(article).to receive(:sync_related_elasticsearch_docs)
|
||||
article.save
|
||||
expect(article).to have_received(:sync_related_elasticsearch_docs)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#after_update_commit" do
|
||||
it "if article is unpublished removes reading list reactions from index" do
|
||||
reaction = create(:reaction, reactable: article, category: "readinglist")
|
||||
sidekiq_perform_enqueued_jobs
|
||||
expect(reaction.elasticsearch_doc).not_to be_nil
|
||||
|
||||
unpublished_body = "---\ntitle: Hellohnnnn#{rand(1000)}\npublished: false\ntags: hiring\n---\n\nHello"
|
||||
article.update(body_markdown: unpublished_body)
|
||||
sidekiq_perform_enqueued_jobs
|
||||
expect { reaction.elasticsearch_doc }.to raise_error(Search::Errors::Transport::NotFound)
|
||||
end
|
||||
|
||||
it "if article is published indexes reading list reactions" do
|
||||
reaction = create(:reaction, reactable: article, category: "readinglist")
|
||||
sidekiq_perform_enqueued_jobs
|
||||
unpublished_body = "---\ntitle: Hellohnnnn#{rand(1000)}\npublished: false\ntags: hiring\n---\n\nHello"
|
||||
article.update(body_markdown: unpublished_body)
|
||||
sidekiq_perform_enqueued_jobs
|
||||
expect { reaction.elasticsearch_doc }.to raise_error(Search::Errors::Transport::NotFound)
|
||||
|
||||
published_body = "---\ntitle: Hellohnnnn#{rand(1000)}\npublished: true\ntags: hiring\n---\n\nHello"
|
||||
article.update(body_markdown: published_body)
|
||||
sidekiq_perform_enqueued_jobs
|
||||
expect(reaction.elasticsearch_doc).not_to be_nil
|
||||
end
|
||||
|
||||
it "indexes reaction if a REACTION_INDEXED_FIELDS is changed" do
|
||||
reaction = create(:reaction, reactable: article, category: "readinglist")
|
||||
allow(article).to receive(:index_to_elasticsearch)
|
||||
allow(article.user).to receive(:index_to_elasticsearch)
|
||||
|
||||
sidekiq_assert_enqueued_with(job: Search::IndexWorker, args: ["Reaction", reaction.id]) do
|
||||
article.update(body_markdown: "---\ntitle: NEW TITLE#{rand(1000)}\n")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when published" do
|
||||
|
|
|
|||
|
|
@ -87,62 +87,6 @@ RSpec.describe Reaction, type: :model do
|
|||
end
|
||||
end
|
||||
|
||||
describe "#after_commit" do
|
||||
context "when category is readingList and reactable is published" do
|
||||
it "on update enqueues job to index reaction to elasticsearch" do
|
||||
reaction.save
|
||||
sidekiq_assert_enqueued_with(job: Search::IndexWorker, args: [described_class.to_s, reaction.id]) do
|
||||
reaction.update(category: "readinglist")
|
||||
end
|
||||
end
|
||||
|
||||
it "on create enqueues job to index reaction to elasticsearch" do
|
||||
reaction.category = "readinglist"
|
||||
sidekiq_assert_enqueued_with(job: Search::IndexWorker) do
|
||||
reaction.save
|
||||
end
|
||||
end
|
||||
|
||||
it "on destroy enqueues job to delete reaction from elasticsearch" do
|
||||
reaction.category = "readinglist"
|
||||
reaction.save
|
||||
sidekiq_assert_enqueued_with(job: Search::RemoveFromIndexWorker,
|
||||
args: [described_class::SEARCH_CLASS.to_s, reaction.id]) do
|
||||
reaction.destroy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when category is not readinglist" do
|
||||
before do
|
||||
reaction.category = "like"
|
||||
allow(reaction.user).to receive(:index_to_elasticsearch)
|
||||
allow(reaction.reactable).to receive(:index_to_elasticsearch)
|
||||
sidekiq_perform_enqueued_jobs
|
||||
end
|
||||
|
||||
it "on update does not enqueue job to index reaction to elasticsearch" do
|
||||
reaction.save
|
||||
sidekiq_assert_no_enqueued_jobs(only: Search::IndexWorker) do
|
||||
reaction.update(category: "unicorn")
|
||||
end
|
||||
end
|
||||
|
||||
it "on create does not enqueue job to index reaction to elasticsearch" do
|
||||
sidekiq_assert_no_enqueued_jobs(only: Search::IndexWorker) do
|
||||
reaction.save
|
||||
end
|
||||
end
|
||||
|
||||
it "on destroy does not enqueue job to delete reaction from elasticsearch" do
|
||||
reaction.save
|
||||
sidekiq_assert_no_enqueued_jobs(only: Search::RemoveFromIndexWorker) do
|
||||
reaction.destroy
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#skip_notification_for?" do
|
||||
let(:receiver) { build(:user) }
|
||||
let(:reaction) { build(:reaction, reactable: build(:article), user: nil) }
|
||||
|
|
|
|||
|
|
@ -127,16 +127,12 @@ RSpec.describe Tag, type: :model do
|
|||
podcast_episode = create(:podcast_episode)
|
||||
tag = described_class.find(article.tags.first.id)
|
||||
podcast_episode.tags << tag
|
||||
reaction = create(:reaction, reactable: article, category: "readinglist")
|
||||
new_keywords = "keyword1, keyword2, keyword3"
|
||||
sidekiq_perform_enqueued_jobs
|
||||
|
||||
tag.update(keywords_for_search: new_keywords)
|
||||
sidekiq_perform_enqueued_jobs
|
||||
expect(collect_keywords(article)).to include(new_keywords)
|
||||
expect(
|
||||
reaction.elasticsearch_doc.dig("_source", "reactable", "tags").flat_map { |t| t["keywords_for_search"] },
|
||||
).to include(new_keywords)
|
||||
expect(collect_keywords(podcast_episode)).to include(new_keywords)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Search::ReactionSerializer do
|
||||
let(:user) { create(:user) }
|
||||
let(:tag) { create(:tag, name: "ama") }
|
||||
let(:article) { create(:article, user: user, tags: tag.name) }
|
||||
let(:comment) { create(:comment, user: user, commentable: article) }
|
||||
let(:article_reaction) { create(:reaction, reactable: article, user: user) }
|
||||
let(:comment_reaction) { create(:reaction, reactable: comment, user: user) }
|
||||
|
||||
it "serializes an article reaction" do
|
||||
data_hash = described_class.new(article_reaction).serializable_hash.dig(:data, :attributes)
|
||||
user_data = Search::NestedUserSerializer.new(user).serializable_hash.dig(:data, :attributes)
|
||||
expect(data_hash.dig(:reactable, :user)).to eq(user_data)
|
||||
expect(data_hash[:reactable].keys).to include(:id, :body_text, :class_name, :path, :tags, :title)
|
||||
expect(data_hash.keys).to include(:id, :created_at, :category, :status, :user_id)
|
||||
end
|
||||
|
||||
it "serializes a comment reaction" do
|
||||
data_hash = described_class.new(comment_reaction).serializable_hash.dig(:data, :attributes)
|
||||
user_data = Search::NestedUserSerializer.new(user).serializable_hash.dig(:data, :attributes)
|
||||
expect(data_hash.dig(:reactable, :user)).to eq(user_data)
|
||||
expect(data_hash[:reactable].keys).to include(:id, :body_text, :class_name, :path, :tags, :title)
|
||||
expect(data_hash.keys).to include(:id, :created_at, :category, :status, :user_id)
|
||||
end
|
||||
|
||||
it "creates valid json for Elasticsearch" do
|
||||
data_hash = described_class.new(article_reaction).serializable_hash.dig(:data, :attributes)
|
||||
result = Reaction::SEARCH_CLASS.index(article.id, data_hash)
|
||||
expect(result["result"]).to eq("created")
|
||||
end
|
||||
end
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe DataSync::Elasticsearch::Article, type: :service do
|
||||
let(:article) { create(:article) }
|
||||
|
||||
it "defines necessary constants" do
|
||||
expect(described_class::RELATED_DOCS).not_to be_nil
|
||||
expect(described_class::SHARED_FIELDS).not_to be_nil
|
||||
end
|
||||
|
||||
describe "#sync_related_documents" do
|
||||
it "removes docs from elasticsearch if article is unpublished" do
|
||||
allow(article).to receive(:saved_changes).and_return(published: [true, false])
|
||||
reaction = create(:reaction, reactable: article, category: "readinglist")
|
||||
sidekiq_perform_enqueued_jobs
|
||||
expect(reaction.elasticsearch_doc).not_to be_nil
|
||||
|
||||
article.update_column(:published, false)
|
||||
|
||||
described_class.new(article).call
|
||||
sidekiq_perform_enqueued_jobs
|
||||
expect { reaction.elasticsearch_doc }.to raise_error(Search::Errors::Transport::NotFound)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -28,8 +28,6 @@ RSpec.describe Moderator::MergeUser, type: :service do
|
|||
drain_all_sidekiq_jobs
|
||||
expect(article.elasticsearch_doc.dig("_source", "user", "id")).to eq(delete_user_id)
|
||||
expect(comment.elasticsearch_doc.dig("_source", "user", "id")).to eq(delete_user_id)
|
||||
expect(reaction.elasticsearch_doc.dig("_source", "user_id")).to eq(delete_user_id)
|
||||
expect(article_reaction.elasticsearch_doc.dig("_source", "reactable", "user", "id")).to eq(delete_user_id)
|
||||
|
||||
sidekiq_perform_enqueued_jobs do
|
||||
described_class.call(admin: admin, keep_user: keep_user, delete_user_id: delete_user.id)
|
||||
|
|
@ -37,8 +35,6 @@ RSpec.describe Moderator::MergeUser, type: :service do
|
|||
drain_all_sidekiq_jobs
|
||||
expect(article.reload.elasticsearch_doc.dig("_source", "user", "id")).to eq(keep_user.id)
|
||||
expect(comment.reload.elasticsearch_doc.dig("_source", "user", "id")).to eq(keep_user.id)
|
||||
expect(reaction.reload.elasticsearch_doc.dig("_source", "user_id")).to eq(keep_user.id)
|
||||
expect(article_reaction.reload.elasticsearch_doc.dig("_source", "reactable", "user", "id")).to eq(keep_user.id)
|
||||
end
|
||||
|
||||
it "updates badge_achievements_count" do
|
||||
|
|
|
|||
|
|
@ -1,92 +0,0 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Search::QueryBuilders::Reaction, type: :service do
|
||||
describe "::initialize" do
|
||||
it "sets params" do
|
||||
filter_params = { foo: "bar" }
|
||||
filter = described_class.new(params: filter_params)
|
||||
expect(filter.params).to include(filter_params)
|
||||
end
|
||||
|
||||
it "builds query body" do
|
||||
filter = described_class.new(params: {})
|
||||
expect(filter.body).not_to be_nil
|
||||
end
|
||||
|
||||
it "sets category to readinglist" do
|
||||
filter = described_class.new(params: {})
|
||||
expect(filter.params).to include(category: "readinglist")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#as_hash" do
|
||||
let(:query_fields) { described_class::QUERY_KEYS[:search_fields] }
|
||||
|
||||
it "applies QUERY_KEYS from params" do
|
||||
params = { search_fields: "test" }
|
||||
filter = described_class.new(params: params)
|
||||
expected_query = [{
|
||||
"simple_query_string" => {
|
||||
"query" => "test",
|
||||
"fields" => query_fields,
|
||||
"lenient" => true,
|
||||
"analyze_wildcard" => true,
|
||||
"minimum_should_match" => 2
|
||||
}
|
||||
}]
|
||||
expect(search_bool_clause(filter)["must"]).to match_array(expected_query)
|
||||
end
|
||||
|
||||
it "applies TERM_KEYS from params" do
|
||||
params = { tag_names: "beginner", user_id: 777, status: "valid" }
|
||||
filter = described_class.new(params: params)
|
||||
expected_filters = [
|
||||
{ "terms" => { "status" => ["valid"] } },
|
||||
{ "terms" => { "reactable.tags.name" => ["beginner"] } },
|
||||
{ "terms" => { "user_id" => [777] } },
|
||||
{ "terms" => { "category" => ["readinglist"] } },
|
||||
]
|
||||
expect(search_bool_clause(filter)["filter"]).to match_array(expected_filters)
|
||||
end
|
||||
|
||||
it "applies QUERY_KEYS and TERM_KEYS from params" do
|
||||
Timecop.freeze(Time.current) do
|
||||
params = { search_fields: "ruby", tag_names: "cfp" }
|
||||
filter = described_class.new(params: params)
|
||||
expected_query = [{
|
||||
"simple_query_string" => { "query" => "ruby", "fields" => query_fields, "lenient" => true,
|
||||
"analyze_wildcard" => true, "minimum_should_match" => 2 }
|
||||
}]
|
||||
expected_filters = [
|
||||
{ "terms" => { "reactable.tags.name" => ["cfp"] } },
|
||||
{ "terms" => { "category" => ["readinglist"] } },
|
||||
]
|
||||
expect(search_bool_clause(filter)["must"]).to match_array(expected_query)
|
||||
expect(search_bool_clause(filter)["filter"]).to match_array(expected_filters)
|
||||
end
|
||||
end
|
||||
|
||||
it "ignores params we don't support" do
|
||||
params = { not_supported: "trash", search_fields: "cfp" }
|
||||
filter = described_class.new(params: params)
|
||||
expected_query = [{
|
||||
"simple_query_string" => {
|
||||
"query" => "cfp", "fields" => query_fields, "lenient" => true,
|
||||
"analyze_wildcard" => true, "minimum_should_match" => 2
|
||||
}
|
||||
}]
|
||||
expect(search_bool_clause(filter)["must"]).to match_array(expected_query)
|
||||
end
|
||||
|
||||
it "allows default params to be overriden" do
|
||||
params = { sort_by: "status", sort_direction: "asc", size: 20 }
|
||||
filter = described_class.new(params: params).as_hash
|
||||
expect(filter["sort"]).to eq("status" => "asc")
|
||||
expect(filter["size"]).to eq(20)
|
||||
end
|
||||
end
|
||||
|
||||
def search_bool_clause(query_builder)
|
||||
query_builder.as_hash.dig("query", "bool")
|
||||
end
|
||||
end
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Search::Reaction, type: :service do
|
||||
it "defines INDEX_NAME, INDEX_ALIAS, and MAPPINGS", :aggregate_failures do
|
||||
expect(described_class::INDEX_NAME).not_to be_nil
|
||||
expect(described_class::INDEX_ALIAS).not_to be_nil
|
||||
expect(described_class::MAPPINGS).not_to be_nil
|
||||
end
|
||||
|
||||
describe "::search_documents", elasticsearch: "Reaction" do
|
||||
let(:article1) { create(:article) }
|
||||
let(:article2) { create(:article) }
|
||||
let(:reaction1) { create(:reaction, category: "readinglist", reactable: article1, created_at: 1.week.ago) }
|
||||
let(:reaction2) { create(:reaction, category: "readinglist", reactable: article2, created_at: Time.current) }
|
||||
let(:query_params) { { size: 5 } }
|
||||
|
||||
it "parses reaction document hits from search response" do
|
||||
mock_search_response = { "hits" => { "hits" => {} } }
|
||||
allow(described_class).to receive(:search) { mock_search_response }
|
||||
described_class.search_documents(params: {})
|
||||
expect(described_class).to have_received(:search).with(body: a_kind_of(Hash))
|
||||
end
|
||||
|
||||
it "returns fields necessary for the view" do
|
||||
view_keys = %w[id reactable]
|
||||
reactable_keys = %w[title path published_date_string tags user]
|
||||
user_keys = %w[username name profile_image_90]
|
||||
index_documents([reaction1])
|
||||
|
||||
result = described_class.search_documents(params: { size: 1 })
|
||||
reaction_doc = result["reactions"].first
|
||||
expect(result["total"]).to be_present
|
||||
expect(reaction_doc.keys).to include(*view_keys)
|
||||
expect(reaction_doc["reactable"].keys).to include(*reactable_keys)
|
||||
expect(reaction_doc.dig("reactable", "user").keys).to include(*user_keys)
|
||||
end
|
||||
|
||||
context "with a query" do
|
||||
it "searches by search_fields" do
|
||||
allow(article1).to receive(:title).and_return("ruby")
|
||||
allow(article2).to receive(:body_text).and_return("Ruby Tuesday")
|
||||
index_documents([reaction1, reaction2])
|
||||
query_params[:search_fields] = "ruby"
|
||||
|
||||
reaction_docs = described_class.search_documents(params: query_params)["reactions"]
|
||||
expect(reaction_docs.count).to eq(2)
|
||||
doc_ids = reaction_docs.map { |t| t["id"] }
|
||||
expect(doc_ids).to include(reaction1.id, reaction2.id)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a filter term" do
|
||||
let(:tag_one) { create(:tag) }
|
||||
let(:tag_two) { create(:tag) }
|
||||
|
||||
it "filters by tag names" do
|
||||
article1.tags << tag_one
|
||||
article2.tags << tag_two
|
||||
index_documents([reaction1, reaction2])
|
||||
query_params[:tag_names] = [tag_one.name]
|
||||
|
||||
reaction_docs = described_class.search_documents(params: query_params)["reactions"]
|
||||
expect(reaction_docs.count).to eq(1)
|
||||
doc_ids = reaction_docs.map { |t| t["id"] }
|
||||
expect(doc_ids).to include(reaction1.id)
|
||||
end
|
||||
|
||||
it "filters by multiple tag names when tag_boolean_mode is set to all" do
|
||||
article1.tags << tag_one
|
||||
article2.tags << tag_two
|
||||
article2.tags << tag_one
|
||||
index_documents([reaction1, reaction2])
|
||||
query_params[:tag_names] = [tag_one.name, tag_two.name]
|
||||
query_params[:tag_boolean_mode] = "all"
|
||||
|
||||
reaction_docs = described_class.search_documents(params: query_params)["reactions"]
|
||||
expect(reaction_docs.count).to eq(1)
|
||||
doc_ids = reaction_docs.map { |t| t["id"] }
|
||||
expect(doc_ids).to include(reaction2.id)
|
||||
end
|
||||
|
||||
it "filters by user_id" do
|
||||
index_documents([reaction1, reaction2])
|
||||
query_params[:user_id] = reaction1.user_id
|
||||
|
||||
reaction_docs = described_class.search_documents(params: query_params)["reactions"]
|
||||
expect(reaction_docs.count).to eq(1)
|
||||
doc_ids = reaction_docs.map { |t| t["id"] }
|
||||
expect(doc_ids).to include(reaction1.id)
|
||||
end
|
||||
|
||||
it "filters by status" do
|
||||
reaction1.update(status: "invalid")
|
||||
index_documents([reaction1, reaction2])
|
||||
query_params[:status] = %w[valid confirmed]
|
||||
|
||||
reaction_docs = described_class.search_documents(params: query_params)["reactions"]
|
||||
expect(reaction_docs.count).to eq(1)
|
||||
doc_ids = reaction_docs.map { |t| t["id"] }
|
||||
expect(doc_ids).to include(reaction2.id)
|
||||
end
|
||||
|
||||
it "filters by category by default" do
|
||||
reaction1.update(category: "like")
|
||||
index_documents([reaction1, reaction2])
|
||||
|
||||
reaction_docs = described_class.search_documents(params: query_params)["reactions"]
|
||||
expect(reaction_docs.count).to eq(1)
|
||||
doc_ids = reaction_docs.map { |t| t["id"] }
|
||||
expect(doc_ids).to include(reaction2.id)
|
||||
end
|
||||
end
|
||||
|
||||
context "with default sorting" do
|
||||
it "sorts by created_at" do
|
||||
index_documents([reaction1, reaction2])
|
||||
|
||||
reaction_docs = described_class.search_documents(params: query_params)["reactions"]
|
||||
doc_ids = reaction_docs.map { |t| t["id"] }
|
||||
expect(doc_ids).to eq([reaction2.id, reaction1.id])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -66,20 +66,6 @@ RSpec.describe Users::Delete, type: :service do
|
|||
expect { article.elasticsearch_doc }.to raise_error(Search::Errors::Transport::NotFound)
|
||||
end
|
||||
|
||||
it "removes reactions from Elasticsearch" do
|
||||
article = create(:article, user: user)
|
||||
reaction = create(:reaction, category: "readinglist", reactable: article)
|
||||
user_reaction = create(:reaction, user_id: user.id, category: "readinglist")
|
||||
sidekiq_perform_enqueued_jobs
|
||||
expect(reaction.elasticsearch_doc).not_to be_nil
|
||||
expect(user_reaction.elasticsearch_doc).not_to be_nil
|
||||
sidekiq_perform_enqueued_jobs do
|
||||
described_class.call(user)
|
||||
end
|
||||
expect { reaction.elasticsearch_doc }.to raise_error(Search::Errors::Transport::NotFound)
|
||||
expect { user_reaction.elasticsearch_doc }.to raise_error(Search::Errors::Transport::NotFound)
|
||||
end
|
||||
|
||||
it "deletes field tests memberships" do
|
||||
create(:field_test_membership, participant_id: user.id)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,19 +4,19 @@ RSpec.describe Search::BulkIndexWorker, type: :worker do
|
|||
let(:worker) { subject }
|
||||
let(:article) { create(:article) }
|
||||
|
||||
include_examples "#enqueues_on_correct_queue", "high_priority", ["Reaction", 1]
|
||||
include_examples "#enqueues_on_correct_queue", "high_priority", ["Tag", 1]
|
||||
|
||||
it "indexes documents for a set of given ids and object class", elasticsearch: "Reaction" do
|
||||
reactions = [create(:reaction, reactable: article), create(:reaction), create(:reaction)]
|
||||
it "indexes documents for a set of given ids and object class", elasticsearch: "Tag" do
|
||||
tags = [create(:tag), create(:tag), create(:tag)]
|
||||
Sidekiq::Worker.clear_all
|
||||
|
||||
reactions.each do |reaction|
|
||||
expect { reaction.elasticsearch_doc }.to raise_error(Search::Errors::Transport::NotFound)
|
||||
tags.each do |tag|
|
||||
expect { tag.elasticsearch_doc }.to raise_error(Search::Errors::Transport::NotFound)
|
||||
end
|
||||
worker.perform("Reaction", reactions.map(&:id))
|
||||
worker.perform("Tag", tags.map(&:id))
|
||||
|
||||
reactions.each do |reaction|
|
||||
expect(reaction.elasticsearch_doc.dig("_source", "id")).to eql(reaction.id)
|
||||
tags.each do |tag|
|
||||
expect(tag.elasticsearch_doc.dig("_source", "id")).to eql(tag.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue