notebook/app/controllers/search_controller.rb
Andrew Brown fe9bcf5cc4 Complete MaterializeCSS to TailwindCSS migration cleanup
This commit finalizes the transition from MaterializeCSS to TailwindCSS by:

**Layout & Controller Changes:**
- Consolidated layouts/tailwind.html.erb into layouts/application.html.erb
- Removed `layout 'tailwind'` declarations from 21 controllers
- Removed MaterializeCSS CDN link while preserving Material Icons

**Asset Cleanup:**
- Deleted materialize.min.js (76KB) and materialize-overrides.scss
- Removed tailwind.html.erb (no longer needed)
- Cleaned up MaterializeCSS-specific JavaScript files

**JavaScript Modernization:**
- Removed MaterializeCSS initialization and select components
- Updated autosave.js and timeline-editor.js to remove M.toast() calls
- Deprecated unused React component with MaterializeCSS dependencies

**Result:**
- Unified TailwindCSS layout system across entire application
- Complete removal of MaterializeCSS dependencies
- Preserved all functionality through Alpine.js implementations
- Clean codebase ready for production

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-27 19:39:15 -07:00

212 lines
7.4 KiB
Ruby

class SearchController < ApplicationController
before_action :authenticate_user!
def results
@query = params[:q]&.strip
@sort = params[:sort] || 'relevance'
@filter = params[:filter] || 'all'
# Return early if query is empty or too short
if @query.blank? || @query.length < 2
@matched_attributes = Attribute.none
@result_types = []
@seen_result_pages = {}
return
end
# Search both Attribute values AND entity names (for content still in old-style columns)
# 1. Search Attribute values (new-style fields) with multi-word support
@matched_attributes = Attribute
.joins(:attribute_field)
.where(user_id: current_user.id)
.where.not(value: [nil, ''])
# Build multi-word search conditions for attributes
search_words = @query.split(/\s+/).reject(&:blank?)
if search_words.length > 1
# Multi-word search: all words must be present (AND logic)
word_conditions = search_words.map { "LOWER(value) LIKE LOWER(?)" }.join(" AND ")
word_values = search_words.map { |word| "%#{word}%" }
@matched_attributes = @matched_attributes.where(word_conditions, *word_values)
else
# Single word search
@matched_attributes = @matched_attributes.where("LOWER(value) LIKE LOWER(?)", "%#{@query}%")
end
# 2. Also search entity names (old-style name columns) with multi-word support
@matched_names = []
Rails.application.config.content_types[:all].each do |content_type|
begin
model_class = content_type.name.constantize
if model_class.column_names.include?('name')
# Build multi-word search for entity names
if search_words.length > 1
# Multi-word search: all words must be present in name
name_conditions = search_words.map { "LOWER(name) LIKE LOWER(?)" }.join(" AND ")
name_values = search_words.map { |word| "%#{word}%" }
matching_entities = model_class
.where(user_id: current_user.id)
.where(name_conditions, *name_values)
else
# Single word search
matching_entities = model_class
.where(user_id: current_user.id)
.where("LOWER(name) LIKE LOWER(?)", "%#{@query}%")
end
matching_entities.each do |entity|
# Create a virtual attribute-like object for the name match
@matched_names << OpenStruct.new(
id: "name_#{entity.class.name}_#{entity.id}",
entity_type: entity.class.name,
entity_id: entity.id,
value: entity.name,
attribute_field: OpenStruct.new(label: 'Name', field_type: 'name'),
created_at: entity.created_at,
updated_at: entity.updated_at
)
end
end
rescue => e
# Skip any content types that don't exist or have issues
Rails.logger.debug "Skipping search in #{content_type.name}: #{e.message}"
end
end
# Get result types for filtering (before pagination)
@result_types = (@matched_attributes.pluck(:entity_type) + @matched_names.map(&:entity_type)).uniq
# Apply content type filter to both attribute results and name results
if @filter != 'all'
@matched_attributes = @matched_attributes.where(entity_type: @filter)
@matched_names.select! { |name_match| name_match.entity_type == @filter }
end
# Combine both result sets for sorting and pagination
all_matches = @matched_attributes.to_a + @matched_names
# Apply search refinement if provided
if params[:refine].present?
refine_query = params[:refine].strip
all_matches.select! do |match|
match.value.downcase.include?(refine_query.downcase)
end
end
# Apply sorting to combined results
case @sort
when 'relevance'
# Order by relevance: exact matches first, then by entity type and id
all_matches.sort_by! do |match|
exact_match = match.value.downcase == @query.downcase ? 0 : 1
[exact_match, match.entity_type, match.entity_id]
end
when 'recent'
all_matches.sort_by! { |match| -match.created_at.to_i }
when 'oldest'
all_matches.sort_by! { |match| match.created_at.to_i }
end
# Add pagination to prevent performance issues with large result sets
page = [params[:page]&.to_i || 1, 1].max # Ensure page is at least 1
per_page = 100
start_index = (page - 1) * per_page
@matched_attributes = all_matches[start_index, per_page] || []
# Debug: Log search info in development
if Rails.env.development?
Rails.logger.info "=== Search Debug ==="
Rails.logger.info "Query: '#{@query}'"
Rails.logger.info "Attribute matches: #{@matched_attributes.respond_to?(:count) ? @matched_attributes.count : @matched_attributes.size}"
Rails.logger.info "Name matches: #{@matched_names.size}"
Rails.logger.info "Total results: #{all_matches.size}"
Rails.logger.info "Sample results: #{@matched_attributes.first(3).map { |m| "#{m.attribute_field.label}: '#{m.value}' (#{m.entity_type}##{m.entity_id})" }}"
end
@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
def highlight_search_terms(text, query)
return text if query.blank?
words = query.split(/\s+/).reject(&:blank?)
highlighted = text
words.each do |word|
highlighted = highlighted.gsub(
/\b#{Regexp.escape(word)}\b/i,
'<mark class="bg-yellow-200 px-1 py-0.5 rounded text-black font-medium">\0</mark>'
)
end
highlighted.html_safe
end
def search_params
params.permit(:q, :sort, :filter, :page, :refine)
end
end