使用Rails和Elasticsearch来创建搜索功能并进行各种试验-第一部分:创建示例应用程序

首先

由于在Rails应用中有机会实现基于Elasticsearch的搜索功能,因此计划将在此过程中进行的研究和尝试分批次汇总。

我們將首先使用docker-compose來構建本地環境,並創建一個可以進行簡單搜索的樣本應用程序。在後續的步驟中,我們還將深入探討搜索功能的自定義和適用於實際操作的實現方式。

用Rails和Elasticsearch创建搜索功能并尝试各种操作 – 文档处理
用Rails和Elasticsearch创建搜索功能并尝试各种操作 – Rspec
用Rails和Elasticsearch创建搜索功能并尝试各种操作 – 添加建议功能
用Rails和Elasticsearch创建搜索功能并尝试各种操作 – 同义词编辑

样本应用程序

我們將創建一個應用程式,用於檢索並顯示已註冊的漫畫資訊。

search_sample.mov.gif

环境

    • Ruby 2.5.3

 

    • Rails 5.2.2

 

    • Mysql 5.7

 

    • Elatsticsearch 6.5.4

 

    Kibana 6.5.4

构成

使用Docker Compose 创建本地环境。

docker-compose_image.png

Rails:应用程序的核心
Mysql:数据的持久化
Elasticsearch:用于搜索
Kibana:与应用程序本身无关(用于在Elasticsearch上进行各种尝试)

创建新的Rails应用的流程

使用docker-compose创建环境并启动Rails和Elasticsearch的步骤将在接下来的内容中描述。(与主题无关的部分请忽略)

docker-compose.yml 文件

将以下文件放置在项目的根目录中。

.
├── Dockerfile
├── docker
│   ├── es
│   │   └── Dockerfile
│   └── mysql
│       └── my.cnf
└── docker-compose.yml

version: '3'
services:
  # Elasticsearch用のコンテナ
  es:
    build: ./docker/es
    container_name: es_sample
    environment:
      - cluster.name=rails-sample-cluster
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - es_sample_data:/usr/share/elasticsearch/data
    ports:
      - 9200:9200
  # Kibana用のコンテナ
  kibana:
    image: docker.elastic.co/kibana/kibana:6.5.4
    environment:
      SERVER_NAME: localhost:5601
      ELASTICSEARCH_URL: http://es_sample:9200
    ports:
      - 5601:5601
    depends_on:
      - es
  # MYSQL用のコンテナ
  db:
    environment:
      - MYSQL_ROOT_PASSWORD=docker
      - MYSQL_PASSWORD=docker
      - MYSQL_USER=docker
      - MYSQL_DATABASE=rails_es_sample
    build: ./docker/mysql
    ports:
      - "3306:3306"
  # Rails用のコンテナ
  rails:
    build: .
    # 必要であればshなどに bundle install や rails s を実行してrailsを起動する処理を書く
    # command: scripts/start-server.sh
    volumes:
      - .:/app
      # 公式のDockerfile(ruby:2.5.3-stretch)では環境変数のBUNDLE_APP_CONFIGがデフォルトで
      # /usr/local/bundleに設定されているため、dockerのローカルvolumeでマウントしてそこにgemを入れている
      - vendor_bundle:/user/local/bundle
    ports:
      - "3003:3000"
    links:
      - db
      - es
    environment:
      - RAILS_DATABASE_USERNAME=root
      - RAILS_DATABASE_PASSWORD=docker
      - RAILS_DATABASE_NAME=rails_es_sample
      - RAILS_DATABASE_HOST=db
    tty: true
    stdin_open: true

volumes:
  es_sample_data:
    driver: local
  vendor_bundle:
    driver: local
FROM ruby:2.5.3-stretch

ENV BUNDLE_GEMFILE=/app/Gemfile \
    BUNDLE_JOBS=2 \
    RAILS_ENV=development \
    LANG=C.UTF-8

RUN apt-get update -qq
RUN apt-get install -y build-essential 
RUN apt-get install -y libpq-dev
RUN apt-get install -y nodejs

# ワーキングディレクトリの設定
RUN mkdir /app
WORKDIR /app
# ElasticDocker
FROM docker.elastic.co/elasticsearch/elasticsearch:6.5.4
# 日本語をあつかうときに使うプラグイン
RUN bin/elasticsearch-plugin install analysis-kuromoji

