Computer >> 컴퓨터 >  >> 프로그램 작성 >> Ruby

Rails에서 Elasticsearch를 사용한 전체 텍스트 검색

Elasticsearch는 가장 인기 있는 검색 엔진 중 하나입니다. 그것을 사랑하고 프로덕션에 적극적으로 사용하는 많은 대기업 중에는 Netflix, Medium, GitHub와 같은 거물이 있습니다.

Elasticsearch는 전체 텍스트 검색, 실시간 로그 및 보안 분석을 특징으로 하는 주요 사용 사례와 함께 매우 강력합니다.

불행히도 Elasticsearch는 Rails 커뮤니티의 많은 관심을 받지 못하므로 이 기사에서는 독자에게 Elasticsearch 개념을 소개하고 Ruby on Rails와 함께 사용하는 방법을 보여주는 두 가지 목표를 염두에 두고 이를 변경하려고 합니다.

여기에서 빌드할 예제 프로젝트의 소스 코드를 찾을 수 있습니다. 커밋 히스토리는 이 기사의 섹션 순서와 거의 일치합니다.

소개

더 넓은 관점에서 Elasticsearch는 검색 엔진입니다.

  • Apache Lucene을 기반으로 합니다.
  • JSON 문서를 저장하고 효과적으로 인덱싱합니다.
  • 오픈 소스입니다.
  • 상호작용을 위한 REST API 세트를 제공합니다.
  • 기본적으로 보안이 없습니다(누구나 공개 엔드포인트를 통해 쿼리할 수 있음).
  • 가로로 잘 확장됩니다.

몇 가지 기본 개념을 간단히 살펴보겠습니다.

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는 가능한 모든 유형의 작업에 대해 상호 작용할 수 있는 REST API 세트를 제공합니다. 예를 들어 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를 설치하고 Dev Tools 콘솔을 사용합니다.
  4. 마침내 훌륭한 Chrome 확장 프로그램도 있습니다.

이 기사에서는 어떤 것을 선택하든 상관없습니다. 어쨌든 API와 직접 상호 작용하지 않을 것입니다. 대신 내부에서 REST API와 통신하는 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-model gem이 필요합니다. Rails 모델과 잘 통합되는 공식 Elasticsearch 보석입니다.

Gemfile에 다음을 추가하세요. :

gem 'elasticsearch-model'

기본적으로 localhost의 포트 9200에 연결되며 이는 우리에게 완벽하지만 변경하려는 경우 클라이언트를 초기화할 수 있습니다.

Song.__elasticsearch__.client = Elasticsearch::Client.new host: 'myserver.com', port: 9876

다음으로 Elasticsearch에서 모델을 인덱싱할 수 있도록 하려면 두 가지 작업을 수행해야 합니다. 먼저 매핑을 준비해야 하고(기본적으로 Elasticsearch에 데이터 구조에 대해 알려주는) 두 번째로 검색 요청을 구성해야 합니다. 우리의 gem은 두 가지를 모두 수행할 수 있으므로 사용 방법을 살펴보겠습니다.

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::콜백 모듈은 레코드를 업데이트할 때 Elasticsearch의 데이터를 자동으로 업데이트하도록 합니다. 매핑 블록은 Elasticsearch에 저장될 필드와 해당 필드의 유형을 정의하는 Elasticsearch 인덱스 매핑을 넣는 곳입니다. 마지막으로 검색이 있습니다. 실제로 Elasticsearch에서 노래 가사를 검색하는 데 사용할 방법입니다. 우리가 사용하는 gem은 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은 이를 추상화하므로 mapping만 제공하면 됩니다. 차단:

# app/models/concerns/searchable.rb

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

아티스트의 색인을 생성할 것입니다. , 제목 , 및 가사 텍스트 유형을 사용하는 필드. 전체 텍스트 검색에 대해 인덱싱되는 유일한 유형입니다. 장르의 경우 , 정확한 값으로 필터링된 이상적인 검색인 키워드 유형을 사용합니다.

이제 bin/rails console로 rails 콘솔을 실행하세요. 그리고 실행

Song.__elasticsearch__.create_index!

그러면 Elasticsearch에 인덱스가 생성됩니다. __elasticsearch__ object는 Elasticsearch와 상호 작용하는 데 유용한 많은 방법으로 가득 찬 Elasticsearch 세계로 가는 우리의 관문입니다.

데이터 가져오기

레코드를 생성할 때마다 데이터가 자동으로 Elasticsearch로 전송됩니다. 따라서 노래 가사가 포함된 데이터 세트를 다운로드하여 앱으로 가져올 것입니다. 먼저 이 링크에서 다운로드하십시오(Creative Commons Attribution 4.0 International license에 있는 데이터세트). ). 이 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

레일스 콘솔을 열고 Song.import_csv!를 실행합니다. (시간이 좀 걸립니다). 또는 데이터를 대량으로 가져올 수 있는데 훨씬 더 빠르지만 이 경우 PostgreSQL 데이터베이스와 Elasticsearch에 레코드를 생성해야 합니다.

