364 lines
10 KiB
Ruby
364 lines
10 KiB
Ruby
class BookmarkQuery < Query
|
|
attr_accessor :bookmarkable_query
|
|
|
|
def klass
|
|
'Bookmark'
|
|
end
|
|
|
|
def index_name
|
|
BookmarkIndexer.index_name
|
|
end
|
|
|
|
def document_type
|
|
BookmarkIndexer.document_type
|
|
end
|
|
|
|
# Load the options and create the linked BookmarkableQuery class (which is
|
|
# used to generate all of our parent filters).
|
|
def initialize(options = {})
|
|
@options = HashWithIndifferentAccess.new(options)
|
|
add_owner
|
|
self.bookmarkable_query = BookmarkableQuery.new(self)
|
|
end
|
|
|
|
# Combine the filters and queries for both the bookmark and the bookmarkable.
|
|
def filtered_query
|
|
make_bool(
|
|
# Score is based on our query + the bookmarkable query:
|
|
must: make_list(queries, bookmarkable_queries_and_filters),
|
|
filter: filters,
|
|
must_not: make_list(exclusion_filters, bookmarkable_exclusion_filters)
|
|
)
|
|
end
|
|
|
|
# Queries that apply only to the bookmark. Bookmarkable 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 bookmark. Bookmarkable filters are handled
|
|
# in filtered_query, and should not be included here.
|
|
def filters
|
|
@filters ||= make_list(
|
|
privacy_filter,
|
|
hidden_filter,
|
|
bookmarks_only_filter,
|
|
pseud_filter,
|
|
user_filter,
|
|
rec_filter,
|
|
notes_filter,
|
|
tags_filter,
|
|
named_tag_inclusion_filter,
|
|
collections_filter,
|
|
type_filter,
|
|
date_filter
|
|
)
|
|
end
|
|
|
|
# Exclusion filters that apply only to the bookmark. Exclusion filters for
|
|
# the bookmarkable are handled in filtered_query, and should not be included
|
|
# here.
|
|
def exclusion_filters
|
|
@exclusion_filters ||= make_list(
|
|
tag_exclusion_filter,
|
|
named_tag_exclusion_filter
|
|
)
|
|
end
|
|
|
|
def add_owner
|
|
owner = options[:parent]
|
|
field = case owner
|
|
when Tag
|
|
# Note that in a bookmark search for a Tag owner, we want to return
|
|
# the bookmarkables, not bookmarks, with that tag.
|
|
# This field will be handled in the linked BookmarkableQuery.
|
|
: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
|
|
|
|
####################
|
|
# QUERIES
|
|
####################
|
|
|
|
def general_query
|
|
return nil if bookmark_query_text.blank?
|
|
|
|
{ query_string: { query: bookmark_query_text, default_operator: "AND" } }
|
|
end
|
|
|
|
def bookmark_query_text
|
|
query_text = (options[:bookmark_query] || "").dup
|
|
query_text << split_query_text_words(:bookmarker, options[:bookmarker])
|
|
query_text << split_query_text_words(:notes, options[:notes])
|
|
escape_slashes(query_text.strip)
|
|
end
|
|
|
|
####################
|
|
# SORTING AND AGGREGATIONS
|
|
####################
|
|
|
|
def sort_column
|
|
@sort_column ||=
|
|
options[:sort_column].present? ? options[:sort_column] : default_sort
|
|
end
|
|
|
|
def sort_direction
|
|
@sort_direction ||=
|
|
options[:sort_direction].present? ? options[:sort_direction] : "desc"
|
|
end
|
|
|
|
def default_sort
|
|
facet_tags? ? 'created_at' : '_score'
|
|
end
|
|
|
|
def sort
|
|
sort_hash = { sort_column => { order: sort_direction } }
|
|
|
|
if %w(created_at bookmarkable_date).include?(sort_column)
|
|
sort_hash[sort_column][:unmapped_type] = 'date'
|
|
end
|
|
|
|
[sort_hash, { id: { order: sort_direction } }]
|
|
end
|
|
|
|
# The aggregations for just the bookmarks:
|
|
def bookmark_aggregations
|
|
aggs = {}
|
|
|
|
if facet_collections?
|
|
aggs[:collections] = { terms: { field: 'collection_ids' } }
|
|
end
|
|
|
|
if facet_tags?
|
|
aggs[:tag] = { terms: { field: "tag_ids" } }
|
|
end
|
|
|
|
aggs
|
|
end
|
|
|
|
# Combine the bookmark aggregations with the bookmarkable aggregations from
|
|
# the bookmarkable query.
|
|
def aggregations
|
|
aggs = bookmark_aggregations
|
|
|
|
bookmarkable_aggregations = bookmarkable_query.bookmarkable_aggregations
|
|
if bookmarkable_aggregations.present?
|
|
aggs[:bookmarkable] = {
|
|
parent: { type: "bookmark" },
|
|
aggs: bookmarkable_aggregations
|
|
}
|
|
end
|
|
|
|
{ aggs: aggs } if aggs.present?
|
|
end
|
|
|
|
####################
|
|
# BOOKMARKABLE
|
|
####################
|
|
|
|
# Wrap both the queries and the filters from the bookmarkable query into a
|
|
# single has_parent query. (The fewer has_parent queries we have, the faster
|
|
# the query will be.)
|
|
def bookmarkable_queries_and_filters
|
|
bool = make_bool(
|
|
must: bookmarkable_query.queries,
|
|
filter: bookmarkable_query.filters
|
|
)
|
|
|
|
return if bool.nil?
|
|
|
|
{
|
|
has_parent: {
|
|
parent_type: "bookmarkable",
|
|
score: true, # include the score from the bookmarkable
|
|
query: bool
|
|
}
|
|
}
|
|
end
|
|
|
|
# Wrap all of the must_not/not filters on the bookmarkable into a single
|
|
# has_parent query. Note that we wrap them in a should/or query because if
|
|
# any of the parent queries return true, we want to return false. (De
|
|
# Morgan's Law.)
|
|
def bookmarkable_exclusion_filters
|
|
return if bookmarkable_query.exclusion_filters.blank?
|
|
|
|
{
|
|
has_parent: {
|
|
parent_type: "bookmarkable",
|
|
query: make_bool(
|
|
should: bookmarkable_query.exclusion_filters
|
|
)
|
|
}
|
|
}
|
|
end
|
|
|
|
####################
|
|
# FILTERS
|
|
####################
|
|
|
|
def privacy_filter
|
|
term_filter(:private, 'false') unless include_private?
|
|
end
|
|
|
|
def hidden_filter
|
|
term_filter(:hidden_by_admin, 'false')
|
|
end
|
|
|
|
def rec_filter
|
|
term_filter(:rec, 'true') if %w(1 true).include?(options[:rec].to_s)
|
|
end
|
|
|
|
def notes_filter
|
|
term_filter(:with_notes, 'true') if %w(1 true).include?(options[:with_notes].to_s)
|
|
end
|
|
|
|
def type_filter
|
|
term_filter(:bookmarkable_type, options[:bookmarkable_type].gsub(" ", "")) if options[:bookmarkable_type].present?
|
|
end
|
|
|
|
# The date filter on the bookmark (i.e. when the bookmark was created).
|
|
def date_filter
|
|
if options[:date].present?
|
|
{ range: { created_at: SearchRange.parsed(options[:date]) } }
|
|
end
|
|
end
|
|
|
|
def pseud_filter
|
|
if options[:pseud_ids].present?
|
|
terms_filter(:pseud_id, options[:pseud_ids].flatten.uniq)
|
|
end
|
|
end
|
|
|
|
def user_filter
|
|
return unless options[:user_ids].present?
|
|
options[:user_ids].flatten.uniq.map { |user_id| term_filter(:user_id, user_id) }
|
|
end
|
|
|
|
def tags_filter
|
|
if included_bookmark_tag_ids.present?
|
|
included_bookmark_tag_ids.map { |tag_id| term_filter(:tag_ids, tag_id) }
|
|
end
|
|
end
|
|
|
|
def collections_filter
|
|
terms_filter(:collection_ids, options[:collection_ids]) if options[:collection_ids].present?
|
|
end
|
|
|
|
def tag_exclusion_filter
|
|
terms_filter(:tag_ids, excluded_bookmark_tag_ids) if excluded_bookmark_tag_ids.present?
|
|
end
|
|
|
|
# We don't want to accidentally return Bookmarkable documents when we're
|
|
# doing a search for Bookmarks. So we should only include documents that are
|
|
# marked as "bookmark" in their bookmarkable_join field.
|
|
def bookmarks_only_filter
|
|
term_filter(:bookmarkable_join, "bookmark")
|
|
end
|
|
|
|
# This filter is used to restrict our results to only include bookmarks whose
|
|
# "tag" text matches all of the tag names in included_bookmark_tag_names.
|
|
# This is useful when the user enters a non-existent tag, which would be
|
|
# discarded by the included_bookmark_tag_ids function.
|
|
def named_tag_inclusion_filter
|
|
return if included_bookmark_tag_names.blank?
|
|
match_filter(:tag, included_bookmark_tag_names.join(" "))
|
|
end
|
|
|
|
# This set of filters is used to prevent us from matching any bookmarks
|
|
# 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
|
|
# excluded_bookmark_tag_ids function.
|
|
#
|
|
# Unlike the inclusion filter, we separate the queries to make sure that with
|
|
# tags "A B" and "C D", we're searching for "not(A and B) and not(C and D)",
|
|
# instead of "not(A and B and C and D)" or "not(A or B or C or D)".
|
|
def named_tag_exclusion_filter
|
|
excluded_bookmark_tag_names.map do |tag_name|
|
|
match_filter(:tag, tag_name)
|
|
end
|
|
end
|
|
|
|
####################
|
|
# HELPERS
|
|
####################
|
|
|
|
def facet_tags?
|
|
options[:faceted]
|
|
end
|
|
|
|
def facet_collections?
|
|
false
|
|
end
|
|
|
|
def include_private?
|
|
# Use fetch instead of || here to make sure that we don't accidentally
|
|
# override a deliberate choice not to show private bookmarks.
|
|
options.fetch(:show_private,
|
|
User.current_user.is_a?(User) &&
|
|
user_ids.include?(User.current_user.id))
|
|
end
|
|
|
|
def user_ids
|
|
user_ids = []
|
|
if options[:user_ids].present?
|
|
user_ids += options[:user_ids].map(&:to_i)
|
|
end
|
|
if options[:pseud_ids].present?
|
|
user_ids += Pseud.where(id: options[:pseud_ids]).pluck(:user_id)
|
|
end
|
|
user_ids
|
|
end
|
|
|
|
# The list of all tag IDs that should be required for our bookmarks.
|
|
def included_bookmark_tag_ids
|
|
@included_bookmark_tag_ids ||= [
|
|
options[:tag_ids],
|
|
parsed_included_tags[:ids]
|
|
].flatten.compact.uniq
|
|
end
|
|
|
|
# The list of all tag IDs that should be prohibited for our bookmarks.
|
|
def excluded_bookmark_tag_ids
|
|
@excluded_bookmark_tag_ids ||= [
|
|
options[:excluded_bookmark_tag_ids],
|
|
parsed_excluded_tags[:ids]
|
|
].flatten.compact.uniq
|
|
end
|
|
|
|
# The list of included tag names that weren't found in the database (and thus
|
|
# have to be used as text-matching constraints on the tag field).
|
|
def included_bookmark_tag_names
|
|
parsed_included_tags[:missing]
|
|
end
|
|
|
|
# The list of excluded tag names that weren't found in the database (and thus
|
|
# have to be used as text-matching constraints on the tag field).
|
|
def excluded_bookmark_tag_names
|
|
parsed_excluded_tags[:missing]
|
|
end
|
|
|
|
# Parse the tag names that should be included in our results.
|
|
def parsed_included_tags
|
|
@parsed_included_tags ||=
|
|
bookmarkable_query.parse_named_tags(%i[other_bookmark_tag_names])
|
|
end
|
|
|
|
# Parse the tag names that should be excluded from our results.
|
|
def parsed_excluded_tags
|
|
@parsed_excluded_tags ||=
|
|
bookmarkable_query.parse_named_tags(%i[excluded_bookmark_tag_names])
|
|
end
|
|
end
|