apeescape2.com
  • メイン
  • データサイエンスとデータベース
  • ヒントとツール
  • アジャイル
  • ライフスタイル
技術

Ruby on RailsのElasticsearch:ChewyGemのチュートリアル

Elasticsearchは、データのインデックス作成とクエリを行うための強力なRESTfulHTTPインターフェースを提供します。 Apache Lucene 図書館。箱から出してすぐに、UTF-8をサポートし、スケーラブルで効率的かつ堅牢な検索を提供します。これは、大量の構造化データのインデックス作成とクエリを行うための強力なツールです。 サルゲッチュ 、プラットフォーム検索を強化し、間もなくオートコンプリートにも使用されます。私たちは大ファンです。

ChewyはElasticsearch-Rubyクライアントを拡張し、より強力にし、Railsとの緊密な統合を提供します。

私たちのプラットフォームはを使用して構築されているので Ruby on Rails 、Elasticsearchの統合は、 Elasticsearch-ルビー プロジェクト(Elasticsearchクラスターに接続するためのクライアントを提供するElasticsearch用のRuby統合フレームワーク、ElasticsearchのRESTAPI用のRubyAPI、およびさまざまな拡張機能とユーティリティ)。この基盤に基づいて、Elasticsearchアプリケーション検索アーキテクチャの独自の改善(および簡略化)を開発およびリリースしました。これは、名前を付けたRubygemとしてパッケージ化されています。 歯ごたえ (利用可能なサンプルアプリ付き ここに )。

ChewyはElasticsearch-Rubyクライアントを拡張し、より強力にし、Railsとの緊密な統合を提供します。 このElasticsearchガイドでは、実装中に発生した技術的な障害を含め、これをどのように達成したかについて(使用例を通じて)説明します。



ElasticsearchとRubyon Railsの関係は、このビジュアルガイドに示されています。

ガイドに進む前の簡単なメモ:

  • どちらも 歯ごたえ と 歯ごたえのあるデモアプリケーション GitHubで入手できます。
  • Elasticsearchの「内部」情報に興味がある人のために、簡単な記事を 付録 この投稿に。

なぜ歯ごたえがあるのですか?

Elasticsearchのスケーラビリティと効率性にもかかわらず、Railsとの統合は予想ほど単純ではありませんでした。 ApeeScapeでは、基本的なElasticsearch-Rubyクライアントを大幅に拡張して、パフォーマンスを向上させ、追加の操作をサポートする必要があることに気付きました。

Elasticsearchのスケーラビリティと効率性にもかかわらず、Railsとの統合は予想されたほど単純ではありませんでした。

そしてこうして、歯ごたえのある宝石が生まれました。

Chewyの特に注目すべき機能は次のとおりです。

クレジットカード番号ビザマスターカードをハックする
  1. すべてのインデックスは、関連するすべてのモデルで監視できます。

    ほとんどのインデックス付きモデルは相互に関連しています。また、場合によっては、この関連データを非正規化し、同じオブジェクトにバインドする必要があります(たとえば、タグの配列を関連する記事と一緒にインデックス付けする場合)。 Chewyを使用すると、すべてのモデルに更新可能なインデックスを指定できるため、関連するタグが更新されるたびに、対応する記事のインデックスが再作成されます。

  2. インデックスクラスは、ORM / ODMモデルから独立しています。

    この機能拡張により、たとえば、クロスモデルオートコンプリートの実装がはるかに簡単になります。インデックスを定義して、オブジェクト指向で操作するだけです。他のクライアントとは異なり、Chewy gemを使用すると、インデックスクラス、データインポートコールバック、およびその他のコンポーネントを手動で実装する必要がなくなります。

  3. 一括インポートは どこにでも 。

    Chewyは、一括のElasticsearch APIを利用して、完全なインデックスの再作成とインデックスの更新を行います。また、アトミック更新の概念を利用して、アトミックブロック内で変更されたオブジェクトを収集し、それらをすべて一度に更新します。

  4. Chewyは、ARスタイルのクエリDSLを提供します。

    この拡張機能は、連鎖可能、マージ可能、および遅延であるため、クエリをより効率的に生成できます。

では、これがすべて宝石でどのように機能するかを見てみましょう…

Elasticsearchの基本ガイド

Elasticsearchには、ドキュメントに関連するいくつかの概念があります。 1つ目はindexのそれです(databaseの類似物 RDBMS )、これはdocumentsのセットで構成され、複数のtypesで構成されます。 (ここで、typeは一種のRDBMSテーブルです)。

