FEATURE: increase search expansion to 50 results

refactor search code to deal with proper objects
use proper serializers, test the controllers
This commit is contained in:
Sam 2014-09-02 19:15:08 +10:00
parent b04a52676e
commit 4f09d552ed
13 changed files with 261 additions and 205 deletions

View File

@ -57,21 +57,50 @@ export default Em.ArrayController.extend(Discourse.Presence, {
if (results) { if (results) {
self.set('noResults', results.length === 0); self.set('noResults', results.length === 0);
var index = 0; var topicMap = {};
results = _(['topic', 'category', 'user']) results.topics = results.topics.map(function(topic){
.map(function(n){ topic = Discourse.Topic.create(topic);
return _(results).where({type: n}).first(); topicMap[topic.id] = topic;
}) return topic;
.compact() });
.each(function(list){
_.each(list.results, function(item){
item.index = index++;
urls.pushObject(item.url);
});
})
.value();
self.setProperties({ resultCount: index, content: results, urls: urls }); results.posts = results.posts.map(function(post){
post = Discourse.Post.create(post);
post.set('topic', topicMap[post.topic_id]);
urls.push(post.get('url'));
return post;
});
results.users = results.users.map(function(user){
user = Discourse.User.create(user);
urls.push(user.get('path'));
return user;
});
results.categories = results.categories.map(function(category){
category = Discourse.Category.create(category);
urls.push(category.get('url'));
return category;
});
var r = results.grouped_search_result;
results.resultTypes = [];
// TODO: consider refactoring front end to take a better structure
[['topic','posts'],['user','users'],['category','categories']].forEach(function(pair){
var type = pair[0], name = pair[1];
if(results[name].length > 0) {
results.resultTypes.push({
results: results[name],
type: type,
more: r['more_' + name]
});
}
});
console.log(results)
self.setProperties({ resultCount: urls.length, content: results, urls: urls });
} }
self.set('loading', false); self.set('loading', false);

View File

@ -10,7 +10,7 @@
<div class='searching'></div> <div class='searching'></div>
{{else}} {{else}}
{{#unless noResults}} {{#unless noResults}}
{{#each resultType in content}} {{#each resultType in content.resultTypes}}
<ul> <ul>
<li class='heading'> <li class='heading'>
{{resultType.name}} {{resultType.name}}

View File

@ -1,3 +1,3 @@
<a href='{{unbound url}}'> <a href='{{unbound url}}'>
<span class='badge-category' style="background-color: #{{unbound color}}; color: #{{unbound text_color}};">{{unbound title}}</span> {{bound-category-link this}}
</a> </a>

View File

@ -1,6 +1,6 @@
<a class='search-link' href='{{unbound url}}'> <a class='search-link' href='{{unbound url}}'>
<span class='topic'> <span class='topic'>
{{unbound title}} {{unbound topic.title}}
</span> </span>
{{#unless Discourse.Mobile.mobileView}} {{#unless Discourse.Mobile.mobileView}}
<span class='blurb'> <span class='blurb'>

View File

@ -1,4 +1,4 @@
<a href='{{unbound url}}'> <a href='{{unbound path}}'>
{{avatar this usernamePath="title" imageSize="small"}} {{avatar this imageSize="small"}}
{{unbound title}} {{unbound username}}
</a> </a>

View File

@ -36,7 +36,8 @@ class SearchController < ApplicationController
end end
search = Search.new(params[:term], search_args.symbolize_keys) search = Search.new(params[:term], search_args.symbolize_keys)
render_json_dump(search.execute.as_json) result = search.execute
render_serialized(result, GroupedSearchResultSerializer, :result => result)
end end
end end

View File

@ -0,0 +1,6 @@
class GroupedSearchResultSerializer < ApplicationSerializer
has_many :posts, serializer: SearchPostSerializer
has_many :users, serializer: BasicUserSerializer
has_many :categories, serializer: BasicCategorySerializer
attributes :more_posts, :more_users, :more_categories
end

View File

@ -0,0 +1,9 @@
class SearchPostSerializer < PostSerializer
has_one :topic, serializer: ListableTopicSerializer
attributes :blurb
def blurb
options[:result].blurb(object)
end
end

View File

@ -8,6 +8,10 @@ class Search
5 5
end end
def self.per_filter
50
end
# Sometimes we want more topics than are returned due to exclusion of dupes. This is the # Sometimes we want more topics than are returned due to exclusion of dupes. This is the
# factor of extra results we'll ask for. # factor of extra results we'll ask for.
def self.burst_factor def self.burst_factor
@ -102,8 +106,16 @@ class Search
@guardian = @opts[:guardian] || Guardian.new @guardian = @opts[:guardian] || Guardian.new
@search_context = @opts[:search_context] @search_context = @opts[:search_context]
@include_blurbs = @opts[:include_blurbs] || false @include_blurbs = @opts[:include_blurbs] || false
@limit = Search.per_facet * Search.facets.size @limit = Search.per_facet
@results = GroupedSearchResults.new(@opts[:type_filter]) if @opts[:type_filter].present?
@limit = Search.per_filter
end
@results = GroupedSearchResults.new(@opts[:type_filter], term, @search_context, @include_blurbs)
end
def self.execute(term, opts=nil)
self.new(term, opts).execute
end end
# Query a term # Query a term
@ -112,15 +124,20 @@ class Search
# If the term is a number or url to a topic, just include that topic # If the term is a number or url to a topic, just include that topic
if @opts[:search_for_id] && @results.type_filter == 'topic' if @opts[:search_for_id] && @results.type_filter == 'topic'
return single_topic(@term.to_i).as_json if @term =~ /^\d+$/ if @term =~ /^\d+$/
begin single_topic(@term.to_i)
route = Rails.application.routes.recognize_path(@term) else
return single_topic(route[:topic_id]).as_json if route[:topic_id].present? begin
rescue ActionController::RoutingError route = Rails.application.routes.recognize_path(@term)
single_topic(route[:topic_id]) if route[:topic_id].present?
rescue ActionController::RoutingError
end
end end
end end
find_grouped_results.as_json find_grouped_results unless @results.posts.present?
@results
end end
private private
@ -151,22 +168,24 @@ class Search
expected_topics = 0 expected_topics = 0
expected_topics = Search.facets.size unless @results.type_filter.present? expected_topics = Search.facets.size unless @results.type_filter.present?
expected_topics = Search.per_facet * Search.facets.size if @results.type_filter == 'topic' expected_topics = Search.per_facet * Search.facets.size if @results.type_filter == 'topic'
expected_topics -= @results.topic_count expected_topics -= @results.posts.length
if expected_topics > 0 if expected_topics > 0
extra_posts = posts_query(expected_topics * Search.burst_factor) extra_posts = posts_query(expected_topics * Search.burst_factor)
extra_posts = extra_posts.where("posts.topic_id NOT in (?)", @results.topic_ids) if @results.topic_ids.present? extra_posts = extra_posts.where("posts.topic_id NOT in (?)", @results.posts.map(&:topic_id)) if @results.posts.present?
extra_posts.each do |p| extra_posts.each do |post|
@results.add_result(SearchResult.from_post(p, @search_context, @term, @include_blurbs)) @results.add(post)
expected_topics -= 1
break if expected_topics == 0
end end
end end
end end
# If we're searching for a single topic # If we're searching for a single topic
def single_topic(id) def single_topic(id)
topic = Topic.find_by(id: id) post = Post.find_by(topic_id: id, post_number: 1)
return nil unless @guardian.can_see?(topic) return nil unless @guardian.can_see?(post)
@results.add_result(SearchResult.from_topic(topic)) @results.add(post)
@results @results
end end
@ -189,8 +208,8 @@ class Search
.secured(@guardian) .secured(@guardian)
.limit(@limit) .limit(@limit)
categories.each do |c| categories.each do |category|
@results.add_result(SearchResult.from_category(c)) @results.add(category)
end end
end end
@ -202,18 +221,18 @@ class Search
.limit(@limit) .limit(@limit)
.references(:user_search_data) .references(:user_search_data)
users.each do |u| users.each do |user|
@results.add_result(SearchResult.from_user(u)) @results.add(user)
end end
end end
def posts_query(limit, opts=nil) def posts_query(limit, opts=nil)
opts ||= {} opts ||= {}
posts = Post.includes(:post_search_data, {:topic => :category}) posts = Post
.joins(:post_search_data, {:topic => :category})
.where("topics.deleted_at" => nil) .where("topics.deleted_at" => nil)
.where("topics.visible") .where("topics.visible")
.where("topics.archetype <> ?", Archetype.private_message) .where("topics.archetype <> ?", Archetype.private_message)
.references(:post_search_data, {:topic => :category})
if @search_context.present? && @search_context.is_a?(Topic) if @search_context.present? && @search_context.is_a?(Topic)
posts = posts.where("posts.raw ilike ?", "%#{@term}%") posts = posts.where("posts.raw ilike ?", "%#{@term}%")
@ -279,45 +298,31 @@ class Search
end end
def aggregate_search def aggregate_search
cols = ['topics.id', 'topics.title', 'topics.slug', 'cooked']
topics = posts_query(@limit, aggregate_search: true) post_sql = posts_query(@limit, aggregate_search: true)
.group(*cols) .select('topics.id', 'min(post_number) post_number, row_number() OVER() row_number')
.pluck('min(posts.post_number)',*cols) .group('topics.id')
.to_sql
posts = Post.includes(:topic => :category)
.joins("JOIN (#{post_sql}) x ON x.id = posts.topic_id AND x.post_number = posts.post_number")
.order('row_number')
topics.each do |t| posts.each do |post|
post_number, topic_id, title, slug, cooked = t @results.add(post)
cooked = SearchObserver::HtmlScrubber.scrub(cooked).squish
blurb = SearchResult.blurb(cooked, @term)
@results.add_result(SearchResult.new(type: :topic,
topic_id: topic_id,
id: topic_id,
title: title,
url: "/t/#{slug}/#{topic_id}/#{post_number}",
blurb: blurb))
end end
end end
def topic_search def topic_search
if @search_context.is_a?(Topic)
posts = if @search_context.is_a?(User) posts = posts_query(@limit).where('posts.topic_id = ?', @search_context.id).includes(:topic => :category)
# If we have a user filter, search all posts by default with a higher limit posts.each do |post|
posts_query(@limit * Search.burst_factor) @results.add(post)
elsif @search_context.is_a?(Topic) end
posts_query(@limit).where('posts.post_number = 1 OR posts.topic_id = ?', @search_context.id) else
elsif @include_blurbs aggregate_search
posts_query(@limit).where('posts.post_number = 1')
end
# If no context, do an aggregate search
return aggregate_search if posts.nil?
posts.each do |p|
@results.add_result(SearchResult.from_post(p, @search_context, @term, @include_blurbs))
end end
end end
end end

View File

@ -1,36 +1,56 @@
require 'sanitize'
class Search class Search
class GroupedSearchResults class GroupedSearchResults
attr_reader :topic_count, :type_filter
def initialize(type_filter) include ActiveModel::Serialization
@type_filter = type_filter
@by_type = {}
@topic_count = 0
end
def topic_ids class TextHelper
topic_results = @by_type[:topic] extend ActionView::Helpers::TextHelper
return Set.new if topic_results.blank? def self.sanitize(text)
return topic_results.results.map{|r| r.topic_id} # we run through sanitize at the end so it does not matter
end text
def as_json(options = nil)
@by_type.values.map do |grouped_result|
grouped_result.as_json
end end
end end
def add_result(result) attr_reader :type_filter,
grouped_result = @by_type[result.type] || (@by_type[result.type] = SearchResultType.new(result.type)) :posts, :categories, :users,
:more_posts, :more_categories, :more_users,
:term, :search_context, :include_blurbs
# Limit our results if there is no filter def initialize(type_filter, term, search_context, include_blurbs)
if @type_filter.present? or (grouped_result.size < Search.per_facet) @type_filter = type_filter
@topic_count += 1 if (result.type == :topic) @term = term
@search_context = search_context
@include_blurbs = include_blurbs
@posts = []
@categories = []
@users = []
end
grouped_result.add(result) def blurb(post)
cooked = SearchObserver::HtmlScrubber.scrub(post.cooked).squish
terms = @term.split(/\s+/)
blurb = TextHelper.excerpt(cooked, terms.first, radius: 100)
# TODO highlight term
# terms.each do |term|
# blurb = TextHelper.highlight(blurb, term)
# end
blurb = TextHelper.truncate(cooked, length: 200) if blurb.blank?
Sanitize.clean(blurb)
end
def add(object)
type = object.class.to_s.downcase.pluralize
if !@type_filter.present? && send(type).length == Search.per_facet
instance_variable_set("@more_#{type}".to_sym, true)
else else
grouped_result.more = true (send type) << object
end end
end end

View File

@ -7,7 +7,7 @@ class Search
extend ActionView::Helpers::TextHelper extend ActionView::Helpers::TextHelper
end end
attr_accessor :type, :id, :topic_id, :blurb attr_accessor :type, :id, :topic_id, :blurb, :locked, :pinned
# Category attributes # Category attributes
attr_accessor :color, :text_color attr_accessor :color, :text_color
@ -27,7 +27,9 @@ class Search
:uploaded_avatar_id, :uploaded_avatar_id,
:color, :color,
:text_color, :text_color,
:blurb :blurb,
:locked,
:pinned
].each do |k| ].each do |k|
val = send(k) val = send(k)
json[k] = val if val json[k] = val if val
@ -73,7 +75,8 @@ class Search
end end
if include_blurbs if include_blurbs
#add a blurb from the post to the search results #add a blurb from the post to the search results
custom_blurb = blurb(p.raw, term) cooked = SearchObserver::HtmlScrubber.scrub(p.cooked).squish
custom_blurb = blurb(cooked, term)
end end
if p.post_number == 1 if p.post_number == 1
# we want the topic link when it's the OP # we want the topic link when it's the OP

View File

@ -13,20 +13,6 @@ describe Search do
ActiveRecord::Base.observers.enable :search_observer ActiveRecord::Base.observers.enable :search_observer
end end
def first_of_type(results, type)
return nil if results.blank?
results.each do |r|
return r[:results].first if r[:type] == type
end
nil
end
def result_ids_for_type(results, type)
results.find do |group|
group[:type] == type
end[:results].map {|r| r[:id]}
end
context 'post indexing observer' do context 'post indexing observer' do
before do before do
@category = Fabricate(:category, name: 'america') @category = Fabricate(:category, name: 'america')
@ -56,11 +42,8 @@ describe Search do
@indexed = @user.user_search_data.search_data @indexed = @user.user_search_data.search_data
end end
it "should pick up on username" do it "should pick up on data" do
@indexed.should =~ /fred/ @indexed.should =~ /fred/
end
it "should pick up on name" do
@indexed.should =~ /jone/ @indexed.should =~ /jone/
end end
end end
@ -77,43 +60,36 @@ describe Search do
end end
it 'returns something blank on a nil search' do
ActiveRecord::Base.expects(:exec_sql).never
Search.new(nil).execute.should be_blank
end
it 'does not search when the search term is too small' do it 'does not search when the search term is too small' do
ActiveRecord::Base.expects(:exec_sql).never ActiveRecord::Base.expects(:exec_sql).never
Search.new('evil', min_search_term_length: 5).execute.should be_blank Search.execute('evil', min_search_term_length: 5)
end end
it 'escapes non alphanumeric characters' do it 'escapes non alphanumeric characters' do
Search.new('foo :!$);}]>@\#\"\'').execute.should be_blank # There are at least three levels of sanitation for Search.query! Search.execute('foo :!$);}]>@\#\"\'').posts.length.should == 0 # There are at least three levels of sanitation for Search.query!
end end
it "doesn't raise an error when single quotes are present" do it "doesn't raise an error when single quotes are present" do
Search.new("'hello' world").execute.should be_blank # There are at least three levels of sanitation for Search.query! Search.execute("'hello' world").posts.length.should == 0 # There are at least three levels of sanitation for Search.query!
end end
it 'works when given two terms with spaces' do it 'works when given two terms with spaces' do
lambda { Search.new('evil trout').execute }.should_not raise_error lambda { Search.execute('evil trout') }.should_not raise_error
end end
context 'users' do context 'users' do
let!(:user) { Fabricate(:user) } let!(:user) { Fabricate(:user) }
let(:result) { first_of_type( Search.new('bruce', type_filter: 'user').execute, 'user') } let(:result) { Search.execute('bruce', type_filter: 'user') }
it 'returns a result' do it 'returns a result' do
result.should be_present result.users.length.should == 1
result[:title].should == user.username result.users[0].id.should == user.id
result[:avatar_template].should_not be_nil
result[:url].should == "/users/#{user.username_lower}"
end end
end end
context 'topics' do context 'topics' do
let(:topic) { Fabricate(:topic) } let(:post) { Fabricate(:post) }
let(:topic) { post.topic}
context 'search within topic' do context 'search within topic' do
@ -139,76 +115,62 @@ describe Search do
# update posts_count # update posts_count
topic.reload topic.reload
results = Search.new('posting', search_context: post1.topic).execute.find do |r| results = Search.execute('posting', search_context: post1.topic)
r[:type] == "topic" results.posts.map(&:id).should == [post1.id, post2.id, post3.id, post4.id]
end[:results]
results.map{|r| r[:id]}.should == [
post1.topic_id,
"_#{post2.id}",
"_#{post3.id}",
"_#{post4.id}"]
# stop words should work # stop words should work
results = Search.new('this', search_context: post1.topic).execute.find do |r| results = Search.execute('this', search_context: post1.topic)
r[:type] == "topic" results.posts.length.should == 4
end[:results]
results.length.should == 4
end end
end end
context 'searching the OP' do context 'searching the OP' do
let!(:post) { Fabricate(:post_with_long_raw_content, topic: topic, user: topic.user) } let!(:post) { Fabricate(:post_with_long_raw_content) }
let(:result) { first_of_type(Search.new('hundred', type_filter: 'topic', include_blurbs: true).execute, 'topic') } let(:result) { Search.execute('hundred', type_filter: 'topic', include_blurbs: true) }
it 'returns a result correctly' do it 'returns a result correctly' do
result.should be_present result.posts.length.should == 1
result[:title].should == topic.title result.posts[0].id.should == post.id
result[:url].should == topic.relative_url
result[:blurb].should == TextHelper.excerpt(post.raw, 'hundred', radius: 100)
end end
end end
context 'searching for a post' do context 'searching for a post' do
let!(:post) { Fabricate(:post, topic: topic, user: topic.user) }
let!(:reply) { Fabricate(:basic_reply, topic: topic, user: topic.user) } let!(:reply) { Fabricate(:basic_reply, topic: topic, user: topic.user) }
let(:result) { first_of_type(Search.new('quote', type_filter: 'topic').execute, 'topic') } let(:result) { Search.execute('quotes', type_filter: 'topic', include_blurbs: true) }
it 'returns the post' do it 'returns the post' do
result.should be_present result.should be_present
result[:title].should == topic.title result.posts.length.should == 1
result[:url].should == topic.relative_url + "/2" p = result.posts[0]
result[:blurb].should == "this reply has no quotes" p.topic.id.should == topic.id
p.id.should == reply.id
result.blurb(p).should == "this reply has no quotes"
end end
end end
context "search for a topic by id" do context "search for a topic by id" do
let(:result) { first_of_type(Search.new(topic.id, type_filter: 'topic', search_for_id: true, min_search_term_length: 1).execute, 'topic') } let(:result) { Search.execute(topic.id, type_filter: 'topic', search_for_id: true, min_search_term_length: 1) }
it 'returns the topic' do it 'returns the topic' do
result.should be_present result.posts.length.should == 1
result[:title].should == topic.title result.posts.first.id.should == post.id
result[:url].should == topic.relative_url
end end
end end
context "search for a topic by url" do context "search for a topic by url" do
let(:result) { first_of_type(Search.new(topic.relative_url, search_for_id: true, type_filter: 'topic').execute, 'topic') } let(:result) { Search.execute(topic.relative_url, search_for_id: true, type_filter: 'topic')}
it 'returns the topic' do it 'returns the topic' do
result.should be_present result.posts.length.should == 1
result[:title].should == topic.title result.posts.first.id.should == post.id
result[:url].should == topic.relative_url
end end
end end
context 'security' do context 'security' do
let!(:post) { Fabricate(:post, topic: topic, user: topic.user) }
def result(current_user) def result(current_user)
first_of_type(Search.new('hello', guardian: current_user).execute, 'topic') Search.execute('hello', guardian: current_user)
end end
it 'secures results correctly' do it 'secures results correctly' do
@ -220,9 +182,9 @@ describe Search do
category.set_permissions(:staff => :full) category.set_permissions(:staff => :full)
category.save category.save
result(nil).should_not be_present result(nil).posts.should_not be_present
result(Fabricate(:user)).should_not be_present result(Fabricate(:user)).posts.should_not be_present
result(Fabricate(:admin)).should be_present result(Fabricate(:admin)).posts.should be_present
end end
end end
@ -236,30 +198,27 @@ describe Search do
end end
} }
let!(:post) {Fabricate(:post, topic: cyrillic_topic, user: cyrillic_topic.user)} let!(:post) {Fabricate(:post, topic: cyrillic_topic, user: cyrillic_topic.user)}
let(:result) { first_of_type(Search.new('запись').execute, 'topic') } let(:result) { Search.execute('запись') }
it 'finds something when given cyrillic query' do it 'finds something when given cyrillic query' do
result.should be_present result.posts.should be_present
end end
end end
context 'categories' do context 'categories' do
let!(:category) { Fabricate(:category) } let!(:category) { Fabricate(:category) }
def result def search
first_of_type(Search.new('amazing').execute, 'category') Search.execute(category.name)
end end
it 'returns the correct result' do it 'returns the correct result' do
r = result search.categories.should be_present
r.should be_present
r[:title].should == category.name
r[:url].should == "/category/#{category.slug}"
category.set_permissions({}) category.set_permissions({})
category.save category.save
result.should_not be_present search.categories.should_not be_present
end end
end end
@ -272,21 +231,23 @@ describe Search do
context 'user filter' do context 'user filter' do
let(:results) { Search.new('amazing', type_filter: 'user').execute } let(:results) { Search.execute('amazing', type_filter: 'user') }
it "returns a user result" do it "returns a user result" do
results.detect {|r| r[:type] == 'user'}.should be_present results.categories.length.should == 0
results.detect {|r| r[:type] == 'category'}.should be_blank results.posts.length.should == 0
results.users.length.should == 1
end end
end end
context 'category filter' do context 'category filter' do
let(:results) { Search.new('amazing', type_filter: 'category').execute } let(:results) { Search.execute('amazing', type_filter: 'category') }
it "returns a category result" do it "returns a category result" do
results.detect {|r| r[:type] == 'user'}.should be_blank results.categories.length.should == 1
results.detect {|r| r[:type] == 'category'}.should be_present results.posts.length.should == 0
results.users.length.should == 0
end end
end end
@ -295,27 +256,30 @@ describe Search do
context 'search_context' do context 'search_context' do
context 'user as a search context' do it 'can find a user when using search context' do
let(:coding_horror) { Fabricate(:coding_horror) }
Given!(:post) { Fabricate(:post) } coding_horror = Fabricate(:coding_horror)
Given!(:coding_horror_post) { Fabricate(:post, user: coding_horror )} post = Fabricate(:post)
When(:search_user) { Search.new('hello', search_context: post.user).execute }
# should find topic created by searched user first Fabricate(:post, user: coding_horror)
Then { first_of_type(search_user, 'topic')[:id].should == post.topic_id }
result = Search.execute('hello', search_context: post.user)
result.posts.first.topic_id = post.topic_id
result.posts.length.should == 1
end end
context 'category as a search context' do it 'can use category as a search context' do
let(:category) { Fabricate(:category) } category = Fabricate(:category)
let(:topic) { Fabricate(:topic, category: category) } topic = Fabricate(:topic, category: category)
let(:topic_no_cat) { Fabricate(:topic) } topic_no_cat = Fabricate(:topic)
Given!(:post) { Fabricate(:post, topic: topic, user: topic.user ) } post = Fabricate(:post, topic: topic, user: topic.user )
Given!(:another_post) { Fabricate(:post, topic: topic_no_cat, user: topic.user ) } _another_post = Fabricate(:post, topic: topic_no_cat, user: topic.user )
When(:search_cat) { Search.new('hello', search_context: category).execute }
# should find topic in searched category first search = Search.execute('hello', search_context: category)
Then { first_of_type(search_cat, 'topic')[:id].should == topic.id } search.posts.length.should == 1
search.posts.first.id.should == post.id
end end
end end
@ -330,10 +294,10 @@ describe Search do
it 'finds chinese topic based on title' do it 'finds chinese topic based on title' do
SiteSetting.default_locale = 'zh_TW' SiteSetting.default_locale = 'zh_TW'
topic = Fabricate(:topic, title: 'My Title Discourse社区指南') topic = Fabricate(:topic, title: 'My Title Discourse社区指南')
Fabricate(:post, topic: topic) post = Fabricate(:post, topic: topic)
Search.new('社区指南').execute[0][:results][0][:id].should == topic.id Search.execute('社区指南').posts.first.id.should == post.id
Search.new('指南').execute[0][:results][0][:id].should == topic.id Search.execute('指南').posts.first.id.should == post.id
end end
end end

View File

@ -2,6 +2,25 @@ require 'spec_helper'
describe SearchController do describe SearchController do
context "integration" do
before do
ActiveRecord::Base.observers.enable :search_observer
end
it "can search correctly" do
my_post = Fabricate(:post, raw: 'this is my really awesome post')
xhr :get, :query, term: 'awesome', include_blurb: true
response.should be_success
data = JSON.parse(response.body)
data['posts'][0]['id'].should == my_post.id
data['posts'][0]['blurb'].should == 'this is my really awesome post'
data['topics'][0]['id'].should == my_post.topic_id
end
end
let(:search_context) { {type: 'user', id: 'eviltrout'} } let(:search_context) { {type: 'user', id: 'eviltrout'} }
context "basics" do context "basics" do