diff --git a/app/models/filter_subscription.rb b/app/models/filter_subscription.rb
index cb088e142..4360ad256 100644
--- a/app/models/filter_subscription.rb
+++ b/app/models/filter_subscription.rb
@@ -70,7 +70,7 @@ def scope_searchable(searchable)
end
scope = scope.where.not("tag_ids && ARRAY[?]", query[:filter_out_tag_ids]) if query[:filter_out_tag_ids].present?
- scope = scope.fulltext_search(query[:fulltext]) if query[:fulltext].present?
+ scope = scope.fulltext_search(query[:fulltext], prefix_search: query[:prefix_search]) if query[:fulltext].present?
scope
end
diff --git a/app/models/searchable/message_thread.rb b/app/models/searchable/message_thread.rb
index 8fad30af7..0867c8906 100644
--- a/app/models/searchable/message_thread.rb
+++ b/app/models/searchable/message_thread.rb
@@ -22,30 +22,39 @@ class Searchable::MessageThread < ApplicationRecord
scope :with_tag_id, ->(tag_id) { where("tag_ids && ARRAY[?]", [tag_id]) }
include PgSearch::Model
- pg_search_scope :pg_search_all,
- against: [:title, :content, :note, :tag_names],
- using: {
- tsearch: {
- highlight: {
- StartSel: '', # if you change classed aad them to view in comment
- StopSel: '',
- MaxWords: 15,
- MinWords: 0,
- ShortWord: 1,
- HighlightAll: true,
- MaxFragments: 2,
- FragmentDelimiter: '…'
- }
- }
- }
+
+
+ pg_search_scope :pg_search_all, lambda { |query, is_prefix = false|
+ {
+ query: query,
+ against: [:title, :content, :note, :tag_names],
+ using: {
+ tsearch: {
+ prefix: is_prefix,
+ highlight: {
+ StartSel: '', # if you change classed aad them to view in comment
+ StopSel: '',
+ MaxWords: 15,
+ MinWords: 0,
+ ShortWord: 1,
+ HighlightAll: true,
+ MaxFragments: 2,
+ FragmentDelimiter: '…'
+ }
+ }
+ }
+ }
+ }
def self.matching(scopeable)
scopeable.scope_searchable(self) # double dispatch
end
- def self.fulltext_search(query)
+
+ def self.fulltext_search(query, prefix_search: false)
pg_search_all(
- Searchable::IndexHelpers.searchable_string(query)
+ Searchable::IndexHelpers.searchable_string(query),
+ prefix_search
)
end
@@ -71,7 +80,7 @@ def self.search_ids(query_filter, search_permissions:, cursor:, per_page:, direc
end
end
scope = scope.where.not("tag_ids && ARRAY[?]", query_filter[:filter_out_tag_ids]) if query_filter[:filter_out_tag_ids].present?
- scope = scope.fulltext_search(query_filter[:fulltext]).with_pg_search_highlight if query_filter[:fulltext].present?
+ scope = scope.fulltext_search(query_filter[:fulltext], prefix_search: query_filter[:prefix_search]).with_pg_search_highlight if query_filter[:fulltext].present?
scope = scope.select(:message_thread_id, :last_message_delivered_at)
# remove default order rule given by pg_search
diff --git a/app/models/searchable/message_thread_query.rb b/app/models/searchable/message_thread_query.rb
index d27b991b2..1c097911d 100644
--- a/app/models/searchable/message_thread_query.rb
+++ b/app/models/searchable/message_thread_query.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class Searchable::MessageThreadQuery
+ PREFIX_SEARCH_REGEXP = /^\S{4,}\*$/
+
def self.parse(query)
filter_labels = []
filter_out_labels = []
@@ -21,16 +23,19 @@ def self.parse(query)
with_text = with_text.gsub("#{match[0]}:#{match[1]}", "")
end
+ with_text = with_text.gsub(/\s+/, ' ').strip
+
{
- fulltext: with_text.gsub(/\s+/, ' ').strip,
+ fulltext: with_text,
+ prefix_search: with_text.match?(PREFIX_SEARCH_REGEXP),
filter_labels: filter_labels,
filter_out_labels: filter_out_labels,
}
end
def self.labels_to_ids(parsed_query, tenant:)
- fulltext, filter_labels, filter_out_labels =
- parsed_query.fetch_values(:fulltext, :filter_labels, :filter_out_labels)
+ fulltext, prefix_search, filter_labels, filter_out_labels =
+ parsed_query.fetch_values(:fulltext, :prefix_search, :filter_labels, :filter_out_labels)
# TODO maybe with one query
found_all, filter_tag_ids = label_names_to_tag_ids(tenant, filter_labels)
@@ -48,6 +53,7 @@ def self.labels_to_ids(parsed_query, tenant:)
result[:filter_out_tag_ids] = filter_out_tag_ids if filter_out_tag_ids.present?
result[:fulltext] = fulltext if fulltext.present?
+ result[:prefix_search] = prefix_search
result
end
diff --git a/test/models/searchable/message_thread_query_test.rb b/test/models/searchable/message_thread_query_test.rb
index 30eacb0e5..ae4315ca8 100644
--- a/test/models/searchable/message_thread_query_test.rb
+++ b/test/models/searchable/message_thread_query_test.rb
@@ -4,12 +4,14 @@ class Searchable::MessageThreadQueryTest < ActiveSupport::TestCase
test "parser empty" do
assert_equal Searchable::MessageThreadQuery.parse(''), {
fulltext: '',
+ prefix_search: false,
filter_labels: [],
filter_out_labels: []
}
assert_equal Searchable::MessageThreadQuery.parse(nil), {
fulltext: '',
+ prefix_search: false,
filter_labels: [],
filter_out_labels: []
}
@@ -18,6 +20,7 @@ class Searchable::MessageThreadQueryTest < ActiveSupport::TestCase
test "parser only include tag" do
assert_equal Searchable::MessageThreadQuery.parse('label:(tag one/with something)'), {
fulltext: '',
+ prefix_search: false,
filter_labels: ['tag one/with something'],
filter_out_labels: []
}
@@ -26,6 +29,7 @@ class Searchable::MessageThreadQueryTest < ActiveSupport::TestCase
test "parser only exclude tag" do
assert_equal Searchable::MessageThreadQuery.parse('-label:(without)'), {
fulltext: '',
+ prefix_search: false,
filter_labels: [],
filter_out_labels: ['without']
}
@@ -35,6 +39,7 @@ class Searchable::MessageThreadQueryTest < ActiveSupport::TestCase
query = 'label:(tag one/with something) hello label:(tag two) world -label:(without this tag) ending'
assert_equal Searchable::MessageThreadQuery.parse(query), {
fulltext: 'hello world ending',
+ prefix_search: false,
filter_labels: ['tag one/with something', 'tag two'],
filter_out_labels: ['without this tag']
}
@@ -44,6 +49,7 @@ class Searchable::MessageThreadQueryTest < ActiveSupport::TestCase
query = 'something -label:* else'
assert_equal Searchable::MessageThreadQuery.parse(query), {
fulltext: 'something else',
+ prefix_search: false,
filter_labels: [],
filter_out_labels: ["*"]
}
@@ -53,8 +59,29 @@ class Searchable::MessageThreadQueryTest < ActiveSupport::TestCase
query = 'something -label:* else -label:two'
assert_equal Searchable::MessageThreadQuery.parse(query), {
fulltext: 'something else',
+ prefix_search: false,
filter_labels: [],
filter_out_labels: ["*", "two"]
}
end
+
+ test "parser with prefix search" do
+ query = 'someth* -label:*'
+ assert_equal Searchable::MessageThreadQuery.parse(query), {
+ fulltext: 'someth*',
+ prefix_search: true,
+ filter_labels: [],
+ filter_out_labels: ["*"]
+ }
+ end
+
+ test "parser without prefix search for multiple words" do
+ query = 'tell me someth*'
+ assert_equal Searchable::MessageThreadQuery.parse(query), {
+ fulltext: 'tell me someth*',
+ prefix_search: false,
+ filter_labels: [],
+ filter_out_labels: []
+ }
+ end
end