Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

RailsでのElasticsearchによる全文検索

Elasticsearchは、世の中で最も人気のある検索エンジンの1つです。それを愛し、制作に積極的に使用している多くの大企業の中には、Netflix、Medium、GitHubなどの巨人がいます。

Elasticsearchは非常に強力であり、主なユースケースは全文検索、リアルタイムログ、セキュリティ分析を特徴としています。

残念ながら、ElasticsearchはRailsコミュニティからあまり注目されていないため、この記事では、Elasticsearchの概念を読者に紹介し、RubyonRailsで使用する方法を示すという2つの目標を念頭に置いてこれを変更しようとしています。

ここでビルドするサンプルプロジェクトのソースコードを見つけることができます。コミット履歴は、この記事のセクションの順序にほぼ対応しています。

はじめに

より広い観点から、Elasticsearchは検索エンジンです

  • ApacheLuceneの上に構築されています;
  • JSONドキュメントを保存して効果的にインデックス付けします。
  • はオープンソースです;
  • それと対話するためのRESTAPIのセットを提供します;
  • デフォルトではセキュリティはありません(誰でもパブリックエンドポイントを介してクエリできます)。
  • 水平方向にかなりうまくスケーリングします。

基本的な概念のいくつかを簡単に見てみましょう。

Elasticsearchを使用して、ドキュメントをインデックスに入れ、データをクエリします。

インデックス リレーショナルデータベースのテーブルに似ています。 書類を置くお店です (行)後で照会できます。

ドキュメント フィールドのコレクションです(リレーショナルデータベースの行に似ています)。

マッピング リレーショナルデータベースのスキーマ定義のようなものです。マッピングは明示的に定義することも、挿入時にElasticsearchによって推測することもできます。インデックスマッピングを事前に定義することをお勧めします。

それが終わったら、環境を設定しましょう。

Elasticsearchのインストール

MacOSにElasticsearchをインストールする最も簡単な方法は、brewを使用することです:

brew tap elastic/tap
brew install elastic/tap/elasticsearch-full

別の方法として、dockerを介して実行することもできます:

docker run \
  -p 127.0.0.1:9200:9200 \
  -p 127.0.0.1:9300:9300 \
  -e "discovery.type=single-node" \
  docker.elastic.co/elasticsearch/elasticsearch:7.16.2

その他のオプションについては、公式リファレンスを参照してください。

Elasticsearchは、デフォルトでポート9200でリクエストを受け入れます。簡単なcurlリクエストで実行されていることを確認できます(またはブラウザで開きます):

curl https://localhost:9200

API

Elasticsearchは、考えられるすべてのタイプのタスクに対して対話するための一連のRESTAPIを提供します。たとえば、JSONコンテンツタイプでPOSTリクエストを実行して、ドキュメントを作成するとします。

curl -X POST https://localhost:9200/my-index/_doc \
  -H 'Content-Type: application/json' \
  -d '{"title": "Banana Cake"}'

この場合、 my-index はインデックスの名前です(インデックスが存在しない場合は、自動的に作成されます)。

_doc はシステムルートです(すべてのシステムルートはアンダースコアで始まります)。

APIを操作する方法は複数あります。

  1. curlの使用 コマンドラインから(jqが便利な場合があります)。
  2. JSONをきれいに印刷するための拡張機能を使用して、ブラウザからGETクエリを実行します。
  3. Kibanaをインストールし、開発ツールコンソールを使用するのが私のお気に入りの方法です。
  4. 最後に、いくつかの優れたChrome拡張機能もあります。

この記事のために、どちらを選択するかは重要ではありません。とにかく、APIと直接対話することはありません。代わりに、内部でRESTAPIと通信するgemを使用します。

新しいアプリを起動する

アイデアは、26K以上の曲の公開データセットを使用して歌詞アプリケーションを作成することです。各曲には、タイトル、アーティスト、ジャンル、テキストの歌詞フィールドがあります。全文検索にはElasticsearchを使用します。

簡単なRailsアプリケーションを作成することから始めましょう:

rails new songs_api --api -d postgresql

APIとしてのみ使用するため、-apiを提供します 使用するミドルウェアのセットを制限するフラグ。

アプリの足場を作りましょう:

bin/rails generate scaffold Song title:string artist:string genre:string lyrics:text

それでは、移行を実行してサーバーを起動しましょう。

bin/rails db:create db:migrate
bin/rails server

その後、GETエンドポイントが機能することを確認します。

curl https://localhost:3000/songs

これにより空の配列が返されますが、まだデータがないので不思議ではありません。

Elasticsearchの紹介

Elasticsearchをミックスに追加しましょう。そのためには、elasticsearch-modelgemが必要になります。これは、Railsモデルとうまく統合できる公式のElasticsearchgemです。

