diff --git a/app/controllers/searches_controller.rb b/app/controllers/searches_controller.rb index 64e80c83f..6c2c33dee 100644 --- a/app/controllers/searches_controller.rb +++ b/app/controllers/searches_controller.rb @@ -1,8 +1,12 @@ class SearchesController < ApplicationController - MAX_RESULTS = 50 + def create + Current.user.remember_search(query_param) + head :ok + end def show - @search = Search.new(Current.user, query_param) + @search_results = Current.user.search(query_param, max_results: 50) + @recent_search_queries = Current.user.search_queries.order(created_at: :desc).limit(10) end private diff --git a/app/models/search.rb b/app/models/search.rb index 1a2e4ddaa..7c0175e7d 100644 --- a/app/models/search.rb +++ b/app/models/search.rb @@ -7,7 +7,7 @@ class Search def initialize(user, query, max_results: 50) @user = user - @query = Query.new(query) + @query = Query.wrap(query) @max_results = max_results end diff --git a/app/models/search/query.rb b/app/models/search/query.rb index a3af39976..122a1d90c 100644 --- a/app/models/search/query.rb +++ b/app/models/search/query.rb @@ -1,32 +1,26 @@ -class Search::Query - attr_reader :terms +class Search::Query < ApplicationRecord + validates :terms, presence: true + before_validation :sanitize_query_syntax class << self def wrap(query) if query.is_a?(self) - self.new(query) - else query + else + self.new(terms: query) end end end - def initialize(terms) - @terms = sanitize_query_syntax(terms) - end - - def valid? - @terms.present? - end - - alias to_s terms + alias_attribute :to_s, :terms private - def sanitize_query_syntax(terms) - terms = terms.to_s - terms = remove_invalid_search_characters(terms) - terms = remove_unbalanced_quotes(terms) - terms.presence + def sanitize_query_syntax + self.terms = begin + terms = remove_invalid_search_characters(self.terms) + terms = remove_unbalanced_quotes(terms) + terms.presence + end end def remove_invalid_search_characters(terms) diff --git a/app/models/user.rb b/app/models/user.rb index 5e295c4dd..40d6116c3 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,5 +1,6 @@ class User < ApplicationRecord - include Accessor, ActionText::Attachable, Assignee, Mentionable, Named, Role, SignalUser, Transferable + include Accessor, ActionText::Attachable, Assignee, Mentionable, Named, Role, Searcher, + SignalUser, Transferable include Timelined # Depends on Accessor has_one_attached :avatar diff --git a/app/models/user/searcher.rb b/app/models/user/searcher.rb new file mode 100644 index 000000000..c8d23b681 --- /dev/null +++ b/app/models/user/searcher.rb @@ -0,0 +1,15 @@ +module User::Searcher + extend ActiveSupport::Concern + + included do + has_many :search_queries, class_name: "Search::Query", dependent: :destroy + end + + def search(terms, max_results: 50) + Search.new(self, terms, max_results: max_results).results + end + + def remember_search(terms) + search_queries.create(terms: terms) if search_queries.last&.terms != terms + end +end diff --git a/app/views/searches/_form.html.erb b/app/views/searches/_form.html.erb new file mode 100644 index 000000000..8bbcf9483 --- /dev/null +++ b/app/views/searches/_form.html.erb @@ -0,0 +1,11 @@ +<%= form_with url: search_path, method: :get, class: "search__form" do |form| %> +
+ <%= text_field_tag :q, params[:q], class: "input", type: "search", placeholder: "What are you looking for?" %> + + <%= icon_tag "search" %> + + <%= link_to search_path, class: "search__field-clear btn btn--circle borderless" do %> + <%= icon_tag "close" %> + <% end %> +
+<% end %> diff --git a/app/views/searches/_search_query.html.erb b/app/views/searches/_search_query.html.erb new file mode 100644 index 000000000..318ea3956 --- /dev/null +++ b/app/views/searches/_search_query.html.erb @@ -0,0 +1,3 @@ +
  • + <%= link_to search_query.terms, search_path(q: search_query.terms) %> +
  • diff --git a/app/views/searches/show.html.erb b/app/views/searches/show.html.erb index 15000fee7..d71c4354d 100644 --- a/app/views/searches/show.html.erb +++ b/app/views/searches/show.html.erb @@ -13,27 +13,15 @@ <% end %> diff --git a/db/migrate/20250624080408_create_search_queries.rb b/db/migrate/20250624080408_create_search_queries.rb new file mode 100644 index 000000000..f5a5a0b6c --- /dev/null +++ b/db/migrate/20250624080408_create_search_queries.rb @@ -0,0 +1,12 @@ +class CreateSearchQueries < ActiveRecord::Migration[8.1] + def change + create_table :search_queries do |t| + t.references :user, null: false, foreign_key: true + t.string :terms, limit: 2000, null: false + + t.timestamps + + t.index %i[ user_id terms ] + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 054f9d6c2..825a6dbef 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2025_06_24_063244) do +ActiveRecord::Schema[8.1].define(version: 2025_06_24_080408) do create_table "accesses", force: :cascade do |t| t.integer "collection_id", null: false t.datetime "created_at", null: false @@ -304,6 +304,15 @@ ActiveRecord::Schema[8.1].define(version: 2025_06_24_063244) do # Unknown type '' for column 'rowid' + create_table "search_queries", force: :cascade do |t| + t.datetime "created_at", null: false + t.string "terms", limit: 2000, null: false + t.datetime "updated_at", null: false + t.integer "user_id", null: false + t.index ["user_id", "terms"], name: "index_search_queries_on_user_id_and_terms" + t.index ["user_id"], name: "index_search_queries_on_user_id" + end + create_table "search_results", force: :cascade do |t| t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -331,6 +340,7 @@ ActiveRecord::Schema[8.1].define(version: 2025_06_24_063244) do t.datetime "created_at", null: false t.string "title" t.datetime "updated_at", null: false + t.index ["title"], name: "index_tags_on_account_id_and_title", unique: true end create_table "users", force: :cascade do |t| @@ -391,6 +401,7 @@ ActiveRecord::Schema[8.1].define(version: 2025_06_24_063244) do add_foreign_key "notifications", "users", column: "creator_id" add_foreign_key "pins", "cards" add_foreign_key "pins", "users" + add_foreign_key "search_queries", "users" add_foreign_key "sessions", "users" add_foreign_key "taggings", "cards" add_foreign_key "taggings", "tags" diff --git a/db/schema_cache.yml b/db/schema_cache.yml index 4206c7468..f37790897 100644 --- a/db/schema_cache.yml +++ b/db/schema_cache.yml @@ -913,6 +913,31 @@ columns: default_function: collation: comment: + search_queries: + - *5 + - *6 + - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + auto_increment: + name: terms + cast_type: !ruby/object:ActiveModel::Type::String + true: t + false: f + precision: + scale: + limit: 2000 + sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type: varchar(2000) + type: :string + limit: 2000 + precision: + scale: + 'null': false + default: + default_function: + collation: + comment: + - *9 + - *25 search_results: - *5 - *6 @@ -1088,6 +1113,7 @@ primary_keys: reactions: id schema_migrations: version search_embeddings_vector_chunks00: rowid + search_queries: id search_results: id sessions: id taggings: id @@ -1130,6 +1156,7 @@ data_sources: reactions: true schema_migrations: true search_embeddings_vector_chunks00: true + search_queries: true search_results: true sessions: true taggings: true @@ -2191,6 +2218,40 @@ indexes: valid: true schema_migrations: [] search_embeddings_vector_chunks00: [] + search_queries: + - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition + table: search_queries + name: index_search_queries_on_user_id + unique: false + columns: + - user_id + lengths: {} + orders: {} + opclasses: {} + where: + type: + using: + include: + nulls_not_distinct: + comment: + valid: true + - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition + table: search_queries + name: index_search_queries_on_user_id_and_terms + unique: false + columns: + - user_id + - terms + lengths: {} + orders: {} + opclasses: {} + where: + type: + using: + include: + nulls_not_distinct: + comment: + valid: true search_results: [] sessions: - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition @@ -2243,7 +2304,23 @@ indexes: nulls_not_distinct: comment: valid: true - tags: [] + tags: + - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition + table: tags + name: index_tags_on_account_id_and_title + unique: true + columns: + - title + lengths: {} + orders: {} + opclasses: {} + where: + type: + using: + include: + nulls_not_distinct: + comment: + valid: true users: - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition table: users @@ -2344,4 +2421,4 @@ indexes: comment: valid: true workflows: [] -version: 20250624063244 +version: 20250624080408