すべてのドキュメントにはfieldsのセットがあります。各フィールドは個別に分析され、その分析オプションはmappingに保存されます。そのタイプのために。 Chewyは、オブジェクトモデルでこの構造を「現状のまま」利用します。

class EntertainmentIndex { author.name } field :author_id, type: 'integer' field :description field :tags, index: 'not_analyzed', value: ->{ tags.map(&:name) } end {movie: Video.movies, cartoon: Video.cartoons}.each do |type_name, scope| define_type scope.includes(:director, :tags), name: type_name do field :title, analyzer: 'title' field :year, type: 'integer' field :author, value: ->{ director.name } field :author_id, type: 'integer', value: ->{ director_id } field :description field :tags, index: 'not_analyzed', value: ->{ tags.map(&:name) } end end end

上記では、entertainmentというElasticsearchインデックスを定義しましたbook、movie、cartoonの3つのタイプがあります。タイプごとに、いくつかのフィールドマッピングと、インデックス全体の設定のハッシュを定義しました。

そこで、EntertainmentIndexを定義しましたいくつかのクエリを実行したいと思います。最初のステップとして、インデックスを作成してデータをインポートする必要があります。

EntertainmentIndex.create! EntertainmentIndex.import # EntertainmentIndex.reset! (which includes deletion, # creation, and import) could be used instead

.importタイプを定義したときにスコープを渡したため、メソッドはインポートされたデータを認識します。したがって、永続ストレージに保存されているすべての本、映画、漫画がインポートされます。

これで、いくつかのクエリを実行できます。

投資収益率の種類
EntertainmentIndex.query(match: {author: 'Tarantino'}).filter{ year > 1990 } EntertainmentIndex.query(match: {title: 'Shawshank'}).types(:movie) EntertainmentIndex.query(match: {author: 'Tarantino'}).only(:id).limit(10).load # the last one loads ActiveRecord objects for documents found

これで、インデックスを検索実装で使用する準備がほぼ整いました。

Railsの統合

Railsと統合するために最初に必要なことは、RDBMSオブジェクトの変更に対応できるようにすることです。 Chewyは、update_index内で定義されたコールバックを介してこの動作をサポートしますクラスメソッド。 update_index 2つの引数を取ります:

  1. 'index_name#type_name'で提供されるタイプ識別子フォーマット
  2. 実行するメソッド名またはブロック。これは、更新されたオブジェクトまたはオブジェクトコレクションへの後方参照を表します。

依存モデルごとに次のコールバックを定義する必要があります。

class Book

タグにもインデックスが付けられているため、次に、変更に反応するように、いくつかの外部モデルにモンキーパッチを適用する必要があります。

ActsAsTaggableOn::Tag.class_eval do has_many :books, through: :taggings, source: :taggable, source_type: 'Book' has_many :videos, through: :taggings, source: :taggable, source_type: 'Video' # Updating all tag-related objects update_index 'entertainment#book', :books update_index('entertainment#movie') { videos.movies } update_index('entertainment#cartoon') { videos.cartoons } end ActsAsTaggableOn::Tagging.class_eval do # Same goes for the intermediate model update_index('entertainment#book') { taggable if taggable_type == 'Book' } update_index('entertainment#movie') { taggable if taggable_type == 'Video' && taggable.movie? } update_index('entertainment#cartoon') { taggable if taggable_type == 'Video' && taggable.cartoon? } end

この時点で、すべてのオブジェクト セーブ または 破壊 対応するElasticsearchインデックスタイプを更新します。

アトミシティ

まだ1つの長引く問題があります。 books.map(&:save)のようなことをすると複数の書籍を保存するには、entertainmentの更新をリクエストしますインデックス 個々の本が保存されるたびに 。したがって、5冊の本を保存すると、Chewyインデックスが5回更新されます。この動作は、 REPL 、ただし、パフォーマンスが重要なコントローラーアクションには受け入れられません。

この問題はChewy.atomicで対処しますブロック:

class ApplicationController

要するに、Chewy.atomicこれらの更新を次のようにバッチ処理します。

  1. after_saveを無効にします折り返し電話。
  2. 保存した本のIDを収集します。
  3. Chewy.atomicの完了時ブロックは、収集されたIDを使用して、単一のElasticsearchインデックス更新リクエストを作成します。

検索中

