diff --git a/Gemfile b/Gemfile index 869ec4f81fc..a993a35f664 100644 --- a/Gemfile +++ b/Gemfile @@ -36,6 +36,7 @@ gem 'resque_mailer' gem 'resque-scheduler', :require => 'resque_scheduler' #gem 'daemon-spawn', :require => 'daemon_spawn' gem 'tire' +gem 'elasticsearch' gem 'aws-sdk' gem 'css_parser' diff --git a/Gemfile.lock b/Gemfile.lock index 925a23ce52d..a18673707dc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -97,6 +97,14 @@ GEM docile (1.1.1) domain_name (0.5.12) unf (>= 0.0.5, < 1.0.0) + elasticsearch (1.0.5) + elasticsearch-api (= 1.0.5) + elasticsearch-transport (= 1.0.5) + elasticsearch-api (1.0.5) + multi_json + elasticsearch-transport (1.0.5) + faraday + multi_json email_spec (1.5.0) launchy (~> 2.1) mail (~> 2.2) @@ -106,6 +114,8 @@ GEM activesupport (>= 3.0.0) faker (1.1.2) i18n (~> 0.5) + faraday (0.9.0) + multipart-post (>= 1.2, < 3) fastimage (1.4.0) gherkin (2.12.0) multi_json (~> 1.3) @@ -149,6 +159,7 @@ GEM mono_logger (1.1.0) multi_json (1.10.0) multi_xml (0.5.5) + multipart-post (2.0.0) mysql2 (0.3.11) mysql2 (0.3.11-x86-mingw32) net-http-digest_auth (1.3) @@ -350,6 +361,7 @@ DEPENDENCIES cucumber-rails database_cleaner delorean + elasticsearch email_spec escape_utils factory_girl diff --git a/app/models/external_work.rb b/app/models/external_work.rb index f3daba90cb0..861df7a94b1 100644 --- a/app/models/external_work.rb +++ b/app/models/external_work.rb @@ -7,55 +7,6 @@ class ExternalWork < ActiveRecord::Base attr_protected :summary_sanitizer_version has_many :related_works, :as => :parent - - has_many :taggings, :as => :taggable, :dependent => :destroy - has_many :tags, :through => :taggings, :source => :tagger, :source_type => 'Tag' - - has_many :filter_taggings, :as => :filterable, :dependent => :destroy - has_many :filters, :through => :filter_taggings - - has_many :ratings, - :through => :taggings, - :source => :tagger, - :source_type => 'Tag', - :before_remove => :remove_filter_tagging, - :conditions => "tags.type = 'Rating'" - has_many :categories, - :through => :taggings, - :source => :tagger, - :source_type => 'Tag', - :before_remove => :remove_filter_tagging, - :conditions => "tags.type = 'Category'" - has_many :warnings, - :through => :taggings, - :source => :tagger, - :source_type => 'Tag', - :before_remove => :remove_filter_tagging, - :conditions => "tags.type = 'Warning'" - has_many :fandoms, - :through => :taggings, - :source => :tagger, - :source_type => 'Tag', - :before_remove => :remove_filter_tagging, - :conditions => "tags.type = 'Fandom'" - has_many :relationships, - :through => :taggings, - :source => :tagger, - :source_type => 'Tag', - :before_remove => :remove_filter_tagging, - :conditions => "tags.type = 'Relationship'" - has_many :characters, - :through => :taggings, - :source => :tagger, - :source_type => 'Tag', - :before_remove => :remove_filter_tagging, - :conditions => "tags.type = 'Character'" - has_many :freeforms, - :through => :taggings, - :source => :tagger, - :source_type => 'Tag', - :before_remove => :remove_filter_tagging, - :conditions => "tags.type = 'Freeform'" belongs_to :language @@ -189,4 +140,40 @@ def tag_groups self.tags.group_by { |t| t.type.to_s } end + ###################### + # SEARCH + ###################### + + def bookmarkable_json + as_json( + root: false, + only: [ + :title, :summary, :hidden_by_admin, :created_at, :language_id + ], + methods: [ + :posted, :restricted, :tag, :filter_ids, :rating_ids, + :warning_ids, :category_ids, :fandom_ids, :character_ids, + :relationship_ids, :freeform_ids, :creators, :revised_at + ] + ).merge(bookmarkable_type: "ExternalWork") + end + + def posted + true + end + alias_method :posted?, :posted + + def restricted + false + end + alias_method :restricted?, :restricted + + def creators + [author] + end + + def revised_at + created_at + end + end diff --git a/app/models/search/bookmark_indexer.rb b/app/models/search/bookmark_indexer.rb new file mode 100644 index 00000000000..24d545f1315 --- /dev/null +++ b/app/models/search/bookmark_indexer.rb @@ -0,0 +1,74 @@ +class BookmarkIndexer < Indexer + + def self.klass + 'Bookmark' + end + + def self.index_all(options={}) + options[:skip_delete] = true + BookmarkableIndexer.delete_index + BookmarkableIndexer.create_index + create_mapping + BookmarkedExternalWorkIndexer.index_all(skip_delete: true) + BookmarkedSeriesIndexer.index_all(skip_delete: true) + BookmarkedWorkIndexer.index_all(skip_delete: true) + super + end + + def self.mapping + { + "bookmark" => { + "_parent" => { + type: 'bookmarkable' + }, + properties: { + bookmarkable_type: { + type: 'string', + index: 'not_analyzed' + }, + bookmarker: { + type: 'string', + analyzer: 'simple' + }, + notes: { + type: 'string', + analyzer: 'snowball' + }, + tag: { + type: 'string', + analyzer: 'simple' + } + } + } + } + end + + #################### + # INSTANCE METHODS + #################### + + # TODO: Make this work for deleted bookmarks + def routing_info(id) + { + '_index' => index_name, + '_type' => document_type, + '_id' => id, + 'parent' => objects[id.to_i].bookmarkable_id + } + end + + def document(object) + tags = object.tags + filters = tags.map{ |t| t.filter }.compact + + object.as_json( + root: false, + except: [:notes_sanitizer_version, :delta], + methods: [:bookmarker, :collection_ids, :with_notes] + ).merge( + tag: (tags + filters).map(&:name).uniq, + tag_ids: tags.map(&:id), + filter_ids: filters.map(&:id) + ) + end +end diff --git a/app/models/search/bookmark_query.rb b/app/models/search/bookmark_query.rb new file mode 100644 index 00000000000..bf5a6a4b0d4 --- /dev/null +++ b/app/models/search/bookmark_query.rb @@ -0,0 +1,2 @@ +class BookmarkQuery < Query +end diff --git a/app/models/search/bookmarkable_indexer.rb b/app/models/search/bookmarkable_indexer.rb new file mode 100644 index 00000000000..3e222e3106c --- /dev/null +++ b/app/models/search/bookmarkable_indexer.rb @@ -0,0 +1,50 @@ +class BookmarkableIndexer < Indexer + + def self.index_name + "ao3_#{Rails.env}_bookmarks" + end + + def self.document_type + 'bookmarkable' + end + + def self.mapping + { + 'bookmarkable' => { + properties: { + title: { + type: 'string', + analyzer: 'simple' + }, + creators: { + type: 'string', + analyzer: 'simple', + index_name: 'creator' + }, + tag: { + type: 'string', + analyzer: 'simple' + }, + work_types: { + type: 'string', + index: 'not_analyzed', + index_name: 'work_type' + } + } + } + } + end + + def routing_info(id) + { + '_index' => index_name, + '_type' => document_type, + '_id' => "#{id}-#{klass.underscore}" + } + end + + def document(object) + object.bookmarkable_json + end + +end diff --git a/app/models/search/bookmarked_external_work_indexer.rb b/app/models/search/bookmarked_external_work_indexer.rb new file mode 100644 index 00000000000..39ea7599b70 --- /dev/null +++ b/app/models/search/bookmarked_external_work_indexer.rb @@ -0,0 +1,5 @@ +class BookmarkedExternalWorkIndexer < BookmarkableIndexer + def self.klass + "ExternalWork" + end +end diff --git a/app/models/search/bookmarked_series_indexer.rb b/app/models/search/bookmarked_series_indexer.rb new file mode 100644 index 00000000000..0b86d4bddd8 --- /dev/null +++ b/app/models/search/bookmarked_series_indexer.rb @@ -0,0 +1,5 @@ +class BookmarkedSeriesIndexer < BookmarkableIndexer + def self.klass + "Series" + end +end diff --git a/app/models/search/bookmarked_work_indexer.rb b/app/models/search/bookmarked_work_indexer.rb new file mode 100644 index 00000000000..b2154781695 --- /dev/null +++ b/app/models/search/bookmarked_work_indexer.rb @@ -0,0 +1,10 @@ +class BookmarkedWorkIndexer < BookmarkableIndexer + def self.klass + "Work" + end + + # Only index works with bookmarks + def self.indexables + Work.joins(:stat_counter).where("bookmarks_count > 0") + end +end diff --git a/app/models/search/indexer.rb b/app/models/search/indexer.rb new file mode 100644 index 00000000000..50e1299a999 --- /dev/null +++ b/app/models/search/indexer.rb @@ -0,0 +1,141 @@ +class Indexer + + BATCH_SIZE = 1000 + + ################## + # CLASS METHODS + ################## + + def self.klass + raise "Must be defined in subclass" + end + + def self.delete_index + if $elasticsearch.indices.exists(index: index_name) + $elasticsearch.indices.delete(index: index_name) + end + end + + def self.create_index + $elasticsearch.indices.create( + index: index_name, + type: document_type, + body: { + settings: { + index: { + number_of_shards: 5 + } + }, + mappings: mapping + } + ) + end + + # Note that the index must exist before you can set the mapping + def self.create_mapping + $elasticsearch.indices.put_mapping( + index: index_name, + type: document_type, + body: mapping + ) + end + + def self.mapping + { + document_type => { + properties: { + #add properties in subclasses + } + } + } + end + + def self.index_all(options={}) + unless options[:skip_delete] + delete_index + create_index + end + index_from_db + end + + def self.index_from_db + total = (indexables.count / BATCH_SIZE) + 1 + i = 1 + indexables.find_in_batches(batch_size: BATCH_SIZE) do |group| + puts "Reindexing #{klass} batch #{i} of #{total}" + self.new(group.map(&:id)).index_documents + i += 1 + end + end + + # Add conditions here + def self.indexables + klass.constantize + end + + def self.index_name + "ao3_#{Rails.env}_#{klass.underscore.pluralize}" + end + + def self.document_type + klass.underscore + end + + #################### + # INSTANCE METHODS + #################### + + attr_reader :ids + + def initialize(ids) + @ids = ids + end + + def klass + self.class.klass + end + + def index_name + self.class.index_name + end + + def document_type + self.class.document_type + end + + def objects + @objects ||= klass.constantize.where(id: ids).inject({}) do |h, obj| + h.merge(obj.id => obj) + end + end + + def batch + @batch = [] + ids.each do |id| + object = objects[id.to_i] + if object.present? + @batch << { index: routing_info(id) } + @batch << document(object) + else + @batch << { delete: routing_info(id) } + end + end + @batch + end + + def index_documents + $elasticsearch.bulk(body: batch) + end + + def routing_info(id) + { + '_index' => index_name, + '_type' => document_type, + '_id' => id + } + end + + def document(object) + object.as_json(root: false) + end +end diff --git a/app/models/search/pseud_indexer.rb b/app/models/search/pseud_indexer.rb new file mode 100644 index 00000000000..e7ae573a857 --- /dev/null +++ b/app/models/search/pseud_indexer.rb @@ -0,0 +1,31 @@ +class PseudIndexer < Indexer + + def self.klass + 'Pseud' + end + + def self.mapping + { + 'pseud' => { + properties: { + name: { + type: 'string', + analyzer: 'simple' + }, + user_login: { + type: 'string', + analyzer: 'simple' + } + } + } + } + end + + def document(object) + object.as_json( + root: false, + only: [:id, :user_id, :name, :description, :created_at], + methods: [:user_login] + ) + end +end diff --git a/app/models/search/pseud_query.rb b/app/models/search/pseud_query.rb new file mode 100644 index 00000000000..3dd09412368 --- /dev/null +++ b/app/models/search/pseud_query.rb @@ -0,0 +1,2 @@ +class PseudQuery < Query +end diff --git a/app/models/search/query.rb b/app/models/search/query.rb new file mode 100644 index 00000000000..c3bb2f0e165 --- /dev/null +++ b/app/models/search/query.rb @@ -0,0 +1,2 @@ +class Query +end diff --git a/app/models/search/tag_indexer.rb b/app/models/search/tag_indexer.rb new file mode 100644 index 00000000000..d9f06e245b6 --- /dev/null +++ b/app/models/search/tag_indexer.rb @@ -0,0 +1,31 @@ +class TagIndexer < Indexer + + def self.klass + 'Tag' + end + + def self.mapping + { + tag: { + properties: { + name: { + type: 'string', + analyzer: 'simple' + }, + tag_type: { + type: 'string', + index: 'not_analyzed' + } + } + } + } + end + + def document(object) + object.as_json( + root: false, + only: [:id, :name, :merger_id, :canonical, :created_at] + ).merge(tag_type: object.type) + end + +end diff --git a/app/models/search/tag_query.rb b/app/models/search/tag_query.rb new file mode 100644 index 00000000000..30cd139dbf5 --- /dev/null +++ b/app/models/search/tag_query.rb @@ -0,0 +1,2 @@ +class TagQuery < Query +end diff --git a/app/models/search/work_indexer.rb b/app/models/search/work_indexer.rb new file mode 100644 index 00000000000..87288fcfc95 --- /dev/null +++ b/app/models/search/work_indexer.rb @@ -0,0 +1,76 @@ +class WorkIndexer < Indexer + + def self.klass + 'Work' + end + + def self.mapping + { + 'work' => { + properties: { + title: { + type: 'string', + analyzer: 'simple', + }, + creators: { + type: 'string', + analyzer: 'simple', + index_name: 'creator' + }, + tag: { + type: 'string', + analyzer: 'simple' + }, + authors_to_sort_on: { + type: 'string', + index: 'not_analyzed' + }, + title_to_sort_on: { + type: 'string', + index: 'not_analyzed' + }, + imported_from_url: { + type: 'string', + index: 'not_analyzed' + }, + work_types: { + type: 'string', + index_name: 'work_type', + index: 'not_analyzed', + } + } + } + } + end + + def document(object) + object.as_json( + root: false, + except: [ + :delta, :summary_sanitizer_version, :notes_sanitizer_version, + :endnotes_sanitizer_version, :hit_count_old, :last_visitor_old], + methods: [ + :rating_ids, + :warning_ids, + :category_ids, + :fandom_ids, + :character_ids, + :relationship_ids, + :freeform_ids, + :filter_ids, + :tag, + :pseud_ids, + :collection_ids, + :hits, + :comments_count, + :kudos_count, + :bookmarks_count, + :creators, + :crossover, + :work_types, + :nonfiction + ] + ) + end + +end diff --git a/app/models/search/work_query.rb b/app/models/search/work_query.rb new file mode 100644 index 00000000000..e12a7461a94 --- /dev/null +++ b/app/models/search/work_query.rb @@ -0,0 +1,2 @@ +class WorkQuery < Query +end diff --git a/app/models/series.rb b/app/models/series.rb index 3e8087c51e9..bc58274412c 100644 --- a/app/models/series.rb +++ b/app/models/series.rb @@ -67,9 +67,19 @@ def posted_works # Get the filters for the works in this series def filters - Tag.joins("JOIN filter_taggings ON tags.id = filter_taggings.filter_id JOIN works ON works.id = filter_taggings.filterable_id JOIN serial_works ON serial_works.work_id = works.id").where("serial_works.series_id = #{self.id} AND works.posted = 1 AND filter_taggings.filterable_type = 'Work'").group("tags.id") + Tag.joins("JOIN filter_taggings ON tags.id = filter_taggings.filter_id + JOIN works ON works.id = filter_taggings.filterable_id + JOIN serial_works ON serial_works.work_id = works.id"). + where("serial_works.series_id = #{self.id} AND + works.posted = 1 AND + filter_taggings.filterable_type = 'Work'"). + group("tags.id") end - + + def direct_filters + filters.where("filter_taggings.inherited = 0") + end + # visibility aped from the work model def visible(current_user=User.current_user) if current_user.is_a?(Admin) || (current_user.is_a?(User) && current_user.is_author_of?(self)) @@ -198,4 +208,81 @@ def revised_at Work.in_series(self).visible.collect(&:revised_at).compact.uniq.sort.last end end + + ###################### + # SEARCH + ###################### + + def bookmarkable_json + as_json( + root: false, + only: [:id, :title, :summary, :hidden_by_admin, :restricted, :created_at], + methods: [:revised_at, :posted, :tag, :filter_ids, :rating_ids, + :warning_ids, :category_ids, :fandom_ids, :character_ids, + :relationship_ids, :freeform_ids, :pseud_ids, :creators, :language_id, + :word_count, :work_types] + ).merge( + anonymous: anonymous?, + unrevealed: unrevealed?, + bookmarkable_type: 'Series' + ) + end + + # FIXME: should series have their own language? + def language_id + works.first.language_id if works.present? + end + + def posted + !posted_works.empty? + end + alias_method :posted?, :posted + + # Simple name to make it easier for people to use in full-text search + def tag + (work_tags + filters).uniq.map{ |t| t.name } + end + + # Index all the filters for pulling works + def filter_ids + filters.value_of :id + end + + # Index only direct filters (non meta-tags) for facets + def filters_for_facets + @filters_for_facets ||= direct_filters + end + def rating_ids + filters_for_facets.select{ |t| t.type.to_s == 'Rating' }.map{ |t| t.id } + end + def warning_ids + filters_for_facets.select{ |t| t.type.to_s == 'Warning' }.map{ |t| t.id } + end + def category_ids + filters_for_facets.select{ |t| t.type.to_s == 'Category' }.map{ |t| t.id } + end + def fandom_ids + filters_for_facets.select{ |t| t.type.to_s == 'Fandom' }.map{ |t| t.id } + end + def character_ids + filters_for_facets.select{ |t| t.type.to_s == 'Character' }.map{ |t| t.id } + end + def relationship_ids + filters_for_facets.select{ |t| t.type.to_s == 'Relationship' }.map{ |t| t.id } + end + def freeform_ids + filters_for_facets.select{ |t| t.type.to_s == 'Freeform' }.map{ |t| t.id } + end + + def pseud_ids + creatorships.value_of :pseud_id + end + + def creators + anonymous? ? ['Anonymous'] : pseuds.map(&:byline) + end + + def work_types + works.map(&:work_types).flatten.uniq + end end diff --git a/app/models/work.rb b/app/models/work.rb index 4b313797033..af24af5560a 100755 --- a/app/models/work.rb +++ b/app/models/work.rb @@ -48,57 +48,6 @@ class Work < ActiveRecord::Base has_many :challenge_claims, :as => :creation accepts_nested_attributes_for :challenge_claims - has_many :filter_taggings, :as => :filterable - has_many :filters, :through => :filter_taggings - has_many :direct_filter_taggings, :class_name => "FilterTagging", :as => :filterable, :conditions => "inherited = 0" - has_many :direct_filters, :source => :filter, :through => :direct_filter_taggings - - has_many :taggings, :as => :taggable, :dependent => :destroy - has_many :tags, :through => :taggings, :source => :tagger, :source_type => 'Tag' - - has_many :ratings, - :through => :taggings, - :source => :tagger, - :source_type => 'Tag', - :before_remove => :remove_filter_tagging, - :conditions => "tags.type = 'Rating'" - has_many :categories, - :through => :taggings, - :source => :tagger, - :source_type => 'Tag', - :before_remove => :remove_filter_tagging, - :conditions => "tags.type = 'Category'" - has_many :warnings, - :through => :taggings, - :source => :tagger, - :source_type => 'Tag', - :before_remove => :remove_filter_tagging, - :conditions => "tags.type = 'Warning'" - has_many :fandoms, - :through => :taggings, - :source => :tagger, - :source_type => 'Tag', - :before_remove => :remove_filter_tagging, - :conditions => "tags.type = 'Fandom'" - has_many :relationships, - :through => :taggings, - :source => :tagger, - :source_type => 'Tag', - :before_remove => :remove_filter_tagging, - :conditions => "tags.type = 'Relationship'" - has_many :characters, - :through => :taggings, - :source => :tagger, - :source_type => 'Tag', - :before_remove => :remove_filter_tagging, - :conditions => "tags.type = 'Character'" - has_many :freeforms, - :through => :taggings, - :source => :tagger, - :source_type => 'Tag', - :before_remove => :remove_filter_tagging, - :conditions => "tags.type = 'Freeform'" - acts_as_commentable has_many :total_comments, :class_name => 'Comment', :through => :chapters has_many :kudos, :as => :commentable, :dependent => :destroy @@ -1308,45 +1257,25 @@ def to_indexed_json ]) end - # Simple name to make it easier for people to use in full-text search - def tag - (tags + filters).uniq.map{ |t| t.name } - end - - # Index all the filters for pulling works - def filter_ids - filters.value_of :id - end - - # Index only direct filters (non meta-tags) for facets - def filters_for_facets - @filters_for_facets ||= filters.where("filter_taggings.inherited = 0") - end - def rating_ids - filters_for_facets.select{ |t| t.type.to_s == 'Rating' }.map{ |t| t.id } - end - def warning_ids - filters_for_facets.select{ |t| t.type.to_s == 'Warning' }.map{ |t| t.id } - end - def category_ids - filters_for_facets.select{ |t| t.type.to_s == 'Category' }.map{ |t| t.id } - end - def fandom_ids - filters_for_facets.select{ |t| t.type.to_s == 'Fandom' }.map{ |t| t.id } - end - def character_ids - filters_for_facets.select{ |t| t.type.to_s == 'Character' }.map{ |t| t.id } - end - def relationship_ids - filters_for_facets.select{ |t| t.type.to_s == 'Relationship' }.map{ |t| t.id } - end - def freeform_ids - filters_for_facets.select{ |t| t.type.to_s == 'Freeform' }.map{ |t| t.id } + def bookmarkable_json + as_json( + root: false, + only: [:id, :title, :summary, :hidden_by_admin, :restricted, :posted, + :created_at, :revised_at, :language_id, :word_count], + methods: [:tag, :filter_ids, :rating_ids, :warning_ids, :category_ids, + :fandom_ids, :character_ids, :relationship_ids, :freeform_ids, + :pseud_ids, :creators, :collection_ids, :work_types] + ).merge( + anonymous: anonymous?, + unrevealed: unrevealed?, + bookmarkable_type: 'Work' + ) end def pseud_ids creatorships.value_of :pseud_id end + def collection_ids approved_collections.value_of(:id, :parent_id).flatten.uniq.compact end @@ -1361,6 +1290,7 @@ def bookmarks_count self.stat_counter.bookmarks_count end + # Deprecated: old search def creator names = "" if anonymous? @@ -1376,4 +1306,44 @@ def creator names end + # New version + def creators + if anonymous? + ["Anonymous"] + else + pseuds.map(&:byline) + external_author_names.value_of(:name) + end + end + + # A work with multiple fandoms which are not related + # to one another can be considered a crossover + def crossover + filters.by_type('Fandom').first_class.count > 1 + end + + # Quick and dirty categorization of the most obvious stuff + # To be replaced by actual categories + def work_types + types = [] + video_ids = [44011] # Video + audio_ids = [70308] # Podfic + art_ids = [7844, 125758] # Fanart, Arts + types << "Video" if (filter_ids & video_ids).present? + types << "Audio" if (filter_ids & audio_ids).present? + types << "Art" if (filter_ids & art_ids).present? + # Very arbitrary cut off here, but wanted to make sure we + # got fic + art/podfic/video tagged as text as well + if types.empty? || word_count > 200 + types << "Text" + end + types + end + + # To be replaced by actual category + # Can't use the 'Meta' tag since that has too many different uses + def nonfiction + nonfiction_tags = [125773, 66586, 123921] # Essays, Nonfiction, Reviews + (filter_ids & nonfiction_tags).present? + end + end diff --git a/config/application.rb b/config/application.rb index 0098a1dc0f8..59d7d03a1fb 100644 --- a/config/application.rb +++ b/config/application.rb @@ -16,9 +16,9 @@ class Application < Rails::Application # config.autoload_paths += %W(#{config.root}/extras) config.autoload_paths += %W(#{Rails.root}/lib) config.autoload_paths += %W(#{Rails.root}/app/sweepers) - config.autoload_paths += %W(#{Rails.root}/app/models/challenge_models) - config.autoload_paths += %W(#{Rails.root}/app/models/tagset_models) - config.autoload_paths += %W(#{Rails.root}/app/models/indexing) + %w(challenge_models tagset_models indexing search).each do |dir| + config.autoload_paths << "#{Rails.root}/app/models/#{dir}" + end # Only load the plugins named here, in the order given (default is alphabetical). # :all can be used as a placeholder for all plugins not explicitly named. diff --git a/config/config.yml b/config/config.yml index 20462dc417d..c9908f49ba1 100644 --- a/config/config.yml +++ b/config/config.yml @@ -50,6 +50,7 @@ OTWLOGO: 'OTWLogo.png' OTWALT_LOGO: 'OTW Logo:closing the circle of the copyright symbol, it symbolizes our creative engagement with media: participating and not just consuming.' REVISION: '' ELASTICSEARCH_URL: 'http://localhost:9200' +ELASTICSEARCH_1_URL: 'http://localhost:9200' MEMCACHED_URL: '127.0.0.1' # tag settings diff --git a/config/initializers/gem-plugin_config/elasticsearch.rb b/config/initializers/gem-plugin_config/elasticsearch.rb new file mode 100644 index 00000000000..d973e4956e7 --- /dev/null +++ b/config/initializers/gem-plugin_config/elasticsearch.rb @@ -0,0 +1 @@ +$elasticsearch = Elasticsearch::Client.new host: ArchiveConfig.ELASTICSEARCH_1_URL \ No newline at end of file diff --git a/lib/taggable.rb b/lib/taggable.rb index e0a6175bbbc..bcd8e974747 100644 --- a/lib/taggable.rb +++ b/lib/taggable.rb @@ -5,6 +5,57 @@ def self.included(taggable) attr_accessor :invalid_tags attr_accessor :preview_mode, :placeholder_tags after_update :reset_placeholders + + has_many :filter_taggings, :as => :filterable + has_many :filters, :through => :filter_taggings + has_many :direct_filter_taggings, :class_name => "FilterTagging", :as => :filterable, :conditions => "inherited = 0" + has_many :direct_filters, :source => :filter, :through => :direct_filter_taggings + + has_many :taggings, :as => :taggable, :dependent => :destroy + has_many :tags, :through => :taggings, :source => :tagger, :source_type => 'Tag' + + has_many :ratings, + :through => :taggings, + :source => :tagger, + :source_type => 'Tag', + :before_remove => :remove_filter_tagging, + :conditions => "tags.type = 'Rating'" + has_many :categories, + :through => :taggings, + :source => :tagger, + :source_type => 'Tag', + :before_remove => :remove_filter_tagging, + :conditions => "tags.type = 'Category'" + has_many :warnings, + :through => :taggings, + :source => :tagger, + :source_type => 'Tag', + :before_remove => :remove_filter_tagging, + :conditions => "tags.type = 'Warning'" + has_many :fandoms, + :through => :taggings, + :source => :tagger, + :source_type => 'Tag', + :before_remove => :remove_filter_tagging, + :conditions => "tags.type = 'Fandom'" + has_many :relationships, + :through => :taggings, + :source => :tagger, + :source_type => 'Tag', + :before_remove => :remove_filter_tagging, + :conditions => "tags.type = 'Relationship'" + has_many :characters, + :through => :taggings, + :source => :tagger, + :source_type => 'Tag', + :before_remove => :remove_filter_tagging, + :conditions => "tags.type = 'Character'" + has_many :freeforms, + :through => :taggings, + :source => :tagger, + :source_type => 'Tag', + :before_remove => :remove_filter_tagging, + :conditions => "tags.type = 'Freeform'" end end @@ -205,4 +256,46 @@ def parse_tags(klass, incoming_tags) end end -end + ################ + # SEARCH + ################ + + public + + # Simple name to make it easier for people to use in full-text search + def tag + (tags + filters).uniq.map{ |t| t.name } + end + + # Index all the filters for pulling works + def filter_ids + filters.value_of :id + end + + # Index only direct filters (non meta-tags) for facets + def filters_for_facets + @filters_for_facets ||= filters.where("filter_taggings.inherited = 0") + end + def rating_ids + filters_for_facets.select{ |t| t.type.to_s == 'Rating' }.map{ |t| t.id } + end + def warning_ids + filters_for_facets.select{ |t| t.type.to_s == 'Warning' }.map{ |t| t.id } + end + def category_ids + filters_for_facets.select{ |t| t.type.to_s == 'Category' }.map{ |t| t.id } + end + def fandom_ids + filters_for_facets.select{ |t| t.type.to_s == 'Fandom' }.map{ |t| t.id } + end + def character_ids + filters_for_facets.select{ |t| t.type.to_s == 'Character' }.map{ |t| t.id } + end + def relationship_ids + filters_for_facets.select{ |t| t.type.to_s == 'Relationship' }.map{ |t| t.id } + end + def freeform_ids + filters_for_facets.select{ |t| t.type.to_s == 'Freeform' }.map{ |t| t.id } + end + +end \ No newline at end of file diff --git a/lib/tasks/search.rake b/lib/tasks/search.rake new file mode 100644 index 00000000000..c1c51f088ea --- /dev/null +++ b/lib/tasks/search.rake @@ -0,0 +1,18 @@ +namespace :search do + desc "Reindex tags" + task(:index_tags => :environment) do + TagIndexer.index_all + end + desc "Reindex pseuds" + task(:index_pseuds => :environment) do + PseudIndexer.index_all + end + desc "Reindex works" + task(:index_works => :environment) do + WorkIndexer.index_all + end + desc "Reindex bookmarks" + task(:index_bookmarks => :environment) do + BookmarkIndexer.index_all + end +end \ No newline at end of file