关于 ./docker/mysql/my.cnf,这不是我们讨论的重点,暂且不提。我将它放在这里供您参考。

图像的构建和启动

# imageのbuildと起動
$ docker-compose up -d

# 起動確認
$ docker-compose ps
          Name                        Command               State                 Ports
-----------------------------------------------------------------------------------------------------
es_sample                  /usr/local/bin/docker-entr ...   Up      0.0.0.0:9200->9200/tcp, 9300/tcp
rails_es_sample_db_1       docker-entrypoint.sh mysqld      Up      0.0.0.0:3306->3306/tcp, 33060/tcp
rails_es_sample_kibana_1   /usr/local/bin/kibana-docker     Up      0.0.0.0:5601->5601/tcp
rails_es_sample_rails_1    irb                              Up      0.0.0.0:3003->3000/tcp

创建一个新的Rails项目

我将在容器中创建一个Rails项目。

# コンテナに入る
# 「rails_es_sample_rails_1」 は docker-compose ps の Name
$ docker exec -it rails_es_sample_rails_1 /bin/bash

# コンテナ内で実行
/app# bundle init

修改gem文件

# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

# railsがコメントアウトされているので外す
gem "rails"

在中国以母语解释:
Rails的安装和项目创建

# railsのコンテナ内
/app# bundle install
/app# bundle exec rails new .

# 以下のようにgemfileを上書きするか聞かれますが、まだ何も追加していない状態なので「Y」で上書き
# Overwrite /app/Gemfile? (enter "h" for help) [Ynaqdhm]

MySQL的设置

添加MySQL适配器

# gem 'sqlite3'
gem 'mysql2'
/app# bundle install

因为数据库配置文件(database.yml)保持默认状态,所以需要进行修改。

default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: docker
  host: db


development:
  <<: *default
  database: rails_es_sample

启动Rails

/app# bundle exec rails s

确认启动

铁轨

スクリーンショット 2019-02-10 19.11.20.png

弹性搜索


$ curl -XGET http://localhost:9200/
# 以下のようなクラスターやversionの情報が返ればOK
{
  "name" : "338gbNM",
  "cluster_name" : "rails-sample-cluster",
  "cluster_uuid" : "HphoN9CyQcmWeruBOQr1oQ",
  "version" : {
    "number" : "6.5.4",
    "build_flavor" : "default",
    "build_type" : "tar",
    "build_hash" : "d2ef93d",
    "build_date" : "2018-12-17T21:17:40.758843Z",
    "build_snapshot" : false,
    "lucene_version" : "7.5.0",
    "minimum_wire_compatibility_version" : "5.6.0",
    "minimum_index_compatibility_version" : "5.0.0"
  },
  "tagline" : "You Know, for Search"
}

Kibana 可视化工具

只需提供一个选择,用中文这样改写:
在浏览器中访问 http://localhost:5601/app/kibana,如果显示出如下所示的页面,那就表示一切正常。

スクリーンショット 2019-02-10 19.49.23.png

实体关系图

由于环境已经准备就绪,我们开始创建示例应用程序。
我们将创建一个表来存储与漫画信息相关的作者、出版社和类别,就像ER图一样。

スクリーンショット 2019-02-09 14.46.36.png

创建模型和表

创建迁移文件。

# migrationファイルの作成
/app# bundle exec rails g model author name:string
/app# bundle exec rails g model publisher name:string
/app# bundle exec rails g model category name:string
/app# bundle exec rails g model manga author:references publisher:references category:references title:string description:text

# テーブルの作成
/app# bundle exec rails db:migrate

数据准备

在db/seeds.rb中准备数据。(这里有要添加的数据样本)

/app# db/seeds.rbを修正後に実行
bundle exec rails db:seed

在中文中使用原生语言表达时,有以下的一个选项:

添加控制器、视图和路由。

使用Rails g命令创建文件,并进行修正。

/app# bundle exec rails g controller Mangas index --helper=false --assets=false
class MangasController < ApplicationController
  def index
    @mangas = Manga.all
  end
end
Rails.application.routes.draw do
  resources :mangas, only: %i(index)
end
<h1>Mangas</h1>

<table>
  <thead>
    <tr>
      <th>Aauthor</th>
      <th>Publisher</th>
      <th>Category</th>
      <th>Author</th>
      <th>Title</th>
      <th>Description</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <% @mangas.each do |manga| %>
      <tr>
        <td><%= manga.author.name %></td>
        <td><%= manga.publisher.name %></td>
        <td><%= manga.category.name %></td>
        <td><%= manga.author.name %></td>
        <td><%= manga.title %></td>
        <td><%= manga.description %></td>
      </tr>
    <% end %>
  </tbody>
