diff --git a/app/models/indexed_post.rb b/app/models/indexed_post.rb new file mode 100644 index 00000000..6456a824 --- /dev/null +++ b/app/models/indexed_post.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +# La representación indexable de un artículo +class IndexedPost < ApplicationRecord + include PgSearch::Model + + # La traducción del locale según Sutty al locale según PostgreSQL + DICTIONARIES = { + es: 'spanish', + en: 'english' + }.freeze + + # TODO: Los indexed posts tienen que estar scopeados al idioma actual, + # no buscar sobre todos + pg_search_scope :search, + lambda { |locale, query| + { + against: :content, + query: query, + using: { + tsearch: { + dictionary: IndexedPost.to_dictionary(locale: locale), + tsvector_column: 'indexed_content' + } + } + } + } + + belongs_to :site + + # Convertir locale a direccionario de PG + # + # @param [String,Symbol] + # @return [String] + def self.to_dictionary(locale:) + DICTIONARIES[locale.to_sym] || 'simple' + end +end diff --git a/app/models/post.rb b/app/models/post.rb index a0e16706..6ef7a2ec 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -17,6 +17,8 @@ class Post attr_reader :attributes, :errors, :layout, :site, :document + include Post::Indexable + class << self # Obtiene el layout sin leer el Document # @@ -191,6 +193,8 @@ class Post post: self, required: true) end + alias locale lang + # TODO: Mover a method_missing def uuid @metadata[:uuid] ||= MetadataUuid.new(document: document, site: site, layout: layout, name: :uuid, type: :uuid, diff --git a/app/models/post/indexable.rb b/app/models/post/indexable.rb new file mode 100644 index 00000000..f25f28d2 --- /dev/null +++ b/app/models/post/indexable.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +class Post + # Vuelve indexables a los Posts + module Indexable + extend ActiveSupport::Concern + + included do + # Devuelve una versión indexable del Post + # + # @return [IndexedPosts] + def to_index + @to_index ||= IndexedPost.find_or_create_by(id: uuid.value).tap do |indexed_post| + indexed_post.layout = layout.name + indexed_post.site_id = site.id + indexed_post.path = path.basename + indexed_post.locale = IndexedPost.to_dictionary(locale: locale.value) + indexed_post.title = title.value + indexed_post.front_matter = indexable_front_matter + indexed_post.content = indexable_content + end + end + + private + + # Los metadatos que se almacenan como objetos JSON. Empezamos con + # las categorías porque se usan para filtrar en el listado de + # artículos. + # + # @return [Hash] + def indexable_front_matter + return {} unless attribute? :categories + + { categories: categories.indexable_values } + end + + # Devuelve un documento indexable en texto plano + # + # XXX: No memoizamos para permitir actualizaciones, aunque + # probablemente se indexe una sola vez. + # + # @return [String] + def indexable_content + indexable_attributes.map do |attr| + self[attr].to_s + end.join("\n") + end + + def indexable_attributes + @indexable_attributes ||= attributes.select do |attr| + self[attr].indexable? + end + end + end + end +end diff --git a/app/models/site.rb b/app/models/site.rb index 1c62e9bc..14238e1a 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -7,6 +7,7 @@ class Site < ApplicationRecord include Site::Forms include Site::FindAndReplace include Site::Api + include Site::Index include Tienda # Cifrar la llave privada que cifra y decifra campos ocultos. Sutty @@ -37,6 +38,7 @@ class Site < ApplicationRecord belongs_to :design belongs_to :licencia + has_many :indexed_posts, dependent: :destroy has_many :log_entries, dependent: :destroy has_many :deploys, dependent: :destroy has_many :build_stats, through: :deploys