Gemfileに以下を追加します :

gem 'elasticsearch-model'

デフォルトでは、ローカルホストのポート9200に接続します。これは私たちにぴったりですが、それを変更したい場合は、

でクライアントを初期化できます。
Song.__elasticsearch__.client = Elasticsearch::Client.new host: 'myserver.com', port: 9876

次に、Elasticsearchでモデルをインデックスに登録できるようにするには、2つのことを行う必要があります。まず、マッピングを準備する必要があります(これは、基本的にElasticsearchにデータ構造を通知します)。次に、検索リクエストを作成する必要があります。私たちの宝石は両方を行うことができるので、それを使用する方法を見てみましょう。

Elastisearch関連のコードを別のモジュールに保持することは常に良い考えです。そのため、 app / models / concerns / searchable.rbで懸念事項を作成しましょう。 追加

# app/models/concerns/searchable.rb

module Searchable
  extend ActiveSupport::Concern

  included do
    include Elasticsearch::Model
    include Elasticsearch::Model::Callbacks

    mapping do
      # mapping definition goes here
    end

    def self.search(query)
      # build and run search
    end
  end
end

単なるスケルトンですが、ここで開梱するものがあります。

最初で最も重要なことは、 Elasticsearch ::Modelです。 、ESと対話するためのいくつかの機能を追加します。 Elasticsearch ::Model ::Callbacks モジュールは、レコードを更新するときに、Elasticsearchのデータを自動的に更新することを保証します。 マッピング ブロックは、Elasticsearchインデックスマッピングを配置する場所です。これは、Elasticsearchに格納されるフィールドと、それらが持つべきタイプを定義します。最後に、 searchがあります Elasticsearchで曲の歌詞を実際に検索するために使用する方法。私たちが使用している宝石は、 searchを提供します Song.search( "genesis")のような単純なクエリで使用できるメソッド 、ただし、クエリDSLを使用して構築されたより複雑な検索クエリで使用します(詳細は後で説明します)。

モデルクラスに懸念事項を含めることを忘れないでください:

# /app/models/song.rb

class Song < ApplicationRecord
  include Searchable
end
マッピング

Elasticsearchでは、マッピングはリレーショナルデータベースのスキーマ定義のようなものです。保存したいドキュメントの構造を説明します。通常のリレーショナルデータベースとは異なり、マッピングを事前に定義する必要はありません。Elasticsearchは、タイプを推測するために最善を尽くします。それでも、驚きはしたくないので、事前にマッピングを明示的に定義します。

マッピングは、 PUT / my-index / _mappingを使用してRESTエンドポイントを介して更新できます GET / my-index / _mappingを介して読み取ります 、ただし、 elasticsearch gemはそれを抽象化するので、必要なのはマッピングを提供することだけです。 ブロック:

# app/models/concerns/searchable.rb

mapping do
  indexes :artist, type: :text
  indexes :title, type: :text
  indexes :lyrics, type: :text
  indexes :genre, type: :keyword
end

artistにインデックスを付けます 、 title 、および歌詞 テキストタイプを使用するフィールド。これは、全文検索で索引付けされる唯一のタイプです。 ジャンルの場合 、キーワードタイプを使用します。これは、正確な値でフィルタリングされた理想的な検索です。

次に、 bin / rails consoleを使用してRailsコンソールを実行します。 次に実行します

Song.__elasticsearch__.create_index!

これにより、Elasticsearchにインデックスが作成されます。 __ elasticsearch __ オブジェクトはElasticsearchの世界への門であり、Elasticsearchとやり取りするための便利なメソッドがたくさん詰まっています。

データのインポート

レコードを作成するたびに、Elasticsearchにデータが自動的に送信されます。そこで、歌詞を含むデータセットをダウンロードして、アプリにインポートします。まず、このリンクからダウンロードします( Creative Commons Attribution4.0Internationalライセンスのデータセット )。このCSVファイルには26,000を超えるレコードが含まれており、以下のコードを使用してデータベースとElasticsearchにインポートします。

require 'csv'

class Song < ApplicationRecord
  include Searchable

  def self.import_csv!
    filepath = "/path/to/your/file/tcc_ceds_music.csv"
    res = CSV.parse(File.read(filepath), headers: true)
    res.each_with_index do |s, ind|
      Song.create!(
        artist: s["artist_name"],
        title: s["track_name"],
        genre: s["genre"],
        lyrics: s["lyrics"]
      )
    end
  end
end

Railsコンソールを開き、 Song.import_csv!を実行します (これには少し時間がかかります)。または、データを一括でインポートすることもできます。これははるかに高速ですが、この場合は、PostgreSQLデータベースとElasticsearchにレコードを作成する必要があります。

インポートが完了すると、検索できる歌詞がたくさんあります。

