add tag scoped search

This commit is contained in:
Michael Williams 2018-03-13 10:55:11 -07:00 committed by Peter Bhat Harkins
parent bb65fddd2a
commit d64700a9c1
2 changed files with 152 additions and 19 deletions

View File

@ -53,15 +53,26 @@ class Search
end
end
def with_tags(base, tag_scopes)
base
.joins({ :taggings => :tag }, :user)
.where(:tags => { :tag => tag_scopes })
.having("COUNT(stories.id) = ?", tag_scopes.length)
.group("stories.id")
end
def search_for_user!(user)
self.results = []
self.total_results = 0
# extract domain query since it must be done separately
domain = nil
tag_scopes = []
words = self.q.to_s.split(" ").reject{|w|
if m = w.match(/^domain:(.+)$/)
domain = m[1]
elsif m = w.match(/^tag:(.+)$/)
tag_scopes << m[1]
end
}.join(" ")
@ -71,9 +82,7 @@ class Search
case self.what
when "stories"
base = Story.unmerged.where(:is_expired => false).
includes({ :taggings => :tag }, :user)
base = Story.unmerged.where(:is_expired => false)
if domain.present?
begin
reg = Regexp.new("//([^/]*\.)?#{domain}/")
@ -84,36 +93,49 @@ class Search
end
end
title_match_sql = "MATCH(stories.title) AGAINST('#{qwords}' IN BOOLEAN MODE)"
description_match_sql = "MATCH(stories.description) AGAINST('#{qwords}' IN BOOLEAN MODE)"
story_cache_match_sql = "MATCH(stories.story_cache) AGAINST('#{qwords}' IN BOOLEAN MODE)"
if qwords.present?
base.where!(
"(MATCH(title) AGAINST('#{qwords}' IN BOOLEAN MODE) OR " +
"MATCH(description) AGAINST('#{qwords}' IN BOOLEAN MODE) OR " +
"MATCH(story_cache) AGAINST('#{qwords}' IN BOOLEAN MODE))"
"(#{title_match_sql} OR " +
"#{description_match_sql} OR " +
"#{story_cache_match_sql})"
)
self.results = base.select(
"stories.*, " +
"MATCH(title) AGAINST('#{qwords}' IN BOOLEAN MODE) AS rel_title, " +
"MATCH(description) AGAINST('#{qwords}' IN BOOLEAN MODE) AS rel_description, " +
"MATCH(story_cache) AGAINST('#{qwords}' IN BOOLEAN MODE) AS rel_story_cache"
)
if tag_scopes.present?
self.results = with_tags(base, tag_scopes)
else
base = base.includes({ :taggings => :tag }, :user)
self.results = base.select(
"stories.*, " +
"#{title_match_sql}, " +
"#{description_match_sql}, " +
"#{story_cache_match_sql}"
)
end
else
self.results = base
if tag_scopes.present?
self.results = with_tags(base, tag_scopes)
else
self.results = base.includes({ :taggings => :tag }, :user)
end
end
case self.order
when "relevance"
if qwords.present?
self.results.order!(
"(rel_title * 2) DESC, " +
"(rel_description * 1.5) DESC, " +
"(rel_story_cache) DESC"
"((#{title_match_sql}) * 2) DESC, " +
"((#{description_match_sql}) * 1.5) DESC, " +
"(#{story_cache_match_sql}) DESC"
)
else
self.results.order!("created_at DESC")
self.results.order!("stories.created_at DESC")
end
when "newest"
self.results.order!("created_at DESC")
self.results.order!("stories.created_at DESC")
when "points"
self.results.order!("#{Story.score_sql} DESC")
end
@ -138,7 +160,11 @@ class Search
end
end
self.total_results = base.count
if tag_scopes.present?
self.total_results = self.results.length
else
self.total_results = base.count
end
if self.page > self.page_count
self.page = self.page_count

107
spec/models/search_spec.rb Normal file
View File

@ -0,0 +1,107 @@
require "spec_helper"
describe Search do
# We need to set up and then teardown the environment
# outside of the typical RSpec transaction because
# the search module uses custom SQL that doesn't
# work inside the transaction
before(:all) do
@user = User.make!
@multi_tag = Story.make!(:title => "multitag term1 t1 t2",
:url => "https://example.com/3",
:user_id => @user.id,
:tags_a => ["tag1", "tag2"])
@stories = [
Story.make!(:title => "unique",
:url => "https://example.com/unique",
:user_id => @user.id,
:tags_a => ["tag1"]),
Story.make!(:title => "term1 domain1",
:url => "https://example.com/1",
:user_id => @user.id,
:tags_a => ["tag1"]),
Story.make!(:title => "term1 t2",
:url => "https://example.com/2",
:user_id => @user.id,
:tags_a => ["tag2"]),
@multi_tag,
Story.make!(:title => "term1 domain2",
:url => "https://lobste.rs/1",
:user_id => @user.id,
:tags_a => ["tag1"]),
]
end
after(:all) do
@user.destroy!
@stories.each { |s| s.destroy! }
end
it "can search for stories" do
search = Search.new
search.q = "unique"
search.search_for_user!(@user)
expect(search.results.length).to eq(1)
expect(search.results.first.title).to eq("unique")
end
it "can search for multitaged stories" do
search = Search.new
search.q = "multitag"
search.search_for_user!(@user)
expect(search.results.length).to eq(1)
expect(search.results.first.title).to eq("multitag term1 t1 t2")
end
it "can search for stories by domain" do
search = Search.new
search.q = "term1 domain:lobste.rs"
search.search_for_user!(@user)
expect(search.results.length).to eq(1)
expect(search.results.first.title).to eq("term1 domain2")
end
it "can search for stories by tag" do
search = Search.new
search.q = "term1 tag:tag1"
search.search_for_user!(@user)
expect(search.results.length).to eq(3)
# Stories with multiple tags should return all the tags
multi_tag_res = search.results.select do |res|
res.id == @multi_tag.id
end
expect(multi_tag_res.length).to eq(1)
expect(multi_tag_res.first.sorted_taggings.first.tag.tag).to eq("tag1")
expect(multi_tag_res.first.sorted_taggings.second.tag.tag).to eq("tag2")
end
it "should return only stories with both tags if multiple tags are present" do
search = Search.new
search.q = "term1 tag:tag1 tag:tag2"
search.search_for_user!(@user)
expect(search.results.length).to eq(1)
end
it "can search for stories with only tags" do
search = Search.new
search.q = "tag:tag2"
search.search_for_user!(@user)
expect(search.results.length).to eq(2)
end
end