Это решение Rails, которое создает самообращающиеся соединения для случая AND
и простое включение SQL для случая OR
. В решении используется Модель с именем TopicTag и, следовательно, таблица с именем topic_tags.
Метод класса Search ожидает 2 аргумента: массив тегов и строку, содержащую либо «и», либо «или»
class TopicTag < ActiveRecord::Base
def self.search(tags, andor)
# Ensure tags are unique or you will get duplicate table names in the SQL
tags.uniq!
if andor.downcase == "and"
first = true
sql = ""
tags.each do |tag|
if first
sql = "SELECT DISTINCT topic_tags.topic_id FROM topic_tags "
first = false
else
sql += " JOIN topic_tags as tag_#{tag} ON tag_#{tag}.topic_id = \
topic_tags.topic_id AND tag_#{tag}.tag = '#{tag}'"
end
end
sql += " WHERE topic_tags.tag = '#{tags[0]}'"
TopicTag.find_by_sql(sql)
else
TopicTag.find(:all, :select => 'DISTINCT topic_id',
:conditions => { :tag => tags})
end
end
end
Чтобы получить больше тестового покрытия, данные были расширены, чтобы включить дополнительную запись для шахмат. База данных была заполнена следующим кодом
[1,2].each {|i| TopicTag.create(:topic_id => i, :tag => 'football')}
[1,3].each {|i| TopicTag.create(:topic_id => i, :tag => 'cricket')}
[2,3,4].each {|i| TopicTag.create(:topic_id => i, :tag => 'basketball')}
[4,5].each {|i| TopicTag.create(:topic_id => i, :tag => 'chess')}
Следующий тестовый код дал показанные результаты
tests = [
%w[football cricket],
%w[chess],
%w[chess cricket basketball]
]
tests.each do |test|
%w[and or].each do |op|
puts test.join(" #{op} ") + " = " +
(TopicTag.search(test, op).map(&:topic_id)).join(', ')
end
end
football and cricket = 1
football or cricket = 1, 2, 3
chess = 4, 5
chess = 4, 5
chess and cricket and basketball =
chess or cricket or basketball = 1, 2, 3, 4, 5
Протестировано на Rails 2.3.8 с использованием SqlLite
EDIT
Если вы хотите использовать подобное, то чехол OR
также становится немного более сложным. Вы также должны знать, что использование LIKE с лидирующим символом «%» может значительно повлиять на производительность, если таблица, которую вы ищете, имеет нетривиальный размер.
Следующая версия модели использует LIKE для обоих случаев.
class TopicTag < ActiveRecord::Base
def self.search(tags, andor)
tags.uniq!
if andor.downcase == "and"
first = true
first_name = ""
sql = ""
tags.each do |tag|
if first
sql = "SELECT DISTINCT topic_tags.topic_id FROM topic_tags "
first = false
else
sql += " JOIN topic_tags as tag_#{tag} ON tag_#{tag}.topic_id = \
topic_tags.topic_id AND tag_#{tag}.tag like '%#{tag}%'"
end
end
sql += " WHERE topic_tags.tag like '%#{tags[0]}%'"
TopicTag.find_by_sql(sql)
else
first = true
tag_sql = ""
tags.each do |tag|
if first
tag_sql = " tag like '%#{tag}%'"
first = false
else
tag_sql += " OR tag like '%#{tag}%'"
end
end
TopicTag.find(:all, :select => 'DISTINCT topic_id',
:conditions => tag_sql)
end
end
end
tests = [
%w[football cricket],
%w[chess],
%w[chess cricket basketball],
%w[chess ll],
%w[ll]
]
tests.each do |test|
%w[and or].each do |op|
result = TopicTag.search(test, op).map(&:topic_id)
puts ( test.size == 1 ? "#{test}(#{op})" : test.join(" #{op} ") ) +
" = " + result.join(', ')
end
end
football and cricket = 1
football or cricket = 1, 2, 3
chess(and) = 4, 5
chess(or) = 4, 5
chess and cricket and basketball =
chess or cricket or basketball = 1, 2, 3, 4, 5
chess and ll = 4
chess or ll = 1, 2, 3, 4, 5
ll(and) = 1, 2, 3, 4
ll(or) = 1, 2, 3, 4