diff --git a/Gemfile b/Gemfile index 63e2ae23c..b66cc87d1 100644 --- a/Gemfile +++ b/Gemfile @@ -118,3 +118,5 @@ gem "openssl", "~> 3.2" gem "mail", "~> 2.7.1" gem "prometheus-client", "~> 4.2" + +gem "meilisearch-rails", "~> 0.10.1" diff --git a/Gemfile.lock b/Gemfile.lock index c9dfd28f0..ea379f909 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -249,6 +249,10 @@ GEM mail (2.7.1) mini_mime (>= 0.1.1) marcel (1.0.2) + meilisearch (0.25.1) + httparty (>= 0.17.1, < 0.22.0) + meilisearch-rails (0.10.1) + meilisearch (~> 0.25.0) method_source (1.0.0) mime-types (3.5.1) mime-types-data (~> 3.2015) @@ -535,6 +539,7 @@ DEPENDENCIES letter_opener lograge mail (~> 2.7.1) + meilisearch-rails (~> 0.10.1) mini_magick net-imap net-pop diff --git a/Procfile.dev b/Procfile.dev index bd494ae84..201c9d43a 100644 --- a/Procfile.dev +++ b/Procfile.dev @@ -2,3 +2,4 @@ web: unset PORT && bin/rails server worker: bundle exec sidekiq css: yarn build:css --watch js: yarn build --watch +search: meilisearch --no-analytics --env development --db-path tmp/meilisearch/db --dump-dir tmp/meilisearch/dump --master-key justfordev42069e621 diff --git a/app/assets/stylesheets/search.scss b/app/assets/stylesheets/search.scss new file mode 100644 index 000000000..1b26d4a36 --- /dev/null +++ b/app/assets/stylesheets/search.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the search controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: https://sass-lang.com/ diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb new file mode 100644 index 000000000..a2cdf81c7 --- /dev/null +++ b/app/controllers/search_controller.rb @@ -0,0 +1,35 @@ +class SearchController < ApplicationController + def index + @results = [] + @query = params[:q] + return if @query.blank? + + @results = if params[:multi_search] == "1" + multi_search_experiment + else + [*Answer.search(@query), *Question.search(@query)] + end + end + + private + + def multi_search_experiment + MeiliSearch::Rails.client.multi_search( + [Answer, Question].map do |klass| + { + q: @query, + index_uid: klass.name.to_s, + show_ranking_score: true, + } + end + )["results"].flat_map do |h| + model = h["indexUid"].constantize # bad practice! + results = model.find(h["hits"].pluck("id")).map { |r| [r.id.to_s, r] }.to_h + h["hits"].map { |hit| [hit["_rankingScore"], results[hit["id"]]] } + end + .sort_by(&:first) + .reverse + .tap { |results| Rails.logger.debug(results) } + .map(&:last) + end +end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb new file mode 100644 index 000000000..b3ce20acb --- /dev/null +++ b/app/helpers/search_helper.rb @@ -0,0 +1,2 @@ +module SearchHelper +end diff --git a/app/models/answer.rb b/app/models/answer.rb index 71417252e..e0be24751 100644 --- a/app/models/answer.rb +++ b/app/models/answer.rb @@ -1,6 +1,12 @@ class Answer < ApplicationRecord extend Answer::TimelineMethods + include MeiliSearch::Rails + + meilisearch do + attribute :content + end + belongs_to :user, counter_cache: :answered_count belongs_to :question, counter_cache: :answer_count has_many :comments, dependent: :destroy diff --git a/app/models/question.rb b/app/models/question.rb index 0a6adec04..505ec13e9 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -1,6 +1,12 @@ class Question < ApplicationRecord include Question::AnswerMethods + include MeiliSearch::Rails + + meilisearch do + attribute :content + end + belongs_to :user, optional: true has_many :answers, dependent: :destroy has_many :inboxes, dependent: :destroy diff --git a/app/views/search/index.haml b/app/views/search/index.haml new file mode 100644 index 000000000..a1e162f1e --- /dev/null +++ b/app/views/search/index.haml @@ -0,0 +1,24 @@ +.container-lg.container--main + .row + .col-sm-10.col-md-10.col-lg-9.mx-auto + .card + .card-body + = bootstrap_form_with url: search_path, layout: :inline, method: :get do |f| + = f.text_field :q, skip_label: true, append: f.primary("Search"), value: params[:q] + = f.check_box :multi_search, label: "Multisearch" + - unless @results.blank? + .container-lg.container--main + .row + .col-sm-10.col-md-10.col-lg-9.mx-auto + - @results.each do |result| + - case result + - when Answer + = render "answerbox", a: result, display_all: false, subscribed_answer_ids: [] + - when Question + = render "shared/question", q: result, type: nil + += render 'shared/links' + +:ruby + provide(:title, generate_title('Search')) + parent_layout 'base' diff --git a/config/initializers/meilisearch.rb b/config/initializers/meilisearch.rb new file mode 100644 index 000000000..ac167bb17 --- /dev/null +++ b/config/initializers/meilisearch.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +return unless ENV["SEARCH_ENABLED"] == "true" + +MeiliSearch::Rails.configuration = { + meilisearch_url: ENV.fetch("MEILISEARCH_HOST", "http://localhost:7700"), + meilisearch_api_key: ENV.fetch("MEILISEARCH_API_KEY", "justfordev42069e621") +} diff --git a/config/routes.rb b/config/routes.rb index e5fcf8999..e3c4f3a4f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -148,6 +148,8 @@ post "/inbox/create", to: "inbox#create", as: :inbox_create get "/inbox", to: "inbox#show", as: :inbox + get "/search", to: "search#index" + get "/user/:username", to: "user#show" get "/@:username", to: "user#show", as: :user get "/@:username/a/:id", to: "answer#show", as: :answer diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb new file mode 100644 index 000000000..74b6daf02 --- /dev/null +++ b/spec/helpers/search_helper_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +# Specs in this file have access to a helper object that includes +# the SearchHelper. For example: +# +# describe SearchHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +RSpec.describe SearchHelper, type: :helper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/requests/search_spec.rb b/spec/requests/search_spec.rb new file mode 100644 index 000000000..5e3640acc --- /dev/null +++ b/spec/requests/search_spec.rb @@ -0,0 +1,11 @@ +require 'rails_helper' + +RSpec.describe "Searches", type: :request do + describe "GET /index" do + it "returns http success" do + get "/search/index" + expect(response).to have_http_status(:success) + end + end + +end diff --git a/spec/views/search/index.html.erb_spec.rb b/spec/views/search/index.html.erb_spec.rb new file mode 100644 index 000000000..bdeb4dc21 --- /dev/null +++ b/spec/views/search/index.html.erb_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe "search/index.html.erb", type: :view do + pending "add some examples to (or delete) #{__FILE__}" +end