Skip to content

Commit

Permalink
Merge pull request #37 from sad16/16-full-text-searching
Browse files Browse the repository at this point in the history
full text searching
  • Loading branch information
sad16 committed Oct 1, 2021
2 parents 1c8cd90 + bb56ec3 commit bc6f97e
Show file tree
Hide file tree
Showing 35 changed files with 1,022 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Expand Up @@ -28,6 +28,9 @@

/config/database.yml

/config/development.sphinx.conf
/db/sphinx

.idea

homework
Expand Down
3 changes: 3 additions & 0 deletions Gemfile
Expand Up @@ -54,6 +54,8 @@ gem 'oj'
gem 'sidekiq'
gem 'whenever', require: false
gem 'redis'
gem 'mysql2'
gem 'thinking-sphinx'

group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
Expand Down Expand Up @@ -85,6 +87,7 @@ group :test do
gem 'rails-controller-testing'
gem 'launchy'
gem 'with_model'
gem 'database_cleaner-active_record'
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
Expand Down
20 changes: 20 additions & 0 deletions Gemfile.lock
Expand Up @@ -101,6 +101,10 @@ GEM
concurrent-ruby (1.1.5)
connection_pool (2.2.5)
crass (1.0.4)
database_cleaner-active_record (2.0.1)
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
devise (4.7.3)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
Expand Down Expand Up @@ -145,10 +149,13 @@ GEM
hashie (4.1.0)
i18n (1.6.0)
concurrent-ruby (~> 1.0)
innertube (1.1.0)
jbuilder (2.8.0)
activesupport (>= 4.2.0)
multi_json (>= 1.2)
jmespath (1.4.0)
joiner (0.4.2)
activerecord (>= 5.2.beta1)
jquery-rails (4.4.0)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
Expand Down Expand Up @@ -178,6 +185,7 @@ GEM
marcel (0.3.3)
mimemagic (~> 0.3.2)
method_source (0.9.2)
middleware (0.1.0)
mimemagic (0.3.3)
mini_mime (1.0.1)
mini_portile2 (2.4.0)
Expand All @@ -186,6 +194,7 @@ GEM
multi_json (1.13.1)
multi_xml (0.6.0)
multipart-post (2.1.1)
mysql2 (0.5.3)
nio4r (2.3.1)
nokogiri (1.10.1)
mini_portile2 (~> 2.4.0)
Expand Down Expand Up @@ -260,6 +269,7 @@ GEM
responders (3.0.1)
actionpack (>= 5.0)
railties (>= 5.0)
riddle (2.4.2)
rspec-core (3.8.0)
rspec-support (~> 3.8.0)
rspec-expectations (3.8.2)
Expand Down Expand Up @@ -320,6 +330,13 @@ GEM
activesupport (>= 4.0)
sprockets (>= 3.0.0)
temple (0.8.1)
thinking-sphinx (5.3.0)
activerecord (>= 4.2.0)
builder (>= 2.1.2)
innertube (>= 1.0.2)
joiner (>= 0.3.4)
middleware (>= 0.1.0)
riddle (~> 2.3)
thor (0.20.3)
thread_safe (0.3.6)
tilt (2.0.9)
Expand Down Expand Up @@ -371,6 +388,7 @@ DEPENDENCIES
capybara-email
cocoon
coffee-rails (~> 4.2)
database_cleaner-active_record
devise
doorkeeper
factory_bot_rails
Expand All @@ -380,6 +398,7 @@ DEPENDENCIES
launchy
letter_opener
listen (>= 3.0.5, < 3.2)
mysql2
oj
omniauth (~> 1.9.1)
omniauth-github
Expand All @@ -399,6 +418,7 @@ DEPENDENCIES
slim-rails
spring
spring-watcher-listen (~> 2.0.0)
thinking-sphinx
turbolinks (~> 5)
twitter-bootstrap-rails
tzinfo-data
Expand Down
5 changes: 5 additions & 0 deletions app/assets/javascripts/helpers.js
@@ -0,0 +1,5 @@
function clearForm(form) {
$(form).find('input').val('');
}


5 changes: 5 additions & 0 deletions app/assets/javascripts/search/errors.js
@@ -0,0 +1,5 @@
function errorSearch(response) {
var errors = response.detail[0].errors;
var errorsBlock = $(response.currentTarget).find('.errors');
errorsBlock.html(errors);
}
53 changes: 53 additions & 0 deletions app/assets/javascripts/search/global.js
@@ -0,0 +1,53 @@
$(document).on('turbolinks:load', function() {
onAjaxSuccessGlobalSearch($('#global-search-form'));
});

function onAjaxSuccessGlobalSearch(form) {
$(form)
.on('ajax:success', function(response) {
successGlobalSearch(response);
})
.on('ajax:error', function(response) {
errorSearch(response);
})
}

function successGlobalSearch(response) {
var resultBlock = $('.result');
resultBlock.html('');

var data = response.detail[0];

if (data.length !== 0) {
$.each(data, function(index, wrapper) {
var item = wrapper.wrapper;
var type = Object.keys(item)[0];
var data = Object.values(item)[0];

resultBlock.append(getTemplate(type, data));
});
} else {
resultBlock.html('No result')
}

clearForm(response.currentTarget);
}

