Skip to content

Commit

Permalink
Merge pull request #502 from slovensko-digital/GO-491/substring-search
Browse files Browse the repository at this point in the history
Allow prefix fulltext search using an asterisk
  • Loading branch information
luciajanikova authored Nov 16, 2024
2 parents 9172720 + b93c418 commit 3c6a5ed
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 23 deletions.
2 changes: 1 addition & 1 deletion app/models/filter_subscription.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 28 additions & 19 deletions app/models/searchable/message_thread.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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: '<span class="bg-yellow-200 text-gray-950">', # if you change classed aad them to view in comment
StopSel: '</span>',
MaxWords: 15,
MinWords: 0,
ShortWord: 1,
HighlightAll: true,
MaxFragments: 2,
FragmentDelimiter: '&hellip;'
}
}
}


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: '<span class="bg-yellow-200 text-gray-950">', # if you change classed aad them to view in comment
StopSel: '</span>',
MaxWords: 15,
MinWords: 0,
ShortWord: 1,
HighlightAll: true,
MaxFragments: 2,
FragmentDelimiter: '&hellip;'
}
}
}
}
}

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

Expand All @@ -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
Expand Down
12 changes: 9 additions & 3 deletions app/models/searchable/message_thread_query.rb
Original file line number Diff line number Diff line change
@@ -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 = []
Expand All @@ -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)
Expand All @@ -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
Expand Down
27 changes: 27 additions & 0 deletions test/models/searchable/message_thread_query_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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: []
}
Expand All @@ -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: []
}
Expand All @@ -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']
}
Expand All @@ -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']
}
Expand All @@ -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: ["*"]
}
Expand All @@ -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

0 comments on commit 3c6a5ed

Please sign in to comment.