見出し画像

RailsとMySQLを用いた全文検索機能付きQ&Aページの作成方法

※※※注意※※※
この記事はChatGPTにベースを書いてもらっています。

はじめに

この記事では、RailsとMySQLを使用して全文検索機能を備えたQ&Aページの作成方法について解説します。RailsとMySQLの基本的な構築手順については、私の以前の記事(RailsとMySQLの構築手順)を参考にしてください。


1. 環境構築

まず、RailsとMySQLの環境を整えます。上述の記事を参照し、RailsのセットアップとMySQLとの接続を確認してください。

***

2. Q&Aモデルの作成

Q&Aシステムの核となるモデル(例えば、QuestionとAnswer)を作成します。rails generate modelコマンドを使用して、必要な属性を持つモデルを作成します。

Questionモデルの作成

rails generate model Question title:string content:text
rails db:migrate

Questionモデルにインデックスを貼る

以下のコマンドで、title カラムのみに全文検索インデックスを追加するマイグレーションファイルを生成します。

rails generate migration AddFulltextIndexToQuestionTitle

このコマンドを実行すると、db/migrate ディレクトリに新しいマイグレーションファイルが作成されます。ファイル名は [timestamp]_add_fulltext_index_to_question_title.rb のようになります。

次に、生成されたマイグレーションファイルを編集して、title カラムに全文検索インデックスを追加します。以下はその編集後のマイグレーションの例です:

class AddFulltextIndexToQuestionTitle < ActiveRecord::Migration[6.0]
  def change
    add_index :questions, :title, type: :fulltext
  end
end

最後に、マイグレーションを実行してインデックスをデータベースに適用します。

rails db:migrate

これで、questions テーブルの title カラムに全文検索用のインデックスが設定されます。

Answerモデルの作成

rails generate model Answer content:text question:references
rails db:migrate

[補足]
以前の記事を参考にした方は、上記コマンドを実行し、dockerのコンテナに入ってから上記コマンドを実行してください。

docker-compose exec web /bin/bash

コンテナから抜けるコマンドは下記です。

exit

***

3. ルーティングとコントローラの設定

Q&Aページにアクセスするためのルーティングを設定し、対応するコントローラーを作成します。routes.rbにリソースを追加して、コントローラーでアクションを定義します。

ルーティングの追加

config/routes.rbにQ&A機能に必要なルーティングを追加します。questionsリソースにanswersリソースをネストさせることで、質問に対する回答をグループ化します。

# config/routes.rb
Rails.application.routes.draw do
  resources :questions do
    resources :answers, only: [:create, :destroy]
  end

  # 全文検索用のルートも追加
  get 'search', to: 'questions#search'
end

コントローラーの設定

QuestionsControllerAnswersControllerを生成し、適切なアクションを追加します。全文検索を行うsearchアクションもQuestionsControllerに定義します。

# app/controllers/questions_controller.rb
class QuestionsController < ApplicationController
  before_action :set_question, only: [:show, :edit, :update, :destroy]

  # GET /questions
  def index
    @questions = Question.all
  end

  # GET /questions/1
  def show
  end

  # GET /questions/new
  def new
    @question = Question.new
  end

  # GET /questions/1/edit
  def edit
  end

  # POST /questions
  def create
    @question = Question.new(question_params)

    if @question.save
      redirect_to @question, notice: '質問が正常に作成されました。'
    else
      render :new
    end
  end

  # PATCH/PUT /questions/1
  def update
    if @question.update(question_params)
      redirect_to @question, notice: '質問が正常に更新されました。'
    else
      render :edit
    end
  end

  # DELETE /questions/1
  def destroy
    @question.destroy
    redirect_to questions_url, notice: '質問が正常に削除されました。'
  end

  # GET /search
  def search
    @questions = Question.search(params[:search])
    render :index
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_question
      @question = Question.find(params[:id])
    end

    # Only allow a list of trusted parameters through.
    def question_params
      params.require(:question).permit(:title, :content)
    end
end
# app/controllers/answers_controller.rb
class AnswersController < ApplicationController
  def create
    @question = Question.find(params[:question_id])
    @answer = @question.answers.create(answer_params)
    redirect_to question_path(@question)
  end

  def destroy
    @answer = Answer.find(params[:id])
    @answer.destroy
    redirect_to question_path(@answer.question)
  end

  private

  def answer_params
    params.require(:answer).permit(:content)
  end
end

***

4. 全文検索機能の実装

MySQLの全文検索機能を利用して、Q&Aの検索機能を実装します。gem 'mysql2'がインストールされていることを確認し、モデルに検索メソッドを追加します。

# app/models/question.rb
class Question < ApplicationRecord
  # タイトルに対する全文検索を行うメソッド
  def self.search(search)
    if search
      where('MATCH(title) AGAINST(?)', search)
    else
      all
    end
  end
end

***

5. フロントエンドの設定