データの検索

elasticsearch-model gemはsearchを追加します すべてのインデックス付きフィールドを検索できるようにするメソッド。検索可能な懸念事項に使用しましょう:

# app/models/concerns/searchable.rb

# ...
def self.search(query)
  self.__elasticsearch__.search(query)
end
# ...

Railsコンソールを開き、 res =Song.search('genesis')を実行します。 。応答オブジェクトには、リクエストにかかった時間、使用されたノードなど、多くのメタ情報が含まれています。 res.response ["hits"] ["hits"]> 。

コントローラのindexを変更しましょう 代わりにESを照会する方法。

# app/controllers/songs_controller.rb

def index
  query = params["query"] || ""
  res = Song.search(query)
  render json: res.response["hits"]["hits"]
end

これで、ブラウザにロードするか、curl http:// localhost:3000 / songs?query =genesisを使用して試すことができます。 。応答は次のようになります:


[
  {
  "_index": "songs",
  "_type": "_doc",
  "_id": "22676",
  "_score": 12.540506,
  "_source": {
    "id": 22676,
    "title": "genesis",
    "artist": "grimes",
    "genre": "pop",
    "lyrics": "heart know heart ...",
    "created_at": "...",
    "updated_at": "..."
    }
  },
...
]

ご覧のとおり、実際のデータは _sourceの下に返されます。 キー、他のフィールドはメタデータであり、その中で最も重要なのは _scoreです。 ドキュメントが特定の検索にどのように関連しているかを示します。すぐにわかりますが、最初にクエリの作成方法を学びましょう。

クエリDSL

ElasticsearchクエリDSLは、複雑なクエリを構築する方法を提供し、rubyコードからも使用できます。たとえば、検索方法を変更して、アーティストフィールドのみを検索してみましょう。

# app/models/concerns/searchable.rb

module Searchable
  extend ActiveSupport::Concern

  included do
    # ...

    def self.search(query)
      params = {
        query: {
          match: {
            artist: query,
          },
        },
      }

      self.__elasticsearch__.search(params)
    end
  end
end

query-match構文を使用すると、特定のフィールド(この場合はアーティスト)のみを検索できます。ここで、「genesis」を使用して曲を再度クエリすると、 http:// localhost:3000 / songs?query =genesisを読み込んで試してみてください。 )、バンド「Genesis」の曲のみを取得し、タイトルに「genesis」が含まれる曲は取得しません。多くの場合、複数のフィールドをクエリする場合は、マルチマッチクエリを使用できます。

# app/models/concerns/searchable.rb

def self.search(query)
  params = {
    query: {
      multi_match: {
        query: query, 
        fields: [ :title, :artist, :lyrics ] 
      },
    },
  }

  self.__elasticsearch__.search(params)
end
フィルタリング

たとえば、ロックソングの中だけを検索したい場合はどうなりますか?次に、ジャンルでフィルタリングする必要があります!これにより、検索が少し複雑になりますが、心配しないでください。すべてを段階的に説明します。

  def self.search(query, genre = nil)
    params = {
      query: {
        bool: {
          must: [
            {
              multi_match: {
                query: query, 
                fields: [ :title, :artist, :lyrics ] 
              }
            },
          ],
          filter: [
            {
              term: { genre: genre }
            }
          ]
        }
      }
    }

    self.__elasticsearch__.search(params)
  end

最初の新しいキーワードはboolです。これは、複数のクエリを1つに結合する方法にすぎません。この例では、 mustを組み合わせています およびfilter 。最初のもの( must )スコアに貢献し、以前に使用したものと同じクエリが含まれています。 2つ目( filter )はスコアに寄与しません。それは単にそれが言うことをします:クエリに一致しないドキュメントを除外します。レコードをジャンルでフィルタリングしたいので、クエリという用語を使用します。

filter-termに注意することが重要です 組み合わせは、全文検索とは何の関係もありません。これは、 WHERE と同じように、正確な値による通常のフィルターです。 句はSQLで機能します( WHERE genre ='rock' )。 termの使い方を知っておくとよいでしょう フィルタリングしますが、ここでは必要ありません。

スコアリング

検索結果は_scoreの順に並べられています これは、アイテムが特定の検索にどのように関連しているかを示しています。スコアが高いほど、ドキュメントの関連性が高くなります。 genesisという単語を検索したときにお気づきかもしれません。 、最初にポップアップした結果はグライムスの曲でしたが、実際にはジェネシスバンドにもっと興味がありました。では、アーティストの分野にもっと注意を払うようにスコアリングメカニズムを変更することはできますか?はい、できますが、そのためには、最初にクエリを微調整する必要があります:

  def self.search(query)
    params = {
      query: {
        bool: {
          should: [
            { match: { title: query }},
            { match: { artist: query }},
            { match: { lyrics: query }},
          ],
        }
      },
    }

    self.__elasticsearch__.search(params)
  end

