Skip to content

Commit 5ca3d6a

Browse files
committed
feat: Enhance search functionality with submitter filtering and tips
- Added support for filtering search results by submitter using the `submitter:username` query. - Updated search methods to handle cases where both query and submitter are specified. - Introduced search tips in the UI to guide users on using the new search operators. - Adjusted the Docker configuration to change the exposed port from 3000 to 3080.
1 parent 2729299 commit 5ca3d6a

File tree

5 files changed

+75
-18
lines changed

5 files changed

+75
-18
lines changed

app/models/search.rb

Lines changed: 59 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,29 @@ def page_count
4646
end
4747

4848
def search_for_user!(user)
49-
# Extract domain query since it must be done separately
49+
# Extract special search operators
5050
domain = nil
51+
submitter = nil
5152
words = q.to_s.split(" ").reject { |w|
5253
if (m = w.match(/^domain:(.+)$/))
5354
domain = m[1]
55+
elsif (m = w.match(/^submitter:(.+)$/i))
56+
submitter = m[1]
5457
end
5558
}.join(" ")
5659

60+
# Handle submitter search - find user by username
61+
submitter_user = nil
62+
if submitter.present?
63+
submitter_user = User.find_by("LOWER(username) = ?", submitter.downcase)
64+
if submitter_user.nil? && words.blank? && domain.blank?
65+
self.results = []
66+
self.total_results = 0
67+
self.page = 0
68+
return false
69+
end
70+
end
71+
5772
# Handle domain search
5873
story_ids = []
5974
if domain.present?
@@ -80,18 +95,20 @@ def search_for_user!(user)
8095
end
8196
end
8297

83-
# Escape query for FULLTEXT search
84-
query = ActiveRecord::Base.connection.quote_string(words)
98+
# Sanitize query for FULLTEXT BOOLEAN MODE
99+
# Escape special characters that have meaning in boolean mode
100+
sanitized_words = sanitize_fulltext_query(words)
101+
query = ActiveRecord::Base.connection.quote_string(sanitized_words)
85102

86103
# Build search based on 'what' parameter
87104
results_array = []
88105

89106
if what == "all" || what == "stories"
90-
results_array.concat(search_stories(query, story_ids))
107+
results_array.concat(search_stories(query, story_ids, submitter_user))
91108
end
92109

93110
if what == "all" || what == "comments"
94-
results_array.concat(search_comments(query))
111+
results_array.concat(search_comments(query, submitter_user))
95112
end
96113

97114
# Sort results
@@ -145,12 +162,17 @@ def search_for_user!(user)
145162

146163
private
147164

148-
def search_stories(query, story_ids = [])
149-
# Return empty if no query AND no story_ids (domain search)
150-
return [] if query.blank? && story_ids.empty?
165+
def search_stories(query, story_ids = [], submitter_user = nil)
166+
# Return empty if no query AND no story_ids AND no submitter
167+
return [] if query.blank? && story_ids.empty? && submitter_user.nil?
151168

152169
relation = Story.joins(:user).where(is_expired: false)
153170

171+
# Filter by submitter if specified
172+
if submitter_user
173+
relation = relation.where(user_id: submitter_user.id)
174+
end
175+
154176
# Filter by story_ids if domain search
155177
if story_ids.any?
156178
relation = relation.where(id: story_ids)
@@ -162,23 +184,43 @@ def search_stories(query, story_ids = [])
162184
.where("MATCH(stories.title, stories.description, stories.url) AGAINST(? IN BOOLEAN MODE)", query)
163185
.select("stories.*, MATCH(stories.title, stories.description, stories.url) AGAINST('#{query}' IN BOOLEAN MODE) as relevance")
164186
else
165-
# Domain-only search: no relevance score
187+
# Domain-only or submitter-only search: no relevance score
166188
relation.select("stories.*, 0 as relevance")
167189
end
168190

169191
relation.includes(:user, :tags).to_a
170192
end
171193

172-
def search_comments(query)
173-
return [] if query.blank?
194+
def search_comments(query, submitter_user = nil)
195+
# Return empty if no query AND no submitter
196+
return [] if query.blank? && submitter_user.nil?
174197

175-
Comment.joins(:user, :story)
198+
relation = Comment.joins(:user, :story)
176199
.where(is_deleted: false, is_moderated: false)
177-
.where("MATCH(comment) AGAINST(? IN BOOLEAN MODE)", query)
178-
.select("comments.*,
179-
MATCH(comment) AGAINST('#{query}' IN BOOLEAN MODE) as relevance")
180-
.includes(:user, :story)
181-
.to_a
200+
201+
# Filter by submitter if specified
202+
if submitter_user
203+
relation = relation.where(user_id: submitter_user.id)
204+
end
205+
206+
# Add FULLTEXT search only if we have a query
207+
relation = if query.present?
208+
relation
209+
.where("MATCH(comment) AGAINST(? IN BOOLEAN MODE)", query)
210+
.select("comments.*, MATCH(comment) AGAINST('#{query}' IN BOOLEAN MODE) as relevance")
211+
else
212+
# Submitter-only search: no relevance score
213+
relation.select("comments.*, 0 as relevance")
214+
end
215+
216+
relation.includes(:user, :story).to_a
217+
end
218+
219+
def sanitize_fulltext_query(query)
220+
# In MySQL FULLTEXT BOOLEAN MODE, certain characters have special meaning:
221+
# + = must include, - = must exclude, * = wildcard, " = phrase, etc.
222+
# We escape these to treat them as literal characters
223+
query.to_s.gsub(/[+\-<>()~*"]/, " ")
182224
end
183225

184226
def sort_results(results)

app/views/search/index.html.erb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@
4343
<%= radio_button_tag "order", "points", @search.order == "points" %>
4444
<label for="order_points" class="normal"><%= t('.points') %></label>
4545
</div>
46+
47+
<div class="boxline search_tips">
48+
<details>
49+
<summary><%= t('.search_tips_title') %></summary>
50+
<p><%= raw t('.search_tips_html') %></p>
51+
</details>
52+
</div>
4653
<% end %>
4754
</div>
4855

config/locales/en.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,10 @@ en:
297297
points: "Points"
298298
searchresults : "%{searchnumber} result%{plural} for \"%{query}\""
299299
searchplural: "s"
300+
search_tips_title: "Search tips"
301+
search_tips_html: |
302+
<code>submitter:username</code> — search posts by a user<br>
303+
<code>domain:example.com</code> — search links from a domain
300304
settings:
301305
delete_account:
302306
deleteaccountflash: "Your account has been deleted."

config/locales/fr.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,10 @@ fr:
328328
points: "Score"
329329
searchresults : "%{searchnumber} résultat%{plural} pour \"%{query}\""
330330
searchplural: "s"
331+
search_tips_title: "Astuces de recherche"
332+
search_tips_html: |
333+
<code>submitter:username</code> — rechercher les publications d'un utilisateur<br>
334+
<code>domain:example.com</code> — rechercher les liens d'un domaine
331335
settings:
332336
delete_account:
333337
deleteaccountflash: "Votre compte a été supprimé."

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ services:
2929
- .:/app
3030
# - bundle_cache:/usr/local/bundle
3131
ports:
32-
- "3000:3000"
32+
- "3080:3000"
3333
depends_on:
3434
- db
3535
- mailpit

0 commit comments

Comments
 (0)