Q&Aページのフロントエンドを設定します。questions#indexをホームページとして設定し、検索フォームと質問一覧を表示します。

質問の一覧ページ

<!-- app/views/questions/index.html.erb -->
<h1>質問一覧</h1>

<%= link_to '新しい質問をする', new_question_path %>

<%= form_tag search_questions_path, method: :get do %>
  <%= text_field_tag :search, params[:search], placeholder: "質問を検索" %>
  <%= submit_tag "検索", name: nil %>
<% end %>

<% @questions.each do |question| %>
  <div>
    <h2><%= link_to question.title, question %></h2>
    <p><%= truncate(question.content, length: 100) %></p>
  </div>
<% end %>

[検索フォームの作成]

このフォームから送信されたキーワードは、searchアクションを通じて全文検索に使用されます。

<%= form_tag search_questions_path, method: :get do %>
  <%= text_field_tag :search, params[:search], placeholder: "質問を検索" %>
  <%= submit_tag "検索", name: nil %>
<% end %>

[質問一覧の表示]

質問一覧を@questions変数から取得し、それぞれの質問に対するリンクを表示します。

<% @questions.each do |question| %>
  <h2><%= link_to question.title, question %></h2>
  <p><%= question.content %></p>
<% end %>

質問の詳細ページ

<!-- app/views/questions/show.html.erb -->
<h1><%= @question.title %></h1>
<p><%= @question.content %></p>

<%= link_to '編集', edit_question_path(@question) %> |
<%= link_to '一覧に戻る', questions_path %>

<h2>回答</h2>
<%= render @question.answers %>

<%= form_for([@question, @question.answers.build]) do |f| %>
  <p>
    <%= f.label :content, "あなたの回答" %><br>
    <%= f.text_area :content %>
  </p>
  <%= f.submit "回答する" %>
<% end %>

[詳細ページと回答フォームの追加]

質問の詳細ページには、質問の内容と、それに対する回答の一覧、回答するためのフォームを配置します。

新規質問作成ページ

<!-- app/views/questions/new.html.erb -->
<h1>新しい質問を作成する</h1>

<%= form_for(@question) do |f| %>
  <% if @question.errors.any? %>
    <div>
      <h2><%= pluralize(@question.errors.count, "error") %> prohibited this question from being saved:</h2>

      <ul>
      <% @question.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <p>
    <%= f.label :title %><br>
    <%= f.text_field :title %>
  </p>

  <p>
    <%= f.label :content %><br>
    <%= f.text_area :content %>
  </p>

  <p>
    <%= f.submit %>
  </p>
<% end %>

<%= link_to '一覧に戻る', questions_path %>

質問の編集ページ

<!-- app/views/questions/edit.html.erb -->
<h1>質問を編集する</h1>

<%= form_for(@question) do |f| %>
  <% if @question.errors.any? %>
    <div>
      <h2><%= pluralize(@question.errors.count, "error") %> prohibited this question from being saved:</h2>

      <ul>
      <% @question.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <p>
    <%= f.label :title %><br>
    <%= f.text_field :title %>
  </p>

  <p>
    <%= f.label :content %><br>
    <%= f.text_area :content %>
  </p>

  <p>
    <%= f.submit %>
  </p>
<% end %>

<%= link_to '詳細に戻る', @question %> |
<%= link_to '一覧に戻る', questions_path %>

回答の部分テンプレート

<!-- app/views/answers/_answer.html.erb -->
<div>
  <p><%= answer.content %></p>
  <% if answer.persisted? %>
    <%= link_to '回答を削除する', [answer.question, answer], method: :delete, data: { confirm: '本当に削除しますか?' } %>
  <% end %>
</div>

スタイルとインタラクションの追加

BootstrapやTailwindCSSのようなCSSフレームワークを使用して、UIを整えます。JavaScriptを用いてユーザーインタラクションを向上させることも検討しましょう。

***

6. テストとデプロイ

システムが正しく動作するかテストし、問題がなければデプロイします。

[補足]
正しく動作しない場合は、一度コンテナを再起動すると改善するかもしれません。

***

まとめ

この記事では、RailsとMySQLを用いた全文検索機能付きのQ&Aページの作成方法を紹介しました。この基本的なフレームワークを使用して、さらに機能を追加することもできます。

***

あとがき

ChatGPTに社内情報を学習させたいとの要望をもらったので、それに関する記事を書いてみました。
本当はベクトルデータベースとか使って構築するのがいいんでしょうけど、とっつきにくいですし…簡単に実装するならMySQLの全文検索を使った方がいいかなと思ったのでRails+MySQLの構成にしています。
いつかLLMの記事も書きたいです。

次は登録したQ&AをAPIで取得する記事を書きます!
その次はChatGPTのプロンプトにQ&A情報を持たせて回答させる、なんてこともやりたいなーと思ってます。

不明点などあればコメントください。

では、また次回。
お疲れ様でした!