가져오기가 완료되면 검색할 수 있는 많은 가사가 있습니다.

데이터 검색

elasticsearch-model gem은 search를 추가합니다. 모든 인덱싱된 필드 중에서 검색할 수 있는 방법입니다. 검색 가능한 관심사에 사용합시다:

# app/models/concerns/searchable.rb

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

레일스 콘솔을 열고 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 https://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은 복잡한 쿼리를 구성하는 방법을 제공하며 루비 코드에서도 사용할 수 있습니다. 예를 들어, 아티스트 필드만 검색하도록 검색 방법을 수정해 보겠습니다.

# 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

쿼리 일치 구성을 사용하면 특정 필드(이 경우 아티스트)만 검색할 수 있습니다. 이제 "genesis"로 노래를 다시 쿼리하면(https://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입니다. 우리의 경우 must를 결합합니다. 및 필터 . 첫 번째(반드시 ) 점수에 기여하고 이전에 이미 사용한 것과 동일한 쿼리를 포함합니다. 두 번째 것(filter )는 점수에 기여하지 않고 쿼리와 일치하지 않는 문서를 필터링합니다. 장르별로 레코드를 필터링하기 위해 검색어라는 용어를 사용합니다.

filter-term 조합은 전체 텍스트 검색과 관련이 없습니다. WHERE 절은 SQL에서 작동합니다(WHERE 장르 ='rock' ). 용어 사용법을 아는 것이 좋습니다. 필터링하지만 여기서는 필요하지 않습니다.

득점

검색 결과는 _score로 정렬됩니다. 항목이 특정 검색과 어떻게 관련되어 있는지 보여줍니다. 점수가 높을수록 문서의 관련성이 높습니다. genesis라는 단어를 검색했을 때 , 가장 먼저 떠오른 결과는 Grimes의 노래였지만 사실 저는 Genesis 밴드에 더 관심이 있었습니다. 그렇다면 아티스트 필드에 더 많은 관심을 기울이도록 채점 메커니즘을 변경할 수 있습니까? 예, 할 수 있지만 그렇게 하려면 먼저 쿼리를 조정해야 합니다.

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

    self.__elasticsearch__.search(params)
  end

이 쿼리는 여러 쿼리를 하나로 결합하는 방법인 bool 키워드를 사용한다는 점을 제외하고 본질적으로 전자와 동일합니다. 우리는 should를 사용합니다. , 3개의 쿼리가 별도로 포함되어 있습니다(필드당 하나씩). 기본적으로 논리적 OR을 사용하여 결합됩니다. 반드시를 사용하는 경우 대신 논리적 AND를 사용하여 결합됩니다. 필드별로 별도의 일치 항목이 필요한 이유는 무엇입니까? 이는 이제 특정 쿼리의 점수를 곱하는 계수인 boost 속성을 지정할 수 있기 때문입니다.

  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배 더 높아집니다. 제네시스를 사용해 보세요. https://localhost:3000/songs?query=genesis를 사용하여 다시 쿼리합니다. , 그리고 당신은 제네시스 밴드 노래가 먼저 나오는 것을 볼 것입니다. 달콤한!

하이라이트

Elasticsearch의 또 다른 유용한 기능은 문서 내에서 일치하는 항목을 강조 표시할 수 있다는 것입니다. 이를 통해 사용자는 특정 결과가 검색에 나타난 이유를 더 잘 이해할 수 있습니다.

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는 강조 표시되어야 하는 필드를 지정합니다. 우리는 그들 모두를 선택합니다. 이제 https://localhost:3000/query=genesis를 로드하면 , em으로 래핑된 일치 구문이 있는 문서 필드를 포함하는 "highlight"라는 새 필드가 표시되어야 합니다. 태그.

하이라이트에 대한 자세한 내용은 공식 가이드를 참조하세요.

퍼지

좋아요, 실수로 benesis를 작성했다면? 제네시스 대신 ? 이것은 결과를 반환하지 않지만 Elasticsearch에 덜 까다롭고 퍼지 검색을 허용하도록 지시할 수 있으므로 genesis가 표시됩니다. 결과도 마찬가지입니다.

수행 방법은 다음과 같습니다. { match:{ 아티스트:{ query:query, boost:5 } }}에서 아티스트 쿼리를 변경하기만 하면 됩니다. { match:{ 아티스트:{ query:query, boost:5, fuzziness:"AUTO" } }}로 . 정확한 퍼지 역학을 구성할 수 있습니다. 자세한 내용은 공식 문서를 참조하십시오.

다음은 어디로?

이 기사를 통해 Elasticsearch가 중요하지 않은 검색을 구현해야 할 때 사용할 수 있고 사용해야 하는 강력한 도구라는 것을 확신하셨기를 바랍니다. 더 자세히 알아볼 준비가 되셨다면 다음과 같은 유용한 링크가 있습니다.

자원

  • 공식 Elasticsearch 참조
  • 루비 보석
  • The Rails의 보석
  • 실용적인 지식으로 가득 찬 아주 좋은 책
  • 자동 완성 만들기

대체 보석

  • 서치킥
  • 쫄깃함