diff options
author | Marius Balteanu <marius.balteanu@zitec.com> | 2021-10-05 19:54:31 +0000 |
---|---|---|
committer | Marius Balteanu <marius.balteanu@zitec.com> | 2021-10-05 19:54:31 +0000 |
commit | 506fc9d74cdb67af2eaea27662420ec07e32b2f3 (patch) | |
tree | 523ac0beb65ba62944c99924397c45669d0f23d6 | |
parent | 04e27aa1616f78381fab69b2b72ba5874e4cc557 (diff) | |
download | redmine-506fc9d74cdb67af2eaea27662420ec07e32b2f3.tar.gz redmine-506fc9d74cdb67af2eaea27662420ec07e32b2f3.zip |
Tokenize search parameter in order to allow multiple search terms in:
* the "contains" operator of text filters
* in issue autocomplete
Patch by Jens Krämer.
git-svn-id: http://svn.redmine.org/redmine/trunk@21238 e93f8b46-1217-0410-a6f0-8f06a7374b81
-rw-r--r-- | app/models/issue.rb | 3 | ||||
-rw-r--r-- | app/models/query.rb | 24 | ||||
-rw-r--r-- | lib/redmine/search.rb | 26 | ||||
-rw-r--r-- | test/unit/lib/redmine/search_test.rb | 27 |
4 files changed, 64 insertions, 16 deletions
diff --git a/app/models/issue.rb b/app/models/issue.rb index 70b408a21..55962f0fe 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -100,9 +100,8 @@ class Issue < ActiveRecord::Base ids.any? ? where(:assigned_to_id => ids) : none end) scope :like, (lambda do |q| - q = q.to_s if q.present? - where("LOWER(#{table_name}.subject) LIKE LOWER(?)", "%#{sanitize_sql_like q}%") + where(*::Query.tokenized_like_conditions("#{table_name}.subject", q)) end end) diff --git a/app/models/query.rb b/app/models/query.rb index bafdd3c2c..024fe591e 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -1440,11 +1440,25 @@ class Query < ActiveRecord::Base prefix = suffix = nil prefix = '%' if options[:ends_with] suffix = '%' if options[:starts_with] - prefix = suffix = '%' if prefix.nil? && suffix.nil? - value = queried_class.sanitize_sql_like value - queried_class.sanitize_sql_for_conditions( - [Redmine::Database.like(db_field, '?', :match => options[:match]), "#{prefix}#{value}#{suffix}"] - ) + if prefix || suffix + value = queried_class.sanitize_sql_like value + queried_class.sanitize_sql_for_conditions( + [Redmine::Database.like(db_field, '?', :match => options[:match]), "#{prefix}#{value}#{suffix}"] + ) + else + queried_class.sanitize_sql_for_conditions( + ::Query.tokenized_like_conditions(db_field, value, **options) + ) + end + end + + def self.tokenized_like_conditions(db_field, value, **options) + tokens = Redmine::Search::Tokenizer.new(value).tokens + tokens = [value] unless tokens.present? + sql, values = tokens.map do |token| + [Redmine::Database.like(db_field, '?', options), "%#{sanitize_sql_like token}%"] + end.transpose + [sql.join(" AND "), *values] end # Adds a filter for the given custom field diff --git a/lib/redmine/search.rb b/lib/redmine/search.rb index 3aeedbcd2..7eae79b5e 100644 --- a/lib/redmine/search.rb +++ b/lib/redmine/search.rb @@ -57,15 +57,7 @@ module Redmine @projects = projects @cache = options.delete(:cache) @options = options - - # extract tokens from the question - # eg. hello "bye bye" => ["hello", "bye bye"] - @tokens = @question.scan(%r{((\s|^)"[^"]+"(\s|$)|\S+)}).collect {|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')} - # tokens must be at least 2 characters long - # but for Chinese characters (Chinese HANZI/Japanese KANJI), tokens can be one character - @tokens = @tokens.uniq.select {|w| w.length > 1 || w =~ /\p{Han}/} - # no more than 5 tokens to search for - @tokens.slice! 5..-1 + @tokens = Tokenizer.new(@question).tokens end # Returns the total result count @@ -135,6 +127,22 @@ module Redmine end end + class Tokenizer + def initialize(question) + @question = question.to_s + end + + def tokens + # extract tokens from the question + # eg. hello "bye bye" => ["hello", "bye bye"] + tokens = @question.scan(%r{((\s|^)"[^"]+"(\s|$)|\S+)}).collect {|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')} + # tokens must be at least 2 characters long + # but for Chinese characters (Chinese HANZI/Japanese KANJI), tokens can be one character + # no more than 5 tokens to search for + tokens.uniq.select{|w| w.length > 1 || w =~ /\p{Han}/}.first 5 + end + end + module Controller def self.included(base) base.extend(ClassMethods) diff --git a/test/unit/lib/redmine/search_test.rb b/test/unit/lib/redmine/search_test.rb new file mode 100644 index 000000000..d369b5370 --- /dev/null +++ b/test/unit/lib/redmine/search_test.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# Redmine - project management software +# Copyright (C) 2006-2021 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../../test_helper', __FILE__) + +class Redmine::Search::Tokenize < ActiveSupport::TestCase + def test_tokenize + value = "hello \"bye bye\"" + assert_equal ["hello", "bye bye"], Redmine::Search::Tokenizer.new(value).tokens + end +end |