287 lines
8.8 KiB
Ruby
287 lines
8.8 KiB
Ruby
class BookmarkableQuery < Query
|
|
include TaggableQuery
|
|
|
|
attr_accessor :bookmark_query
|
|
|
|
# Rather than compute this information twice, we rely on the BookmarkQuery
|
|
# class to calculate information about sorting.
|
|
delegate :sort_column, :sort_direction,
|
|
to: :bookmark_query
|
|
|
|
# The "klass" function here returns the class name used to load search
|
|
# results. The BookmarkableQuery is unique among Query classes because it can
|
|
# return objects from more than one table, so we need to use a special class
|
|
# that can handle IDs of multiple types.
|
|
def klass
|
|
'BookmarkableDecorator'
|
|
end
|
|
|
|
def index_name
|
|
BookmarkableIndexer.index_name
|
|
end
|
|
|
|
def document_type
|
|
BookmarkableIndexer.document_type
|
|
end
|
|
|
|
# The BookmarkableQuery is unique among queries in that it depends wholly on
|
|
# the BookmarkQuery for all of its options. So we have a slightly different
|
|
# constructor.
|
|
def initialize(bookmark_query)
|
|
self.bookmark_query = bookmark_query
|
|
@options = bookmark_query.options
|
|
end
|
|
|
|
# Combine the filters and queries for both the bookmark and the bookmarkable.
|
|
def filtered_query
|
|
make_bool(
|
|
# All queries/filters/exclusion filters for the bookmark are wrapped in a
|
|
# single has_child query by the bookmark_filter function:
|
|
must: bookmark_filter,
|
|
# We never sort by score, so we can always ignore the score on our
|
|
# queries, grouping them together with our filters. (Note, however, that
|
|
# the bookmark search can incorporate our score, so there is a
|
|
# distinction between queries and filters -- just not in this function.)
|
|
filter: make_list(queries, filters),
|
|
must_not: exclusion_filters
|
|
)
|
|
end
|
|
|
|
# Queries that apply only to the bookmarkable. Bookmark queries are handled
|
|
# in filtered_query, and should not be included here.
|
|
def queries
|
|
@queries ||= make_list(
|
|
general_query
|
|
)
|
|
end
|
|
|
|
# Filters that apply only to the bookmarkable. Bookmark filters are handled
|
|
# in filtered_query, and should not be included here.
|
|
def filters
|
|
@filters ||= make_list(
|
|
complete_filter,
|
|
language_filter,
|
|
filter_id_filter,
|
|
named_tag_inclusion_filter,
|
|
date_filter
|
|
)
|
|
end
|
|
|
|
# Exclusion filters that apply only to the bookmarkable. Exclusion filters
|
|
# for the bookmark are handled in filtered_query, and should not be included
|
|
# here.
|
|
def exclusion_filters
|
|
@exclusion_filters ||= make_list(
|
|
unposted_filter,
|
|
hidden_filter,
|
|
restricted_filter,
|
|
tag_exclusion_filter,
|
|
named_tag_exclusion_filter
|
|
)
|
|
end
|
|
|
|
####################
|
|
# QUERIES
|
|
####################
|
|
|
|
def general_query
|
|
return nil if bookmarkable_query_text.blank?
|
|
|
|
{ query_string: { query: bookmarkable_query_text, default_operator: "AND" } }
|
|
end
|
|
|
|
def bookmarkable_query_text
|
|
query_text = (options[:bookmarkable_query] || "").dup
|
|
escape_slashes(query_text.strip)
|
|
end
|
|
|
|
####################
|
|
# SORTING AND AGGREGATIONS
|
|
####################
|
|
|
|
# When sorting by bookmarkable date, we use the revised_at field to order the
|
|
# results. When sorting by created_at, we use _score to sort (because the
|
|
# only way to sort by a child's fields is to store the value in the _score
|
|
# field and sort by score).
|
|
def sort
|
|
if sort_column == "bookmarkable_date"
|
|
sort_hash = { revised_at: { order: sort_direction, unmapped_type: "date" } }
|
|
else
|
|
sort_hash = { _score: { order: sort_direction } }
|
|
end
|
|
|
|
[sort_hash, { sort_id: { order: sort_direction } }]
|
|
end
|
|
|
|
# Define the aggregations for just the bookmarkable. This is combined with
|
|
# the bookmark's aggregations below.
|
|
def bookmarkable_aggregations
|
|
aggs = {}
|
|
|
|
if bookmark_query.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
|
|
end
|
|
|
|
# Combine the bookmarkable aggregations with the bookmark aggregations from
|
|
# the bookmark query.
|
|
def aggregations
|
|
aggs = bookmarkable_aggregations
|
|
|
|
bookmark_aggregations = bookmark_query.bookmark_aggregations
|
|
if bookmark_aggregations.present?
|
|
aggs[:bookmarks] = {
|
|
# Aggregate on our child bookmarks.
|
|
children: { type: "bookmark" },
|
|
aggs: {
|
|
filtered_bookmarks: {
|
|
filter: bookmark_bool,
|
|
aggs: bookmark_aggregations
|
|
}
|
|
}
|
|
}
|
|
end
|
|
|
|
{ aggs: aggs } if aggs.present?
|
|
end
|
|
|
|
####################
|
|
# BOOKMARKS
|
|
####################
|
|
|
|
# Create a single has_child query with ALL of the child's queries and filters
|
|
# included. In order to avoid issues with multiple bookmarks combining to
|
|
# create an (incorrect) bookmarkable match, there MUST be exactly one
|
|
# has_child query. (Plus, it probably makes it faster.)
|
|
def bookmark_filter
|
|
bool = bookmark_bool
|
|
|
|
# If we're sorting by created_at, we actually need to fetch the bookmarks'
|
|
# created_at as the score of this query, so that we can sort by score (and
|
|
# therefore by the bookmarks' created_at).
|
|
bool = field_value_score("created_at", bool) if sort_column == "created_at"
|
|
|
|
{
|
|
has_child: {
|
|
type: "bookmark",
|
|
score_mode: "max",
|
|
query: bool,
|
|
inner_hits: {
|
|
size: inner_hits_size,
|
|
sort: { created_at: { order: "desc", unmapped_type: "date" } }
|
|
}
|
|
}
|
|
}
|
|
end
|
|
|
|
# The bool used in the has_child query and to filter the bookmark
|
|
# aggregations. Contains all of the constraints on bookmarks, and no
|
|
# constraints on bookmarkables.
|
|
def bookmark_bool
|
|
make_bool(
|
|
must: bookmark_query.queries,
|
|
filter: bookmark_query.filters,
|
|
must_not: bookmark_query.exclusion_filters
|
|
)
|
|
end
|
|
|
|
####################
|
|
# FILTERS
|
|
####################
|
|
|
|
def complete_filter
|
|
term_filter(:complete, 'true') if options[:complete].present?
|
|
end
|
|
|
|
def language_filter
|
|
term_filter(:"language_id.keyword", options[:language_id]) if options[:language_id].present?
|
|
end
|
|
|
|
def filter_id_filter
|
|
if filter_ids.present?
|
|
filter_ids.map { |filter_id| term_filter(:filter_ids, filter_id) }
|
|
end
|
|
end
|
|
|
|
# The date filter on the bookmarkable (i.e. when the bookmarkable was last
|
|
# updated).
|
|
def date_filter
|
|
if options[:bookmarkable_date].present?
|
|
{ range: { revised_at: SearchRange.parsed(options[:bookmarkable_date]) } }
|
|
end
|
|
end
|
|
|
|
# Exclude drafts from bookmarkable search results.
|
|
# Note that this is used as an exclusion filter, not an inclusion filter, so
|
|
# the boolean is flipped from the way you might expect.
|
|
def unposted_filter
|
|
term_filter(:posted, 'false')
|
|
end
|
|
|
|
# Exclude items hidden by admin from bookmarkable search results.
|
|
# Note that this is used as an exclusion filter, not an inclusion filter, so
|
|
# the boolean is flipped from the way you might expect.
|
|
def hidden_filter
|
|
term_filter(:hidden_by_admin, 'true')
|
|
end
|
|
|
|
# Exclude restricted works/series when the user isn't logged in.
|
|
# Note that this is used as an exclusion filter, not an inclusion filter, so
|
|
# the boolean is flipped from the way you might expect.
|
|
def restricted_filter
|
|
term_filter(:restricted, 'true') unless include_restricted?
|
|
end
|
|
|
|
def tag_exclusion_filter
|
|
if exclusion_ids.present?
|
|
terms_filter(:filter_ids, exclusion_ids)
|
|
end
|
|
end
|
|
|
|
# This filter is used to restrict our results to only include bookmarkables
|
|
# 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 bookmarkables
|
|
# 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.
|
|
#
|
|
# Note that we separate these into different filters to get the logic of tag
|
|
# exclusion right: if we're excluding "A B" and "C D", we want the query to
|
|
# be "not(A and B) and not(C and D)", which can't be accomplished in a single
|
|
# match query.
|
|
def named_tag_exclusion_filter
|
|
excluded_tag_names.map do |tag_name|
|
|
match_filter(:tag, tag_name)
|
|
end
|
|
end
|
|
|
|
####################
|
|
# HELPERS
|
|
####################
|
|
|
|
# The number of bookmarks to return with each bookmarkable.
|
|
def inner_hits_size
|
|
ArchiveConfig.NUMBER_OF_BOOKMARKS_SHOWN_PER_BOOKMARKABLE || 5
|
|
end
|
|
|
|
def include_restricted?
|
|
# Use fetch instead of || here to make sure that we don't accidentally
|
|
# override a deliberate choice not to show restricted bookmarks.
|
|
options.fetch(:show_restricted, User.current_user.present?)
|
|
end
|
|
end
|