370 lines
9.3 KiB
Ruby
370 lines
9.3 KiB
Ruby
class WorkQuery < Query
|
|
include TaggableQuery
|
|
|
|
def klass
|
|
'Work'
|
|
end
|
|
|
|
def index_name
|
|
WorkIndexer.index_name
|
|
end
|
|
|
|
def document_type
|
|
WorkIndexer.document_type
|
|
end
|
|
|
|
# Combine the available filters
|
|
def filters
|
|
add_owner
|
|
|
|
@filters ||= (
|
|
visibility_filters +
|
|
work_filters +
|
|
creator_filters +
|
|
collection_filters +
|
|
tag_filters +
|
|
range_filters
|
|
).flatten.compact
|
|
end
|
|
|
|
def exclusion_filters
|
|
@exclusion_filters ||= [
|
|
tag_exclusion_filter,
|
|
named_tag_exclusion_filter
|
|
].flatten.compact
|
|
end
|
|
|
|
# Combine the available queries
|
|
# In this case, name is the only text field
|
|
def queries
|
|
@queries = [
|
|
general_query
|
|
].flatten.compact
|
|
end
|
|
|
|
def add_owner
|
|
owner = options[:works_parent]
|
|
|
|
if owner.is_a?(Language)
|
|
options[:language_id] = owner.short
|
|
return
|
|
end
|
|
|
|
field = case owner
|
|
when Tag
|
|
:filter_ids
|
|
when Pseud
|
|
:pseud_ids
|
|
when User
|
|
:user_ids
|
|
when Collection
|
|
:collection_ids
|
|
end
|
|
return unless field.present?
|
|
options[field] ||= []
|
|
options[field] << owner.id
|
|
end
|
|
|
|
####################
|
|
# GROUPS OF FILTERS
|
|
####################
|
|
|
|
def visibility_filters
|
|
[
|
|
posted_filter,
|
|
hidden_filter,
|
|
restricted_filter,
|
|
unrevealed_filter,
|
|
anon_filter
|
|
]
|
|
end
|
|
|
|
def work_filters
|
|
[
|
|
complete_filter,
|
|
single_chapter_filter,
|
|
language_filter,
|
|
crossover_filter,
|
|
type_filter
|
|
]
|
|
end
|
|
|
|
def creator_filters
|
|
[user_filter, pseud_filter]
|
|
end
|
|
|
|
def collection_filters
|
|
[collection_filter]
|
|
end
|
|
|
|
def tag_filters
|
|
[
|
|
filter_id_filter,
|
|
named_tag_inclusion_filter
|
|
].flatten.compact
|
|
end
|
|
|
|
def range_filters
|
|
ranges = []
|
|
[:word_count, :hits, :kudos_count, :comments_count, :bookmarks_count, :revised_at].each do |countable|
|
|
if options[countable].present?
|
|
ranges << { range: { countable => SearchRange.parsed(options[countable]) } }
|
|
end
|
|
end
|
|
ranges += [date_range_filter, word_count_filter].compact
|
|
ranges
|
|
end
|
|
|
|
####################
|
|
# FILTERS
|
|
####################
|
|
|
|
def posted_filter
|
|
term_filter(:posted, 'true')
|
|
end
|
|
|
|
def hidden_filter
|
|
term_filter(:hidden_by_admin, 'false')
|
|
end
|
|
|
|
def restricted_filter
|
|
term_filter(:restricted, 'false') unless include_restricted?
|
|
end
|
|
|
|
def unrevealed_filter
|
|
term_filter(:in_unrevealed_collection, 'false') unless include_unrevealed?
|
|
end
|
|
|
|
def anon_filter
|
|
term_filter(:in_anon_collection, 'false') unless include_anon?
|
|
end
|
|
|
|
def complete_filter
|
|
term_filter(:complete, bool_value(options[:complete])) if options[:complete].present?
|
|
end
|
|
|
|
def single_chapter_filter
|
|
term_filter(:expected_number_of_chapters, 1) if options[:single_chapter].present?
|
|
end
|
|
|
|
def language_filter
|
|
term_filter(:"language_id.keyword", options[:language_id]) if options[:language_id].present?
|
|
end
|
|
|
|
def crossover_filter
|
|
term_filter(:crossover, bool_value(options[:crossover])) if options[:crossover].present?
|
|
end
|
|
|
|
def type_filter
|
|
terms_filter(:work_type, options[:work_types]) if options[:work_types]
|
|
end
|
|
|
|
def user_filter
|
|
return if user_ids.blank?
|
|
|
|
if viewing_own_collected_works_page?
|
|
{
|
|
has_child: {
|
|
type: "creator",
|
|
query: terms_filter(:private_user_ids, user_ids)
|
|
}
|
|
}
|
|
else
|
|
terms_filter(:user_ids, user_ids)
|
|
end
|
|
end
|
|
|
|
def pseud_filter
|
|
terms_filter(:pseud_ids, pseud_ids) if pseud_ids.present?
|
|
end
|
|
|
|
def collection_filter
|
|
terms_filter(:collection_ids, options[:collection_ids]) if options[:collection_ids].present?
|
|
end
|
|
|
|
def filter_id_filter
|
|
if filter_ids.present?
|
|
filter_ids.map { |filter_id| term_filter(:filter_ids, filter_id) }
|
|
end
|
|
end
|
|
|
|
def tag_exclusion_filter
|
|
if exclusion_ids.present?
|
|
exclusion_ids.map { |exclusion_id| term_filter(:filter_ids, exclusion_id) }
|
|
end
|
|
end
|
|
|
|
# This filter is used to restrict our results to only include works
|
|
# whose "tag" text matches all of the tag names in included_tag_names. This
|
|
# is useful when the user enters a non-existent tag, which would be discarded
|
|
# by the TaggableQuery.filter_ids function.
|
|
def named_tag_inclusion_filter
|
|
return if included_tag_names.blank?
|
|
match_filter(:tag, included_tag_names.join(" "))
|
|
end
|
|
|
|
# This set of filters is used to prevent us from matching any works whose
|
|
# "tag" text matches one of the passed-in tag names. This is useful when the
|
|
# user enters a non-existent tag, which would be discarded by the
|
|
# TaggableQuery.exclusion_ids function.
|
|
#
|
|
# Unlike the inclusion filter, we must separate these into different match
|
|
# filters to get the results that we want (that is, excluding "A B" and "C D"
|
|
# is the same as "not(A and B) and not(C and D)").
|
|
def named_tag_exclusion_filter
|
|
excluded_tag_names.map do |tag_name|
|
|
match_filter(:tag, tag_name)
|
|
end
|
|
end
|
|
|
|
def date_range_filter
|
|
return unless options[:date_from].present? || options[:date_to].present?
|
|
begin
|
|
range = {}
|
|
range[:gte] = clamp_search_date(options[:date_from].to_date) if options[:date_from].present?
|
|
range[:lte] = clamp_search_date(options[:date_to].to_date) if options[:date_to].present?
|
|
{ range: { revised_at: range } }
|
|
rescue ArgumentError
|
|
nil
|
|
end
|
|
end
|
|
|
|
def word_count_filter
|
|
return unless options[:words_from].present? || options[:words_to].present?
|
|
range = {}
|
|
range[:gte] = options[:words_from].delete(",._").to_i if options[:words_from].present?
|
|
range[:lte] = options[:words_to].delete(",._").to_i if options[:words_to].present?
|
|
{ range: { word_count: range } }
|
|
end
|
|
|
|
####################
|
|
# QUERIES
|
|
####################
|
|
|
|
# Search for a tag by name
|
|
# Note that fields don't need to be explicitly included in the
|
|
# field list to be searchable directly (ie, "complete:true" will still work)
|
|
def general_query
|
|
input = (options[:q] || options[:query] || "").dup
|
|
query = generate_search_text(input)
|
|
|
|
return {
|
|
query_string: {
|
|
query: query,
|
|
fields: ["creators^5", "title^7", "endnotes", "notes", "summary", "tag", "series.title"],
|
|
default_operator: "AND"
|
|
}
|
|
} unless query.blank?
|
|
end
|
|
|
|
def generate_search_text(query = '')
|
|
search_text = query
|
|
%i[title creators].each do |field|
|
|
search_text << split_query_text_words(field, options[field])
|
|
end
|
|
|
|
if options[:series_titles].present?
|
|
search_text << split_query_text_words("series.title", options[:series_titles])
|
|
end
|
|
|
|
if options[:collection_ids].blank? && collected?
|
|
search_text << " collection_ids:*"
|
|
end
|
|
escape_slashes(search_text.strip)
|
|
end
|
|
|
|
def sort
|
|
column = options[:sort_column].present? ? options[:sort_column] : default_sort
|
|
direction = options[:sort_direction].present? ? options[:sort_direction] : 'desc'
|
|
sort_hash = { column => { order: direction } }
|
|
|
|
if column == 'revised_at'
|
|
sort_hash[column][:unmapped_type] = 'date'
|
|
end
|
|
|
|
[sort_hash, { id: { order: direction } }]
|
|
end
|
|
|
|
# When searching outside of filters, use relevance instead of date
|
|
def default_sort
|
|
facet_tags? || collected? ? 'revised_at' : '_score'
|
|
end
|
|
|
|
def aggregations
|
|
aggs = {}
|
|
if collected?
|
|
aggs[:collections] = { terms: { field: 'collection_ids' } }
|
|
end
|
|
|
|
if facet_tags?
|
|
%w(rating archive_warning category fandom character relationship freeform).each do |facet_type|
|
|
aggs[facet_type] = { terms: { field: "#{facet_type}_ids" } }
|
|
end
|
|
end
|
|
|
|
{ aggs: aggs }
|
|
end
|
|
|
|
def works_per_language(languages_count)
|
|
response = $elasticsearch.search(index: index_name, body: {
|
|
size: 0,
|
|
query: filtered_query,
|
|
aggregations: {
|
|
languages: {
|
|
terms: { field: "language_id.keyword", size: languages_count }
|
|
}
|
|
}
|
|
})
|
|
language_counts = response.dig("aggregations", "languages", "buckets") || []
|
|
language_counts.map(&:values).to_h
|
|
end
|
|
|
|
####################
|
|
# HELPERS
|
|
####################
|
|
|
|
def facet_tags?
|
|
options[:faceted]
|
|
end
|
|
|
|
def collected?
|
|
options[:collected]
|
|
end
|
|
|
|
def viewing_own_collected_works_page?
|
|
collected? && options[:works_parent].present? &&
|
|
options[:works_parent] == User.current_user
|
|
end
|
|
|
|
def include_restricted?
|
|
User.current_user.present? || options[:show_restricted]
|
|
end
|
|
|
|
# Include unrevealed works only if we're on a collection page
|
|
# OR the collected works page of a user
|
|
def include_unrevealed?
|
|
options[:collection_ids].present? || collected?
|
|
end
|
|
|
|
# Include anonymous works if we're not on a user/pseud page
|
|
# OR if the user is viewing their own collected works
|
|
def include_anon?
|
|
(user_ids.blank? && pseud_ids.blank?) ||
|
|
viewing_own_collected_works_page?
|
|
end
|
|
|
|
def user_ids
|
|
options[:user_ids]
|
|
end
|
|
|
|
def pseud_ids
|
|
options[:pseud_ids]
|
|
end
|
|
|
|
# By default, ES6 expects yyyy-MM-dd and can't parse years with 4+ digits.
|
|
def clamp_search_date(date)
|
|
return date.change(year: 0) if date.year.negative?
|
|
return date.change(year: 9999) if date.year > 9999
|
|
date
|
|
end
|
|
end
|