</table>

使用Bulma进行样式修正

在这个阶段,当访问http://localhost:3003/mangas时,注册的数据将以列表形式显示出来,但是由于外观很简陋,因此我们将使用名为Bulma的CSS框架来稍微美化外观。

Gem的额外增加

添加 gem 并运行 bundle install

gem "bulma-rails", "~> 0.7.2"

将CSS更改为SCSS,并导入Bulma

 /
 *= require_tree .
 *= require_self
 */

 @import "bulma";

样式的调整

更正资料

スクリーンショット 2019-02-10 22.55.20.png

添加到Elasticsearch的宝石库

在这里,我们将开始对Elasticsearch相关的修正,虽然前言有点长。

我们将使用 Elastic 官方仓库中的 gem。

gem 'elasticsearch-model', github: 'elasticsearch/elasticsearch-rails', branch: '6.x'
gem 'elasticsearch-rails', github: 'elasticsearch/elasticsearch-rails', branch: '6.x'

Elasticsearch模型

通过将Elasticsearch::Model添加到模型中,您将可以使用各种方法来处理文档。

Elasticsearch-rails 弹性搜索栏架

据说可以使用 Elasticsearch 来定制 rake 任务、日志记录器以及提供模板等。

配置設置

设置连接目标的信息。

# 「es」はdocker-composeのservicesに設定した名前
config = {
    host:  ENV['ELASTICSEARCH_HOST'] || "es:9200/",
}

Elasticsearch::Model.client = Elasticsearch::Client.new(config)

关于concerns的添加

我們將創建一個關於整理Elasticsearch相關操作的concern。

创建一个名为”concern”的文件,并在模型中包含它。

class Manga < ApplicationRecord
  include MangaSearchable

  belongs_to :author
  belongs_to :publisher
  belongs_to :category
end
module MangaSearchable
  extend ActiveSupport::Concern

  included do
    include Elasticsearch::Model

    # ①index名
    index_name "es_manga_#{Rails.env}"

    # ②マッピング情報
    settings do
      mappings dynamic: 'false' do
        indexes :id,                   type: 'integer'
        indexes :publisher,            type: 'keyword'
        indexes :author,               type: 'keyword'
        indexes :category,             type: 'text', analyzer: 'kuromoji'
        indexes :title,                type: 'text', analyzer: 'kuromoji'
        indexes :description,          type: 'text', analyzer: 'kuromoji'
      end
    end

    # ③mappingの定義に合わせてindexするドキュメントの情報を生成する
    def as_indexed_json(*)
      attributes
        .symbolize_keys
        .slice(:id, :title, :description)
        .merge(publisher: publisher_name, author: author_name, category: category_name)
    end
  end

  def publisher_name
    publisher.name
  end

  def author_name
    author.name
  end

  def category_name
    category.name
  end

  class_methods do
    # ④indexを作成するメソッド
    def create_index!
      client = __elasticsearch__.client
      # すでにindexを作成済みの場合は削除する
      client.indices.delete index: self.index_name rescue nil
      # indexを作成する
      client.indices.create(index: self.index_name,
                            body: {
                                settings: self.settings.to_hash,
                                mappings: self.mappings.to_hash
                            })
    end
  end
end

①设置索引名称。为了防止错误操作,我们要包含环境名称。

我們正在定義登記文件映射信息。在這裡,您可以指定字段類型、使用的分析器等。同時,您也可以定義設置信息,但在這個例子中,我們將保持預設設置。

这是一个方法,用于根据已经定义的映射信息,将模型的信息转换为JSON以进行注册。

④ 创建索引的方法。如果已经创建过,则先进行删除处理再重新创建。

确认动作

通过包括Elasticsearch::Model,可以使用添加到gem中的方法等。
我们来在控制台上进行验证。

确认连接到Elasticsearch。