これで、検索インターフェースを実装する準備が整いました。私たちのユーザーインターフェースはフォームなので、それを構築するための最良の方法は、もちろん、 FormBuilder そして ActiveModel 。 (ApeeScapeでは、 ActiveData ActiveModelインターフェイスを実装しますが、お気に入りのgemを自由に使用してください。)

class EntertainmentSearch include ActiveData::Model attribute :query, type: String attribute :author_id, type: Integer attribute :min_year, type: Integer attribute :max_year, type: Integer attribute :tags, mode: :arrayed, type: String, normalize: ->(value) { value.reject(&:blank?) } # This accessor is for the form. It will have a single text field # for comma-separated tag inputs. def tag_list= value self.tags = value.split(',').map(&:strip) end def tag_list self.tags.join(', ') end end

クエリとフィルターのチュートリアル

属性を受け入れてタイプキャストできるActiveModelのようなオブジェクトができたので、検索を実装しましょう。

class EntertainmentSearch ... def index EntertainmentIndex end def search # We can merge multiple scopes [query_string, author_id_filter, year_filter, tags_filter].compact.reduce(:merge) end # Using query_string advanced query for the main query input def query_string index.query(query_string: {fields: [:title, :author, :description], query: query, default_operator: 'and'}) if query? end # Simple term filter for author id. `:author_id` is already # typecasted to integer and ignored if empty. def author_id_filter index.filter(term: {author_id: author_id}) if author_id? end # For filtering on years, we will use range filter. # Returns nil if both min_year and max_year are not passed to the model. def year_filter body = {}.tap do |body| body.merge!(gte: min_year) if min_year? body.merge!(lte: max_year) if max_year? end index.filter(range: {year: body}) if body.present? end # Same goes for `author_id_filter`, but `terms` filter used. # Returns nil if no tags passed in. def tags_filter index.filter(terms: {tags: tags}) if tags? end end

コントローラーとビュー

この時点で、モデルは渡された属性を使用して検索要求を実行できます。使用法は次のようになります。

EntertainmentSearch.new(query: 'Tarantino', min_year: 1990).search

コントローラでは、代わりに正確なActiveRecordオブジェクトをロードしたいことに注意してください 歯ごたえ ドキュメントラッパー:

class EntertainmentController

さて、いくつかを書く時が来ました HAML entertainment/index.html.hamlで:

= form_for @search, as: :search, url: entertainment_index_path, method: :get do |f| = f.text_field :query = f.select :author_id, Dude.all.map d, include_blank: true = f.text_field :min_year = f.text_field :max_year = f.text_field :tag_list = f.submit - if @entertainments.any? %dl - @entertainments.each do |entertainment| %dt %h1= entertainment.title %strong= entertainment.class %dd %p= entertainment.year %p= entertainment.description %p= entertainment.tag_list = paginate @entertainments - else Nothing to see here

並べ替え

ボーナスとして、検索機能に並べ替えも追加します。

タイトルフィールドと年フィールド、および関連性で並べ替える必要があると想定します。残念ながら、タイトルOne Flew Over the Cuckoo's Nest個々の用語に分割されるため、これらの異なる用語による並べ替えはランダムになりすぎます。代わりに、タイトル全体で並べ替えたいと思います。

解決策は、特別なタイトルフィールドを使用し、独自のアナライザーを適用することです。

class EntertainmentIndex

さらに、これらの新しい属性と並べ替え処理ステップの両方を検索モデルに追加します。

class EntertainmentSearch # we are going to use `title.sorted` field for sort SORT = {title: {'title.sorted' => :asc}, year: {year: :desc}, relevance: :_score} ... attribute :sort, type: String, enum: %w(title year relevance), default_blank: 'relevance' ... def search # we have added `sorting` scope to merge list [query_string, author_id_filter, year_filter, tags_filter, sorting].compact.reduce(:merge) end def sorting # We have one of the 3 possible values in `sort` attribute # and `SORT` mapping returns actual sorting expression index.order(SORT[sort.to_sym]) end end

最後に、並べ替えオプションの選択ボックスを追加してフォームを変更します。

= form_for @search, as: :search, url: entertainment_index_path, method: :get do |f| ... / `EntertainmentSearch.sort_values` will just return / enum option content from the sort attribute definition. = f.select :sort, EntertainmentSearch.sort_values ...

エラー処理

