Class: HashtagAutocompleteService
- Inherits:
-
Object
- Object
- HashtagAutocompleteService
- Defined in:
- app/services/hashtag_autocomplete_service.rb
Defined Under Namespace
Classes: HashtagItem
Constant Summary collapse
- HASHTAGS_PER_REQUEST =
20
- SEARCH_MAX_LIMIT =
50
- DEFAULT_DATA_SOURCES =
[CategoryHashtagDataSource, TagHashtagDataSource]
- DEFAULT_CONTEXTUAL_TYPE_PRIORITIES =
[ { type: "category", context: "topic-composer", priority: 100 }, { type: "tag", context: "topic-composer", priority: 50 }, ]
Instance Attribute Summary collapse
-
#guardian ⇒ Object
readonly
Returns the value of attribute guardian.
Class Method Summary collapse
- .contexts_with_ordered_types ⇒ Object
- .contextual_type_priorities ⇒ Object
- .data_source_from_type(type) ⇒ Object
- .data_source_icon_map ⇒ Object
- .data_source_types ⇒ Object
-
.data_sources ⇒ Object
NOTE: This is not meant to be called directly; use ‘enabled_data_sources` or the individual data_source_X methods instead.
- .enabled_data_sources ⇒ Object
- .find_priorities_for_context(context) ⇒ Object
- .ordered_types_for_context(context) ⇒ Object
- .search_conditions ⇒ Object
- .unique_contexts ⇒ Object
Instance Method Summary collapse
- #find_by_ids(ids_by_type) ⇒ Object
-
#initialize(guardian) ⇒ HashtagAutocompleteService
constructor
A new instance of HashtagAutocompleteService.
-
#lookup(slugs, types_in_priority_order) ⇒ Object
Finds resources of the provided types by their exact slugs, unlike search which can search partial names, slugs, etc.
-
#search(term, types_in_priority_order, limit: SiteSetting.experimental_hashtag_search_result_limit) ⇒ Object
Searches registered hashtag data sources using the provided term (data sources determine what is actually searched) and prioritises the results based on types_in_priority_order and the limit.
Constructor Details
#initialize(guardian) ⇒ HashtagAutocompleteService
Returns a new instance of HashtagAutocompleteService.
134 135 136 |
# File 'app/services/hashtag_autocomplete_service.rb', line 134 def initialize(guardian) @guardian = guardian end |
Instance Attribute Details
#guardian ⇒ Object (readonly)
Returns the value of attribute guardian.
16 17 18 |
# File 'app/services/hashtag_autocomplete_service.rb', line 16 def guardian @guardian end |
Class Method Details
.contexts_with_ordered_types ⇒ Object
66 67 68 |
# File 'app/services/hashtag_autocomplete_service.rb', line 66 def self.contexts_with_ordered_types Hash[unique_contexts.map { |context| [context, ordered_types_for_context(context)] }] end |
.contextual_type_priorities ⇒ Object
26 27 28 29 30 31 32 33 |
# File 'app/services/hashtag_autocomplete_service.rb', line 26 def self.contextual_type_priorities # Category and Tag type priorities for the composer are default and # always are included. Set.new( DEFAULT_CONTEXTUAL_TYPE_PRIORITIES | DiscoursePluginRegistry.hashtag_autocomplete_contextual_type_priorities, ) end |
.data_source_from_type(type) ⇒ Object
47 48 49 |
# File 'app/services/hashtag_autocomplete_service.rb', line 47 def self.data_source_from_type(type) self.enabled_data_sources.find { |ds| ds.type == type } end |
.data_source_icon_map ⇒ Object
43 44 45 |
# File 'app/services/hashtag_autocomplete_service.rb', line 43 def self.data_source_icon_map self.enabled_data_sources.map { |ds| [ds.type, ds.icon] }.to_h end |
.data_source_types ⇒ Object
39 40 41 |
# File 'app/services/hashtag_autocomplete_service.rb', line 39 def self.data_source_types self.enabled_data_sources.map(&:type) end |
.data_sources ⇒ Object
NOTE: This is not meant to be called directly; use ‘enabled_data_sources` or the individual data_source_X methods instead.
20 21 22 23 24 |
# File 'app/services/hashtag_autocomplete_service.rb', line 20 def self.data_sources # Category and Tag data sources are in core and always should be # included for searches and lookups. Set.new(DEFAULT_DATA_SOURCES | DiscoursePluginRegistry.hashtag_autocomplete_data_sources) end |
.enabled_data_sources ⇒ Object
35 36 37 |
# File 'app/services/hashtag_autocomplete_service.rb', line 35 def self.enabled_data_sources self.data_sources.filter(&:enabled?) end |
.find_priorities_for_context(context) ⇒ Object
51 52 53 |
# File 'app/services/hashtag_autocomplete_service.rb', line 51 def self.find_priorities_for_context(context) contextual_type_priorities.select { |ctp| ctp[:context] == context } end |
.ordered_types_for_context(context) ⇒ Object
59 60 61 62 63 64 |
# File 'app/services/hashtag_autocomplete_service.rb', line 59 def self.ordered_types_for_context(context) find_priorities_for_context(context) .sort_by { |ctp| -ctp[:priority] } .map { |ctp| ctp[:type] } .reject { |type| data_source_types.exclude?(type) } end |
.search_conditions ⇒ Object
12 13 14 |
# File 'app/services/hashtag_autocomplete_service.rb', line 12 def self.search_conditions @search_conditions ||= Enum.new(contains: 0, starts_with: 1) end |
.unique_contexts ⇒ Object
55 56 57 |
# File 'app/services/hashtag_autocomplete_service.rb', line 55 def self.unique_contexts contextual_type_priorities.map { |ctp| ctp[:context] }.uniq end |
Instance Method Details
#find_by_ids(ids_by_type) ⇒ Object
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
# File 'app/services/hashtag_autocomplete_service.rb', line 138 def find_by_ids(ids_by_type) HashtagAutocompleteService .data_source_types .each_with_object({}) do |type, hash| next if ids_by_type[type].blank? data_source = HashtagAutocompleteService.data_source_from_type(type) next if !data_source.respond_to?(:find_by_ids) = data_source.find_by_ids(guardian, ids_by_type[type]) next if .blank? hash[type] = set_types(, type).map(&:to_h) end end |
#lookup(slugs, types_in_priority_order) ⇒ Object
Finds resources of the provided types by their exact slugs, unlike search which can search partial names, slugs, etc. Used for cooking fully formed #hashtags in the markdown pipeline. The @guardian handles permissions around which results should be returned here.
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 |
# File 'app/services/hashtag_autocomplete_service.rb', line 169 def lookup(slugs, types_in_priority_order) raise Discourse::InvalidParameters.new(:slugs) if !slugs.is_a?(Array) raise Discourse::InvalidParameters.new(:order) if !types_in_priority_order.is_a?(Array) types_in_priority_order = types_in_priority_order.select do |type| HashtagAutocompleteService.data_source_types.include?(type) end lookup_results = Hash[types_in_priority_order.collect { |type| [type.to_sym, []] }] limited_slugs = slugs[0..HashtagAutocompleteService::HASHTAGS_PER_REQUEST] slugs_without_suffixes = limited_slugs.reject do |slug| HashtagAutocompleteService.data_source_types.any? { |type| slug.ends_with?("::#{type}") } end slugs_with_suffixes = (limited_slugs - slugs_without_suffixes) # For all the slugs without a type suffix, we need to lookup in order, falling # back to the next type if no results are returned for a slug for the current # type. This way slugs without suffix make sense in context, e.g. in the topic # composer we want a slug without a suffix to be a category first, tag second. if slugs_without_suffixes.any? types_in_priority_order.each do |type| # We do not want to continue fallback if there are conflicting slugs where # one has a type and one does not, this may result in duplication. An # example: # # A category with slug `management` is not found because of permissions # and we also have a slug with suffix in the form of `management::tag`. # There is a tag that exists with the `management` slug. The tag should # not be found here but rather in the next lookup since it's got a more # specific lookup with the type. slugs_to_lookup = slugs_without_suffixes.reject { |slug| slugs_with_suffixes.include?("#{slug}::#{type}") } found_from_slugs = execute_lookup!(lookup_results, type, guardian, slugs_to_lookup) slugs_without_suffixes = slugs_without_suffixes - found_from_slugs.map(&:ref) break if slugs_without_suffixes.empty? end end # We then look up the remaining slugs based on their type suffix, stripping out # the type suffix first since it will not match the actual slug. if slugs_with_suffixes.any? types_in_priority_order.each do |type| slugs_for_type = slugs_with_suffixes .select { |slug| slug.ends_with?("::#{type}") } .map { |slug| slug.gsub("::#{type}", "") } next if slugs_for_type.empty? execute_lookup!(lookup_results, type, guardian, slugs_for_type) # Make sure the refs are the same going out as they were going in. lookup_results[type.to_sym].each do |item| item.ref = "#{item.ref}::#{type}" if slugs_with_suffixes.include?("#{item.ref}::#{type}") end end end lookup_results end |
#search(term, types_in_priority_order, limit: SiteSetting.experimental_hashtag_search_result_limit) ⇒ Object
Searches registered hashtag data sources using the provided term (data sources determine what is actually searched) and prioritises the results based on types_in_priority_order and the limit. For example, if 5 categories were returned for the term and the limit was 5, we would not even bother searching tags. The @guardian handles permissions around which results should be returned here.
Items which have a slug that exactly matches the search term via lookup will be found first and floated to the top of the results, and still be ordered by type.
250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 |
# File 'app/services/hashtag_autocomplete_service.rb', line 250 def search( term, types_in_priority_order, limit: SiteSetting.experimental_hashtag_search_result_limit ) raise Discourse::InvalidParameters.new(:order) if !types_in_priority_order.is_a?(Array) limit = [limit, SEARCH_MAX_LIMIT].min types_in_priority_order = types_in_priority_order.select do |type| HashtagAutocompleteService.data_source_types.include?(type) end return search_without_term(types_in_priority_order, limit) if term.blank? limited_results = [] top_ranked_type = nil term = term.downcase # Float exact matches by slug to the top of the list, any of these will be excluded # from further results. types_in_priority_order.each do |type| search_results = execute_lookup!(nil, type, guardian, [term]) limited_results.concat(search_results) if search_results break if limited_results.length >= limit end # Next priority are slugs which start with the search term. if limited_results.length < limit types_in_priority_order.each do |type| limited_results = search_using_condition( limited_results, term, type, limit, HashtagAutocompleteService.search_conditions[:starts_with], ) top_ranked_type = type if top_ranked_type.nil? break if limited_results.length >= limit end end # Search the data source for each type, validate and sort results, # and break off from searching more data sources if we reach our limit if limited_results.length < limit types_in_priority_order.each do |type| limited_results = search_using_condition( limited_results, term, type, limit, HashtagAutocompleteService.search_conditions[:contains], ) top_ranked_type = type if top_ranked_type.nil? break if limited_results.length >= limit end end # Any items that are _not_ the top-ranked type (which could possibly not be # the same as the first item in the types_in_priority_order if there was # no data for that type) that have conflicting slugs with other items for # other higher-ranked types need to have a ::type suffix added to their ref. # # This will be used for the lookup method above if one of these items is # chosen in the UI, otherwise there is no way to determine whether a hashtag is # for a category or a tag etc. # # For example, if there is a category with the slug #general and a tag # with the slug #general, then the tag will have its ref changed to #general::tag append_types_to_conflicts(limited_results, top_ranked_type, types_in_priority_order, limit) end |