このクエリは、boolキーワードを使用していることを除いて、基本的に前のクエリと同じです。これは、複数のクエリを1つに結合する方法にすぎません。 shouldを使用します 、3つのクエリが個別に含まれています(フィールドごとに1つ)。これらは基本的に論理ORを使用して結合されます。 mustを使用する場合 代わりに、論理積を使用して結合されます。フィールドごとに個別の一致が必要なのはなぜですか?これは、特定のクエリのスコアを乗算する係数であるブーストプロパティを指定できるようになったためです。

  def self.search(query)
    params = {
      query: {
        bool: {
          should: [
            { match: { title: query }},
            { match: { artist: { query: query, boost: 5 } }},
            { match: { lyrics: query }},
          ],
        }
      },
    }

    self.__elasticsearch__.search(params)
  end

他の条件が同じであれば、クエリがアーティストと一致する場合、スコアは5倍高くなります。 genesisをお試しください http:// localhost:3000 / songs?query =genesis を使用して、もう一度クエリを実行します 、そしてあなたはジェネシスバンドの曲が最初に来るのを見るでしょう。甘い!

ハイライト

Elasticsearchのもう1つの便利な機能は、ドキュメント内の一致を強調表示できることです。これにより、ユーザーは特定の結果が検索に表示された理由をよりよく理解できます。

HTMLには、そのための特別なHTMLタグがあり、Elasticsearchはそれを自動的に追加できます。

searchable.rbを開いてみましょう もう一度懸念し、新しいキーワードを追加します:

def self.search(query)
  params = {
    query: {
      bool: {
        should: [
          { match: { title: query }},
          { match: { artist: { query: query, boost: 5 } }},
          { match: { lyrics: query }},
        ],
      }
    },
    highlight: { fields: { title: {}, artist: {}, lyrics: {} } }
  }

  self.__elasticsearch__.search(params)
end

新しいハイライト fieldは、強調表示するフィールドを指定します。それらすべてを選択します。ここで、 http:// localhost:3000 / query =genesisをロードすると 、 emでラップされた一致するフレーズを持つドキュメントフィールドを含む「highlight」と呼ばれる新しいフィールドが表示されます。 タグ。

ハイライトの詳細については、公式ガイドを参照してください。

あいまいさ

了解しました。誤ってbenesisと書いた場合はどうでしょうか。 genesisの代わりに ?これは結果を返しませんが、Elasticsearchにうるさくなく、あいまい検索を許可するように指示できるため、 genesisが表示されます。 結果も。

これがその方法です。アーティストクエリを{match:{artist:{query:query、boost:5}}}から変更するだけです。 to {match:{artist:{query:query、boost:5、fuzziness: "AUTO"}}} 。正確なあいまいさのメカニズムを構成できます。詳細については、公式ドキュメントを参照してください。

次はどこへ?

この記事で、Elasticsearchが強力なツールであり、重要な検索を実装する必要がある場合に使用でき、使用する必要があることを確信していただければ幸いです。詳細を確認する準備ができている場合は、次のリンクをご覧ください。

リソース
  • Elasticsearchの公式リファレンス
  • Rubyの宝石
  • レールの宝石
  • 実践的な知識が満載のとても素敵な本
  • オートコンプリートの構築
代替宝石
  • サーチキック
  • 歯ごたえ

  1. RailsでのTailwindCSSの使用

    CSSは魔法のようですが、時間がかかります。美しく、機能的で、アクセスしやすいサイトを使用するのは楽しいことですが、独自のCSSを作成するのは大変です。 Bootstrapなどの多くのCSSライブラリは近年爆発的に増加しており、Tailwindは2021年にパックをリードしています。 RailsにはTailwindが付属していませんが、この記事では、TailwindCSSを新しいRubyon Railsプロジェクトに追加する方法を説明します。これにより、設計の実装にかかる時間を節約できます。また、Tailwindのユーティリティクラスを使用した設計のウォークスルーも行います。このチュートリア

  2. Rails5でのAngularの使用

    あなたは前にその話を聞いたことがあります。分散型で完全に機能するバックエンドAPIと、通常のツールセットで作成されたフロントエンドで実行されているアプリケーションがすでにあります。 次に、Angularに移動します。または、AngularをRailsプロジェクトと統合する方法を探しているだけかもしれません。これは、この方法を好むためです。私たちはあなたを責めません。 このようなアプローチを使用すると、両方の世界を活用して、たとえばRailsとAngularのどちらの機能を使用してフォーマットするかを決定できます。 構築するもの 心配する必要はありません。このチュートリアルは、この目的のた