function getTemplate(type, data) {
switch (type) {
case 'question':
var template = questionTemplate(data)
break;
case 'answer':
var template = answerTemplate(data)
break;
case 'comment':
var template = commentTemplate(data)
break;
case 'user':
var template = userTemplate(data)
break;
}

return template;
}
33 changes: 33 additions & 0 deletions app/assets/javascripts/search/init.js
@@ -0,0 +1,33 @@
$(document).on('turbolinks:load', function() {
onAjaxSuccessSearch($('#questions-search-form'), questionTemplate);
onAjaxSuccessSearch($('#answers-search-form'), answerTemplate);
onAjaxSuccessSearch($('#comments-search-form'), commentTemplate);
onAjaxSuccessSearch($('#users-search-form'), userTemplate);
});

function onAjaxSuccessSearch(form, template) {
$(form)
.on('ajax:success', function(response) {
successSearch(response, template);
})
.on('ajax:error', function(response) {
errorSearch(response);
})
}

function successSearch(response, template) {
var resultBlock = $('.result');
resultBlock.html('');

var data = response.detail[0].data;

if (data.length !== 0) {
$.each(data, function(index, item) {
resultBlock.append(template(item));
});
} else {
resultBlock.html('No result')
}

clearForm(response.currentTarget);
}
15 changes: 15 additions & 0 deletions app/assets/javascripts/search/templates.js
@@ -0,0 +1,15 @@
function questionTemplate(question) {
return `<div className="item"><p className="link"><a href="/questions/${question.id}">Link</a></p><p className="title">${question.title}</p><p className="body">${question.body}</p><br></div>`
}

function answerTemplate(answer) {
return `<div className="item"><p className="link"><a href="/questions/${answer.question_id}">Question Link</a></p><p className="body">${answer.body}</p><br></div>`
}

function commentTemplate(comment) {
return `<div className="item"><p className="text">${comment.text}</p><br></div>`
}

function userTemplate(user) {
return `<div className="item"><p className="email">${user.email}</p><br></div>`
}
54 changes: 54 additions & 0 deletions app/controllers/search_controller.rb
@@ -0,0 +1,54 @@
class SearchController < ApplicationController
before_action :authenticate_user!

rescue_from Services::Search::Base::EmptySearchError, with: :empty_search_error

def global
respond_to do |format|
format.html
format.json { render json: Services::Search::Global.new.call(search_params).to_a, each_serializer: Search::GlobalSerializer, adapter: :attributes }
end
end

def questions
respond_to do |format|
format.html
format.json { render_json(Services::Search::Questions.new.call(search_params)) }
end
end

def answers
respond_to do |format|
format.html
format.json { render_json(Services::Search::Answers.new.call(search_params)) }
end
end

def comments
respond_to do |format|
format.html
format.json { render_json(Services::Search::Comments.new.call(search_params)) }
end
end

def users
respond_to do |format|
format.html
format.json { render_json(Services::Search::Users.new.call(search_params)) }
end
end

private

def search_params
params.require(:search).permit!
end

def empty_search_error
render json: { errors: ['Error: search without params'] }, status: :unprocessable_entity
end

def render_json(data)
render json: { data: data }
end
end
8 changes: 8 additions & 0 deletions app/indices/answers_index.rb
@@ -0,0 +1,8 @@
ThinkingSphinx::Index.define :answer, with: :active_record do
# fileds
indexes body
indexes user.email, as: :author, sortable: true

# attributes
has question_id, user_id, created_at, updated_at
end
8 changes: 8 additions & 0 deletions app/indices/comments_index.rb
@@ -0,0 +1,8 @@
ThinkingSphinx::Index.define :comment, with: :active_record do
# fileds
indexes text
indexes user.email, as: :author, sortable: true

# attributes
has commentable_id, commentable_type, user_id, created_at, updated_at
end
9 changes: 9 additions & 0 deletions app/indices/questions_index.rb
@@ -0,0 +1,9 @@
ThinkingSphinx::Index.define :question, with: :active_record do
# fileds
indexes title, sortable: true
indexes body
indexes user.email, as: :author, sortable: true

# attributes
has user_id, created_at, updated_at
end
7 changes: 7 additions & 0 deletions app/indices/user_index.rb
@@ -0,0 +1,7 @@
ThinkingSphinx::Index.define :user, with: :active_record do
# fileds
indexes email

# attributes
has created_at, updated_at
end
11 changes: 11 additions & 0 deletions app/serializers/search/global_serializer.rb
@@ -0,0 +1,11 @@
module Search
class GlobalSerializer < ApplicationSerializer
attributes :wrapper

def wrapper
{
object.class.name.downcase => object
}
end
end
end
20 changes: 20 additions & 0 deletions app/services/search/answers.rb
@@ -0,0 +1,20 @@
module Services
module Search
class Answers < Base

private

def search_klass
Answer
end

def search_conditions(params)
{
body: params[:body].presence,
author: params[:author].presence,
}
end

end
end
end
39 changes: 39 additions & 0 deletions app/services/search/base.rb
@@ -0,0 +1,39 @@
module Services
module Search
class Base < ApplicationService
class Error < StandardError; end
class EmptySearchError < Error; end

def call(params)
search(params.to_h.deep_symbolize_keys)
end

private

def search(params)
search_options = search_options(params)
validate_search_options(search_options)
search_klass.search(*search_options)
end

def search_options(params)
[
params[:global].presence,
{ conditions: search_conditions(params).compact }
]
end

def validate_search_options(search_options)
raise EmptySearchError if search_options[0].nil? && search_options[1][:conditions].blank?
end

def search_klass
raise NotImplementedError
end

def search_conditions(params)
{}
end
end
end
end

0 comments on commit bc6f97e

Please sign in to comment.