diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 10d11b15..78210ed6 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -129,6 +129,64 @@ class SearchController < ApplicationController
@seen_result_pages = {}
end
+ def autocomplete
+ @query = params[:q]&.strip
+
+ # Return empty results for short or empty queries
+ if @query.blank? || @query.length < 2
+ render json: []
+ return
+ end
+
+ results = []
+
+ # Fast name-only search across all content types
+ Rails.application.config.content_types[:all].each do |content_type|
+ begin
+ model_class = content_type.name.constantize
+
+ # Only search content types that have name columns
+ if model_class.column_names.include?('name')
+ # Fast query with limit to prevent slow searches
+ matching_entities = model_class
+ .where(user_id: current_user.id)
+ .where("LOWER(name) LIKE LOWER(?)", "%#{@query}%")
+ .limit(5) # Limit per content type
+ .select(:id, :name, :created_at) # Only select needed columns
+
+ matching_entities.each do |entity|
+ results << {
+ id: entity.id,
+ name: entity.name,
+ type: content_type.name,
+ icon: content_type.icon,
+ color: content_type.hex_color,
+ url: main_app.polymorphic_path(entity),
+ created_at: entity.created_at
+ }
+ end
+ end
+ rescue => e
+ # Skip any content types that have issues
+ Rails.logger.debug "Autocomplete: Skipping #{content_type.name}: #{e.message}"
+ end
+
+ # Stop if we have enough results
+ break if results.size >= 15
+ end
+
+ # Sort by relevance: exact matches first, then by name
+ results.sort_by! do |result|
+ exact_match = result[:name].downcase == @query.downcase ? 0 : 1
+ [exact_match, result[:name].downcase]
+ end
+
+ # Limit total results
+ results = results.first(12)
+
+ render json: results
+ end
+
helper_method :highlight_search_terms
private
diff --git a/app/views/layouts/tailwind/navbar/_logged_in.html.erb b/app/views/layouts/tailwind/navbar/_logged_in.html.erb
index c5817177..ec30c69d 100644
--- a/app/views/layouts/tailwind/navbar/_logged_in.html.erb
+++ b/app/views/layouts/tailwind/navbar/_logged_in.html.erb
@@ -27,21 +27,98 @@
<% if user_signed_in? %>
- <%= form_tag main_app.search_path, method: :get, class: 'hidden md:inline-flex items-center relative' do %>
+
+
+
-
- <% end %>
+ type="search"
+ x-model="query"
+ @input="handleInput()"
+ @keydown="handleKeydown($event)"
+ @focus="handleFocus()"
+ placeholder="Search your notebook"
+ autocomplete="off">
+
+
+
+
+
+
+
+
info
+
+
Quick Search Tips:
+
+ -
+ keyboard
+ Start typing to see your pages as suggestions
+
+ -
+ mouse
+ Click on any result to quickly navigate to it
+
+ -
+ search
+ Press Enter to search all content in your notebook
+
+
+
+
+
+
+
+
+ refresh
+ Searching...
+
+
+
+
+
+
+
+
+
+
+ arrow_forward
+
+
+
+
+
+ No pages found matching ""
+
+
+
+
+
+
@@ -136,4 +213,113 @@
<% end %>
-
\ No newline at end of file
+
+
+
\ No newline at end of file
diff --git a/config/routes.rb b/config/routes.rb
index 5c9ea5c0..a6d43bba 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -354,6 +354,7 @@ Rails.application.routes.draw do
end
get 'search/', to: 'search#results'
+ get 'search/autocomplete', to: 'search#autocomplete'
authenticate :user, lambda { |u| u.site_administrator? || Rails.env.development? } do
scope 'admin' do
diff --git a/db/migrate/20250707024945_add_name_indexes_to_content_types.rb b/db/migrate/20250707024945_add_name_indexes_to_content_types.rb
new file mode 100644
index 00000000..5871357b
--- /dev/null
+++ b/db/migrate/20250707024945_add_name_indexes_to_content_types.rb
@@ -0,0 +1,47 @@
+class AddNameIndexesToContentTypes < ActiveRecord::Migration[6.1]
+ def up
+ # Add optimized name search indexes for all content types
+ content_tables = %w[
+ universes characters locations items buildings conditions continents countries creatures deities
+ floras foods governments groups jobs landmarks languages lores magics planets races religions
+ scenes schools sports technologies towns traditions vehicles
+ ]
+
+ content_tables.each do |table_name|
+ next unless table_exists?(table_name)
+ next unless column_exists?(table_name, :name)
+ next unless column_exists?(table_name, :user_id)
+
+ if ActiveRecord::Base.connection.adapter_name.downcase.include?('postgresql')
+ # PostgreSQL: Add trigram index for fast LIKE searches
+ begin
+ execute "CREATE EXTENSION IF NOT EXISTS pg_trgm"
+ add_index table_name, [:user_id, :name], name: "idx_#{table_name}_user_name"
+ add_index table_name, :name, using: :gin, opclass: {name: :gin_trgm_ops}, name: "idx_#{table_name}_name_trgm"
+ rescue => e
+ Rails.logger.warn "Could not add trigram index to #{table_name}: #{e.message}"
+ # Fallback to regular index
+ add_index table_name, [:user_id, :name], name: "idx_#{table_name}_user_name" unless index_exists?(table_name, [:user_id, :name])
+ end
+ else
+ # SQLite and other databases: Regular indexes
+ add_index table_name, [:user_id, :name], name: "idx_#{table_name}_user_name" unless index_exists?(table_name, [:user_id, :name])
+ end
+ end
+ end
+
+ def down
+ content_tables = %w[
+ universes characters locations items buildings conditions continents countries creatures deities
+ floras foods governments groups jobs landmarks languages lores magics planets races religions
+ scenes schools sports technologies towns traditions vehicles
+ ]
+
+ content_tables.each do |table_name|
+ next unless table_exists?(table_name)
+
+ remove_index table_name, name: "idx_#{table_name}_user_name" if index_exists?(table_name, [:user_id, :name], name: "idx_#{table_name}_user_name")
+ remove_index table_name, name: "idx_#{table_name}_name_trgm" if index_exists?(table_name, :name, name: "idx_#{table_name}_name_trgm")
+ end
+ end
+end