otwarchive-symphonyarchive/app/models/search/bookmark_query.rb
2026-03-11 22:22:11 +00:00

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