使用Rails和Elasticsearch来创建搜索功能并进行各种试验-第一部分:创建示例应用程序
首先
由于在Rails应用中有机会实现基于Elasticsearch的搜索功能,因此计划将在此过程中进行的研究和尝试分批次汇总。
我們將首先使用docker-compose來構建本地環境,並創建一個可以進行簡單搜索的樣本應用程序。在後續的步驟中,我們還將深入探討搜索功能的自定義和適用於實際操作的實現方式。
用Rails和Elasticsearch创建搜索功能并尝试各种操作 – 文档处理
用Rails和Elasticsearch创建搜索功能并尝试各种操作 – Rspec
用Rails和Elasticsearch创建搜索功能并尝试各种操作 – 添加建议功能
用Rails和Elasticsearch创建搜索功能并尝试各种操作 – 同义词编辑
样本应用程序
我們將創建一個應用程式,用於檢索並顯示已註冊的漫畫資訊。
环境
-
- Ruby 2.5.3
-
- Rails 5.2.2
-
- Mysql 5.7
-
- Elatsticsearch 6.5.4
- Kibana 6.5.4
构成
使用Docker Compose 创建本地环境。
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
确认启动
铁轨
弹性搜索
$ 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,如果显示出如下所示的页面,那就表示一切正常。
实体关系图
由于环境已经准备就绪,我们开始创建示例应用程序。
我们将创建一个表来存储与漫画信息相关的作者、出版社和类别,就像ER图一样。
创建模型和表
创建迁移文件。
# 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";
样式的调整
更正资料
添加到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">
// ...
确认动作
分頁
现在搜索已经可以工作了,但是显示所有搜索结果的数据有点微妙,所以我们会添加分页功能。
添加宝石
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进行搜索的样例应用程序。
目前已经完成了一个可以运行的版本,所以下次我打算写一些更深入的内容。