ユーザーが(のような誤ったクエリを実行した場合またはANDの場合、Elasticsearchクライアントはエラーを発生させます。これを処理するために、コントローラーにいくつかの変更を加えましょう。

class EntertainmentController e @entertainments = [] @error = e.message.match(/QueryParsingException[([^;]+)]/).try(:[], 1) end end

さらに、ビューにエラーをレンダリングする必要があります。

... - if @entertainments.any? ... - else - if @error = @error - else Nothing to see here

Elasticsearchクエリのテスト

基本的なテスト設定は次のとおりです。

  1. Elasticsearchサーバーを起動します。
  2. インデックスをクリーンアップして作成します。
  3. データをインポートします。
  4. クエリを実行します。
  5. 結果を私たちの期待と相互参照します。

ステップ1では、で定義されたテストクラスターを使用すると便利です。 Elasticsearch-拡張機能 宝石。プロジェクトのRakefileに次の行を追加するだけです。ジェム後のインストール:

require 'elasticsearch/extensions/test/cluster/tasks'

次に、次のようになります レーキ タスク:

$ rake -T elasticsearch rake elasticsearch:start # Start Elasticsearch cluster for tests rake elasticsearch:stop # Stop Elasticsearch cluster for tests

ElasticsearchとRspec

まず、データの変更と同期するようにインデックスが更新されていることを確認する必要があります。幸いなことに、歯ごたえのある宝石には便利なupdate_indexが付属しています rspec 一致:

describe EntertainmentIndex do # No need to cleanup Elasticsearch as requests are # stubbed in case of `update_index` matcher usage. describe 'Tag' do # We create several books with the same tag let(:books) { create_list :book, 2, tag_list: 'tag1' } specify do # We expect that after modifying the tag name... expect do ActsAsTaggableOn::Tag.where(name: 'tag1').update_attributes(name: 'tag2') # ... the corresponding type will be updated with previously-created books. end.to update_index('entertainment#book').and_reindex(books, with: {tags: ['tag2']}) end end end

次に、実際の検索クエリが適切に実行され、期待される結果が返されることをテストする必要があります。

describe EntertainmentSearch do # Just defining helpers for simplifying testing def search attributes = {} EntertainmentSearch.new(attributes).search end # Import helper as well def import *args # We are using `import!` here to be sure all the objects are imported # correctly before examples run. EntertainmentIndex.import! *args end # Deletes and recreates index before every example before { EntertainmentIndex.purge! } describe '#min_year, #max_year' do let(:book) { create(:book, year: 1925) } let(:movie) { create(:movie, year: 1970) } let(:cartoon) { create(:cartoon, year: 1995) } before { import book: book, movie: movie, cartoon: cartoon } # NOTE: The sample code below provides a clear usage example but is not # optimized code. Something along the following lines would perform better: # `specify { search(min_year: 1970).map(&:id).map(&:to_i) # .should =~ [movie, cartoon].map(&:id) }` specify { search(min_year: 1970).load.should =~ [movie, cartoon] } specify { search(max_year: 1980).load.should =~ [book, movie] } specify { search(min_year: 1970, max_year: 1980).load.should == [movie] } specify { search(min_year: 1980, max_year: 1970).should == [] } end end

テストクラスターのトラブルシューティング

最後に、テストクラスターのトラブルシューティングのガイドを次に示します。

クレジットカードのトップアップハック
  • 開始するには、インメモリの1ノードクラスターを使用します。スペック的にははるかに高速になります。私たちの場合:TEST_CLUSTER_NODES=1 rake elasticsearch:start

  • elasticsearch-extensionsにはいくつかの既存の問題があります1ノードのクラスターステータスチェックに関連するクラスター実装自体をテストします(場合によっては黄色で、緑色になることはないため、緑色のステータスのクラスター開始チェックは毎回失敗します)。この問題はフォークで修正されましたが、メインリポジトリですぐに修正されることを願っています。

  • データセットごとに、リクエストを仕様にグループ化します(つまり、データを1回インポートしてから、複数のリクエストを実行します)。 Elasticsearchは長時間ウォームアップし、データのインポート中に大量のヒープメモリを使用するため、特に多数の仕様がある場合は、やりすぎないでください。

  • マシンに十分なメモリがあることを確認してください。そうしないと、Elasticsearchがフリーズします(テスト仮想マシンごとに約5GB、Elasticsearch自体に約1GBが必要です)。

まとめ

Elasticsearchは、「柔軟で強力なオープンソース、分散型のリアルタイム検索、および分析エンジン」と自称しています。これは検索テクノロジーのゴールドスタンダードです。

Chewyで、私たちの レール開発者 これらの利点を、Railsとの緊密な統合を提供するシンプルで使いやすい本番品質のオープンソースRubygemとしてパッケージ化しました。 ElasticsearchとRails–なんて素晴らしい組み合わせでしょう!

ElasticsearchとRails-なんて素晴らしい組み合わせでしょう! つぶやき


付録:Elasticsearchの内部

これが 非常に Elasticsearchの「内部」の簡単な紹介…

Elasticsearchは上に構築されています Lucene 、それ自体が使用します 転置インデックス その主要なデータ構造として。たとえば、「犬が高くジャンプする」、「フェンスを飛び越える」、「フェンスが高すぎる」という文字列がある場合、次の構造になります。

'the' [0, 0], [1, 2], [2, 0] 'dogs' [0, 1] 'jump' [0, 2], [1, 0] 'high' [0, 3], [2, 4] 'over' [1, 1] 'fence' [1, 3], [2, 1] 'was' [2, 2] 'too' [2, 3]

したがって、すべての用語には、テキストへの参照とテキスト内の位置の両方が含まれます。さらに、用語を変更して(たとえば、「the」などのストップワードを削除して)適用することを選択します ふりがな すべての用語に(あなたは推測できますか アルゴリズム ?):

'DAG' [0, 1] 'JANP' [0, 2], [1, 0] 'HAG' [0, 3], [2, 4] 'OVAR' [1, 1] 'FANC' [1, 3], [2, 1] 'W' [2, 2] 'T' [2, 3]

次に「犬のジャンプ」をクエリすると、ソーステキストと同じ方法で分析され、ハッシュ後に「DAG JANP」になります(「犬」は「犬」と同じハッシュを持ち、「ジャンプ」と'ジャンプ')。

また、文字列内の個々の単語の間に(構成設定に基づいて)ロジックを追加し、( 'DAG' AND 'JANP')または( 'DAG' OR 'JANP')から選択します。前者は[0] & [0, 1]の共通部分を返します(つまり、ドキュメント0)および後者の[0] | [0, 1] (つまり、ドキュメント0と1)。テキスト内の位置は、結果のスコアリングと位置に依存するクエリに使用できます。

優れた質問は優れたデザインにつながる–デザイン思考プロセスのガイド

Uxデザイン

優れた質問は優れたデザインにつながる–デザイン思考プロセスのガイド
レガシーソフトウェアの最新化:ErlangとCloudIを使用したMUDプログラミング

レガシーソフトウェアの最新化:ErlangとCloudIを使用したMUDプログラミング

データサイエンスとデータベース

人気の投稿
複数の主要な利害関係者による製品バックログの優先順位付け:ケーススタディ
複数の主要な利害関係者による製品バックログの優先順位付け:ケーススタディ
生成的敵対的ネットワークを使用してランダムノイズからデータを作成する
生成的敵対的ネットワークを使用してランダムノイズからデータを作成する
アジャイルコーチは何をし、どうすれば1つになることができますか?
アジャイルコーチは何をし、どうすれば1つになることができますか?
Railsヘルパーの使用方法:ブートストラップカルーセルのデモンストレーション
Railsヘルパーの使用方法:ブートストラップカルーセルのデモンストレーション
国際送金市場はどのように進化していますか?
国際送金市場はどのように進化していますか?
 
あなたの本当に、フリーランスのデザインアドバイス
あなたの本当に、フリーランスのデザインアドバイス
デザインツールの対決– Adob​​e XDとSketch(2019)
デザインツールの対決– Adob​​e XDとSketch(2019)
ゴールドスミスチームのシニアバックエンドエンジニア
ゴールドスミスチームのシニアバックエンドエンジニア
シニアRubyonRailsエンジニア
シニアRubyonRailsエンジニア
DesignOpsのフィールドガイド
DesignOpsのフィールドガイド
人気の投稿
  • PHP7を使用する必要があります
  • 最初にcまたはc ++
  • デザインアートの5つの原則
  • 法人LLCとは
  • 初心者のためのAngular5チュートリアル
  • コミュニケーションデザインvsグラフィックデザイン
カテゴリー
分散チーム Webフロントエンド トレンド Kpiと分析 収益性と効率性 製品の担当者とチーム リモートの台頭 ブランドデザイン 仕事の未来 モバイル

© 2021 | 全著作権所有

apeescape2.com