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 %>
- <%= 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 %>
+ <%= render "searches/form" %>
+
+ <%= auto_submit_form_with url: search_path(q: params[:q]), method: :post %>
- <%= render partial: "searches/result", collection: @search.results %>
+ <%= render partial: "searches/result", collection: @search_results %>
- <% 7.times do %>
- -
- <%= link_to "historic search term", "#" %>
-
- <% end %>
+ <%= render partial: "searches/search_query", collection: @recent_search_queries %>
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