Merge pull request #3847 from r888888888/intelligent-autocomplete

Intelligent autocomplete
This commit is contained in:
Albert Yi
2018-08-29 11:15:39 -07:00
committed by GitHub
6 changed files with 245 additions and 37 deletions

View File

@@ -10,6 +10,7 @@ class TagsController < ApplicationController
def index
@tags = Tag.search(search_params).paginate(params[:page], :limit => params[:limit], :search_count => params[:search])
respond_with(@tags) do |format|
format.xml do
render :xml => @tags.to_xml(:root => "tags")
@@ -18,7 +19,13 @@ class TagsController < ApplicationController
end
def autocomplete
@tags = Tag.names_matches_with_aliases(params[:search][:name_matches])
if CurrentUser.is_builder?
# limit rollout
@tags = TagAutocomplete.search(params[:search][:name_matches])
else
@tags = Tag.names_matches_with_aliases(params[:search][:name_matches])
end
expires_in params[:expiry].to_i.days if params[:expiry]
respond_with(@tags) do |format|

View File

@@ -307,6 +307,11 @@ Autocomplete.insert_completion = function(input, completion) {
var regexp = new RegExp("(" + Autocomplete.TAG_PREFIXES + ")?\\S+$", "g");
before_caret_text = before_caret_text.replace(regexp, "$1") + completion + " ";
if (Utility.meta('current-user-id') === '1') {
// is this actually better?
after_caret_text = after_caret_text.replace(/^\S+/, "");
}
input.value = before_caret_text + after_caret_text;
input.selectionStart = input.selectionEnd = before_caret_text.length;
};

View File

@@ -0,0 +1,99 @@
module TagAutocomplete
extend self
PREFIX_BOUNDARIES = "(_/:;-"
class Result < Struct.new(:name, :post_count, :category, :antecedent_name)
def to_xml(options = {})
to_h.to_xml(options)
end
end
def search(query)
candidates = count_sort(
query,
search_prefix(query, 3) +
search_fuzzy(query, 5) +
search_exact(query, 3) +
search_aliases(query, 3)
)
end
def count_sort(query, words)
words.uniq.sort_by do |x|
x.post_count
end.reverse
end
def search_exact(query, n=3)
Tag
.where("name like ? escape e'\\\\'", query.to_escaped_for_sql_like + "%")
.where("post_count > 0")
.order("post_count desc")
.limit(n)
.pluck(:name, :post_count, :category)
.map {|row| Result.new(*row)}
end
def search_fuzzy(query, n=5)
if query.size <= 3
return []
end
Tag
.where("name % ?", query)
.where("name like ? escape E'\\\\'", query[0].to_escaped_for_sql_like + '%')
.where("post_count > 0")
.order(Arel.sql("similarity(name, #{Tag.connection.quote(query)}) * log(10, post_count + 1) DESC"))
.limit(n)
.pluck(:name, :post_count, :category)
.map {|row| Result.new(*row)}
end
def search_prefix(query, n=3)
if query.size >= 5
return []
end
if query.size <= 1
return []
end
if query =~ /[-_()]/
return []
end
if query.size >= 3
min_post_count = 0
else
min_post_count = 5_000
n += 2
end
anchors = "^" + query.split("").map {|x| Regexp.escape(x)}.join(".*[#{PREFIX_BOUNDARIES}]")
Tag
.where("name ~ ?", anchors)
.where("post_count > ?", min_post_count)
.where("post_count > 0")
.order("post_count desc")
.limit(n)
.pluck(:name, :post_count, :category)
.map {|row| Result.new(*row)}
end
def search_aliases(query, n=20)
wildcard_name = query + "*"
TagAlias
.select("tags.name, tags.post_count, tags.category, tag_aliases.antecedent_name")
.joins("INNER JOIN tags ON tags.name = tag_aliases.consequent_name")
.where("tag_aliases.antecedent_name LIKE ? ESCAPE E'\\\\'", wildcard_name.to_escaped_for_sql_like)
.active
.where("tags.name NOT LIKE ? ESCAPE E'\\\\'", wildcard_name.to_escaped_for_sql_like)
.where("tag_aliases.post_count > 0")
.order("tag_aliases.post_count desc")
.limit(n)
.pluck(:name, :post_count, :category, :antecedent_name)
.map {|row| Result.new(*row)}
end
end

View File

@@ -1,8 +1,9 @@
class TagImplication < TagRelationship
extend Memoist
before_save :update_descendant_names
after_save :update_descendant_names_for_parents
after_destroy :update_descendant_names_for_parents
after_save :update_descendant_names_for_parents, if: ->(rec) { rec.is_retired? }
after_save :create_mod_action
validates_uniqueness_of :antecedent_name, :scope => :consequent_name
validate :absence_of_circular_relation
@@ -17,6 +18,7 @@ class TagImplication < TagRelationship
module DescendantMethods
extend ActiveSupport::Concern
extend Memoist
module ClassMethods
# assumes names are normalized
@@ -32,17 +34,16 @@ class TagImplication < TagRelationship
end
def descendants
@descendants ||= begin
[].tap do |all|
children = [consequent_name]
[].tap do |all|
children = [consequent_name]
until children.empty?
all.concat(children)
children = TagImplication.active.where(antecedent_name: children).pluck(:consequent_name)
end
end.sort.uniq
end
until children.empty?
all.concat(children)
children = TagImplication.active.where(antecedent_name: children).pluck(:consequent_name)
end
end.sort.uniq
end
memoize :descendants
def descendant_names_array
descendant_names.split(/ /)
@@ -53,7 +54,7 @@ class TagImplication < TagRelationship
end
def update_descendant_names!
clear_descendants_cache
flush_cache
update_descendant_names
update_attribute(:descendant_names, descendant_names)
end
@@ -64,20 +65,15 @@ class TagImplication < TagRelationship
parent.update_descendant_names_for_parents
end
end
def clear_descendants_cache
@descendants = nil
end
end
module ParentMethods
def parents
@parents ||= self.class.where(["consequent_name = ?", antecedent_name])
end
extend Memoist
def clear_parents_cache
@parents = nil
def parents
self.class.where("consequent_name = ?", antecedent_name)
end
memoize :parents
end
module ValidationMethods
@@ -139,6 +135,8 @@ class TagImplication < TagRelationship
end
module ApprovalMethods
extend Memoist
def process!(update_topic: true)
unless valid?
raise errors.full_messages.join("; ")
@@ -215,19 +213,18 @@ class TagImplication < TagRelationship
end
def forum_updater
@forum_updater ||= begin
post = if forum_topic
forum_post || forum_topic.posts.where("body like ?", TagImplicationRequest.command_string(antecedent_name, consequent_name) + "%").last
else
nil
end
ForumUpdater.new(
forum_topic,
forum_post: post,
expected_title: TagImplicationRequest.topic_title(antecedent_name, consequent_name)
)
post = if forum_topic
forum_post || forum_topic.posts.where("body like ?", TagImplicationRequest.command_string(antecedent_name, consequent_name) + "%").last
else
nil
end
ForumUpdater.new(
forum_topic,
forum_post: post,
expected_title: TagImplicationRequest.topic_title(antecedent_name, consequent_name)
)
end
memoize :forum_updater
end
include DescendantMethods
@@ -236,8 +233,7 @@ class TagImplication < TagRelationship
include ApprovalMethods
def reload(options = {})
flush_cache
super
clear_parents_cache
clear_descendants_cache
end
end