pry(main)> Manga.__elasticsearch__.client.cluster.health
=> {"cluster_name"=>"rails-sample-cluster",
 "status"=>"green",
 "timed_out"=>false,
 "number_of_nodes"=>1,
 "number_of_data_nodes"=>1,
 "active_primary_shards"=>1,
 "active_shards"=>1,
 "relocating_shards"=>0,
 "initializing_shards"=>0,
 "unassigned_shards"=>0,
 "delayed_unassigned_shards"=>0,
 "number_of_pending_tasks"=>0,
 "number_of_in_flight_fetch"=>0,
 "task_max_waiting_in_queue_millis"=>0,
 "active_shards_percent_as_number"=>100.0}
[5] pry(main)>

创建索引


pry(main)> Manga.create_index!
=> {"acknowledged"=>true, "shards_acknowledged"=>true, "index"=>"es_manga_development"}

数据的登记

使用import方法将模型信息进行注册。将之前添加的as_indexed_json格式进行转换,以将数据进行注册。

pry(main)> Manga.__elasticsearch__.import
   (5.5ms)  SET NAMES utf8,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
  Manga Load (3.0ms)  SELECT  `mangas`.* FROM `mangas` ORDER BY `mangas`.`id` ASC LIMIT 1000
  Publisher Load (3.3ms)  SELECT  `publishers`.* FROM `publishers` WHERE `publishers`.`id` = 1 LIMIT 1
  Author Load (0.5ms)  SELECT  `authors`.* FROM `authors` WHERE `authors`.`id` = 1 LIMIT 1

添加搜索功能

由于连接到 Elasticsearch 并成功注册数据,所以接下来我们将创建搜索功能。

增加一个用于搜索的方法

我們將為concern添加一個用於搜索的方法。在這個例子中,我們使用multi_match和cross_fields來搜索與多個字段中的任意字段匹配的內容。有關可指定的查詢等詳細信息,請參閱文檔。

  class_methods do
    # ...

    def es_search(query)
      __elasticsearch__.search({
        query: {
          multi_match: {
            fields: %w(id publisher author category title description),
            type: 'cross_fields',
            query: query,
            operator: 'and'
          }
        }
      })
    end
  end
end

控制器的修改

使用接收到的search_word参数,并通过之前创建的es_search方法进行搜索。如果搜索词为空,则获取所有数据。

class MangasController < ApplicationController
  def index
    @mangas = if search_word.present?
                Manga.es_search(search_word).records
              else
                Manga.all
              end
  end

  private

    def search_word
      @search_word ||= params[:search_word]
    end
end

修改视图

添加搜索框。


// ...
    </div>
  </div>
</section>

// ヘッダーとテーブルの間に検索窓を追加
<div class="container" style="margin-top: 30px">
  <%= form_tag(mangas_path, method: :get, class: "field has-addons has-addons-centered") do %>
    <div class="control">
      <%= text_field_tag :search_word, @search_word, class: "input", placeholder: "漫画を検索する" %>
    </div>
    <div class="control">
      <%= submit_tag "検索", class: "button is-info" %>
    </div>
  <% end %>
</div>


<div class="container" style="margin-top: 50px">
  <table class="table is-striped is-hoverable">
// ...

确认动作

search_sample2.mov.gif

分頁

现在搜索已经可以工作了,但是显示所有搜索结果的数据有点微妙,所以我们会添加分页功能。

添加宝石

gem 'kaminari'

需要注意的是,必須將其添加到Elasticsearch gem之上。
https://github.com/elastic/elasticsearch-rails/tree/master/elasticsearch-model#pagination

在你的Gemfile中,或者在你的应用程序中,必须先添加分页宝石再添加Elasticsearch宝石。

控制器修改修正

将在Elasticsearch响应中添加页面和每页结果数的参数。同时,还会在不通过Elasticsearch进行搜索的情况下添加这些参数。

  def index
    @mangas = if search_word.present?
                Manga.es_search(search_word).page(params[:page] || 1).per(5).records
              else
                Manga.page(params[:page] || 1).per(5)
              end
  end

查看的修改

我将创建一个可以应用Bulma样式的Kaminari模板。

/app# bundle exec rails g kaminari:views default

当执行此操作时,会在app/views/kaminari目录下生成文件,我们逐步对这些文件进行修改。
由于有许多细微的修改,这里就不一一列举了,修正版将会发布在这里。

概括

这次稍微变长了一点,我使用docker-compose来创建了一个环境,从rails new开始,然后制作了一个使用Elasticsearch进行搜索的样例应用程序。
目前已经完成了一个可以运行的版本,所以下次我打算写一些更深入的内容。

广告
将在 10 秒后关闭
bannerAds