Computer >> Máy Tính >  >> Lập trình >> Ruby

Tìm kiếm toàn văn với Elasticsearch trong Rails

Elasticsearch là một trong những công cụ tìm kiếm phổ biến nhất hiện có. Trong số nhiều công ty lớn yêu thích và tích cực sử dụng nó trong sản xuất của họ, có những người khổng lồ như Netflix, Medium, GitHub.

Elasticsearch rất mạnh mẽ, với các trường hợp sử dụng chính là tìm kiếm toàn văn bản, nhật ký thời gian thực và phân tích bảo mật.

Thật không may, Elasticsearch không nhận được nhiều sự chú ý từ cộng đồng Rails, vì vậy bài viết này cố gắng thay đổi điều này với hai mục tiêu:giới thiệu cho người đọc các khái niệm Elasticsearch và hướng dẫn cách sử dụng nó với Ruby on Rails.

Bạn có thể tìm thấy mã nguồn của một dự án mẫu mà chúng tôi sẽ xây dựng ở đây. Lịch sử cam kết ít nhiều tương ứng với thứ tự của các phần trong bài viết này.

Giới thiệu

Từ một góc nhìn rộng hơn, Elasticsearch là một công cụ tìm kiếm

  • được xây dựng dựa trên Apache Lucene;
  • lưu trữ và lập chỉ mục hiệu quả các tài liệu JSON;
  • là mã nguồn mở;
  • cung cấp một bộ REST API để tương tác với nó;
  • theo mặc định không có bảo mật (bất kỳ ai cũng có thể truy vấn nó thông qua các điểm cuối công khai);
  • chia tỷ lệ theo chiều ngang khá tốt.

Hãy cùng xem nhanh một số khái niệm cơ bản.

Với Elasticsearch, chúng tôi đưa tài liệu vào các chỉ mục, sau đó được truy vấn dữ liệu.

Một chỉ mục tương tự như một bảng trong cơ sở dữ liệu quan hệ; đó là một cửa hàng nơi chúng tôi đặt tài liệu (hàng) mà sau này có thể được truy vấn.

Một tài liệu là một tập hợp các trường (tương tự như một hàng trong cơ sở dữ liệu quan hệ).

Một ánh xạ giống như định nghĩa lược đồ trong cơ sở dữ liệu quan hệ. Ánh xạ có thể được xác định rõ ràng hoặc đoán bởi Elasticsearch tại thời điểm chèn; tốt hơn hết là xác định trước ánh xạ chỉ mục.

Với điều đó được bảo hiểm, bây giờ chúng ta hãy thiết lập môi trường của chúng ta.

Cài đặt Elasticsearch

Cách dễ nhất để cài đặt Elasticsearch trên macOS là sử dụng brew:

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

Thay vào đó, chúng tôi có thể chạy nó qua 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

Đối với các tùy chọn khác, vui lòng tham khảo tài liệu tham khảo chính thức.

Elasticsearch chấp nhận các yêu cầu trên cổng 9200 theo mặc định. Bạn có thể kiểm tra xem nó có đang chạy hay không bằng một yêu cầu curl đơn giản (hoặc mở nó trong trình duyệt):

curl https://localhost:9200

API

Elasticsearch cung cấp một tập hợp các API REST để tương tác với mọi loại tác vụ có thể. Ví dụ:giả sử chúng tôi chạy một yêu cầu ĐĂNG với kiểu nội dung JSON để tạo tài liệu:

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

Trong trường hợp này, my-index là tên của một chỉ mục (nếu nó không có, nó sẽ được tạo tự động).

_doc là một tuyến hệ thống (tất cả các tuyến hệ thống đều bắt đầu bằng dấu gạch dưới).

Có nhiều cách để chúng ta có thể tương tác với các API.

  1. Sử dụng curl từ dòng lệnh (bạn có thể thấy jq hữu ích).
  2. Chạy các truy vấn GET từ trình duyệt bằng một số tiện ích mở rộng cho JSON in ấn đẹp.
  3. Cài đặt Kibana và sử dụng bảng điều khiển Dev Tools, đây là cách yêu thích của tôi.
  4. Cuối cùng, cũng có một số tiện ích mở rộng tuyệt vời của Chrome.

Vì lợi ích của bài viết này, không quan trọng bạn chọn cái nào — dù sao thì chúng tôi sẽ không tương tác trực tiếp với các API. Thay vào đó, chúng tôi sẽ sử dụng một viên đá quý, nó nói chuyện với API REST.

Bắt đầu một ứng dụng mới

Ý tưởng là tạo một ứng dụng lời bài hát bằng cách sử dụng tập dữ liệu công khai gồm 26K + bài hát. Mỗi bài hát có tiêu đề, nghệ sĩ, thể loại và trường lời bài hát văn bản. Chúng tôi sẽ sử dụng Elasticsearch để tìm kiếm toàn văn.

Hãy bắt đầu bằng cách tạo một ứng dụng Rails đơn giản:

rails new songs_api --api -d postgresql

Vì chúng tôi sẽ chỉ sử dụng nó làm API nên chúng tôi cung cấp --api cờ để giới hạn bộ phần mềm trung gian được sử dụng.

Hãy xây dựng ứng dụng của chúng tôi:

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

Bây giờ, hãy chạy quá trình di chuyển và khởi động máy chủ:

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

Sau đó, chúng tôi xác minh rằng điểm cuối GET hoạt động:

curl https://localhost:3000/songs

Điều này trả về một mảng trống, không có gì lạ vì chưa có dữ liệu.

Giới thiệu Elasticsearch

Hãy thêm Elasticsearch vào hỗn hợp. Để làm như vậy, chúng ta sẽ cần đến đá quý mô hình tìm kiếm đàn hồi. Đó là một viên ngọc Elasticsearch chính thức tích hợp độc đáo với các mô hình Rails.

Thêm phần sau vào Gemfile của bạn :

gem 'elasticsearch-model'

Theo mặc định, nó sẽ kết nối với cổng 9200 trên localhost, điều này hoàn toàn phù hợp với chúng tôi, nhưng nếu bạn muốn thay đổi điều đó, bạn có thể khởi tạo máy khách bằng

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

Tiếp theo, để Elasticsearch có thể lập chỉ mục mô hình của chúng ta, chúng ta cần làm hai việc. Đầu tiên, chúng ta cần chuẩn bị một ánh xạ (về cơ bản là cho Elasticsearch biết về cấu trúc dữ liệu của chúng ta) và thứ hai, chúng ta nên xây dựng một yêu cầu tìm kiếm. Đá quý của chúng tôi có thể làm được cả hai, vì vậy hãy xem cách sử dụng nó.

Luôn luôn là một ý tưởng hay nếu giữ mã liên quan đến Elastisearch trong một mô-đun riêng biệt, vì vậy hãy tạo mối quan tâm tại app/models/concerns/searchable.rb và thêm

# 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

Mặc dù nó chỉ là một bộ xương, vẫn có thứ để giải nén ở đây.

Điều đầu tiên và quan trọng nhất là Elasticsearch::Model , bổ sung một số chức năng để tương tác với ES. Elasticsearch::Model::Callbacks mô-đun đảm bảo rằng khi chúng tôi cập nhật bản ghi, nó sẽ tự động cập nhật dữ liệu trong Elasticsearch. Ánh xạ mapping khối là nơi chúng tôi đặt ánh xạ chỉ mục Elasticsearch, xác định những trường nào sẽ được lưu trữ trong Elasticsearch và loại chúng nên có. Cuối cùng, có một search phương pháp mà chúng tôi sẽ sử dụng để thực sự tìm kiếm Elasticsearch cho lời bài hát. Đá quý chúng tôi đang sử dụng cung cấp một search phương thức có thể được sử dụng với một truy vấn đơn giản như Song.search("genesis”) , nhưng chúng tôi sẽ sử dụng nó với một truy vấn tìm kiếm phức tạp hơn được tạo bằng truy vấn DSL (sẽ nói thêm về điều đó sau).

Đừng quên bao gồm mối quan tâm trong lớp mô hình của chúng tôi:

# /app/models/song.rb

class Song < ApplicationRecord
  include Searchable
end

Ánh xạ

Trong Elasticsearch, ánh xạ giống như một định nghĩa lược đồ trong cơ sở dữ liệu quan hệ. Chúng tôi mô tả cấu trúc của các tài liệu mà chúng tôi muốn lưu trữ. Không giống như một cơ sở dữ liệu quan hệ điển hình, chúng tôi không phải xác định trước ánh xạ của mình:Elasticsearch sẽ cố gắng hết sức để đoán loại cho chúng tôi. Tuy nhiên, vì chúng tôi không muốn có bất kỳ sự ngạc nhiên nào, chúng tôi sẽ xác định rõ ràng việc lập bản đồ của chúng tôi trước.

Ánh xạ có thể được cập nhật thông qua điểm cuối REST bằng cách sử dụng PUT /my-index/_mapping và đọc qua GET /my-index/_mapping , nhưng elasticsearch gem tóm tắt cho chúng tôi, vì vậy tất cả những gì chúng tôi cần làm là cung cấp ánh xạ mapping khối:

# app/models/concerns/searchable.rb

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

Chúng tôi sẽ lập chỉ mục artist , titlelyrics các trường sử dụng kiểu văn bản. Đây là loại duy nhất được lập chỉ mục cho tìm kiếm toàn văn. Đối với genre , chúng tôi sẽ sử dụng loại từ khóa, đây là một tìm kiếm lý tưởng được lọc theo một giá trị chính xác.

Bây giờ hãy chạy bảng điều khiển rails với bin/rails console và sau đó chạy

Song.__elasticsearch__.create_index!

Điều này sẽ tạo chỉ mục của chúng tôi trong Elasticsearch. __elasticsearch__ đối tượng là cánh cổng của chúng ta vào thế giới Elasticsearch, được đóng gói với rất nhiều phương pháp hữu ích để tương tác với Elasticsearch.

Nhập dữ liệu

Mỗi khi chúng tôi tạo một bản ghi, nó sẽ tự động gửi dữ liệu đến Elasticsearch. Vì vậy, chúng tôi sẽ tải xuống một tập dữ liệu với lời bài hát và nhập nó vào ứng dụng của chúng tôi. Trước tiên, hãy tải xuống từ liên kết này (tập dữ liệu theo Creative Commons Attribution 4.0 International license ). Tệp CSV này chứa hơn 26.000 bản ghi, chúng tôi sẽ nhập vào cơ sở dữ liệu của mình và Elasticsearch với mã bên dưới:

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

Mở bảng điều khiển rails và chạy Song.import_csv! (Điều này sẽ mất một thời gian). Ngoài ra, chúng tôi có thể nhập dữ liệu hàng loạt, nhanh hơn nhiều, nhưng trong trường hợp này, chúng tôi muốn đảm bảo rằng chúng tôi tạo các bản ghi trong cơ sở dữ liệu PostgreSQL và Elasticsearch.

Khi quá trình nhập kết thúc, chúng tôi hiện có rất nhiều lời bài hát mà chúng tôi có thể tìm kiếm.

Tìm kiếm dữ liệu

elasticsearch-model gem thêm một search phương pháp cho phép chúng tôi tìm kiếm trong số tất cả các trường được lập chỉ mục. Hãy sử dụng nó trong mối quan tâm có thể tìm kiếm của chúng tôi:

# app/models/concerns/searchable.rb

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

Mở bảng điều khiển rails và chạy res = Song.search('genesis') . Đối tượng phản hồi chứa nhiều thông tin meta:yêu cầu mất bao nhiêu thời gian, những nút nào đã được sử dụng, v.v. Chúng tôi quan tâm đến lượt truy cập, tại res.response["hits"]["hits"] .

Hãy thay đổi index của bộ điều khiển của chúng tôi để truy vấn ES thay thế.

# app/controllers/songs_controller.rb

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

Bây giờ chúng ta có thể thử tải nó trong trình duyệt hoặc sử dụng curl https://localhost:3000/songs?query=genesis . Câu trả lời sẽ giống như sau:


[
  {
  "_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": "..."
    }
  },
...
]

Như bạn có thể thấy, dữ liệu thực tế được trả về trong _source khóa, các trường khác là siêu dữ liệu, trong đó quan trọng nhất là _score hiển thị cách tài liệu có liên quan cho việc tìm kiếm cụ thể. Chúng ta sẽ sớm làm được điều đó, nhưng trước tiên hãy học cách tạo truy vấn.

DSL truy vấn

Truy vấn Elasticsearch DSL cung cấp một cách để xây dựng các truy vấn phức tạp và chúng ta cũng có thể sử dụng nó từ mã ruby. Ví dụ:hãy sửa đổi phương pháp tìm kiếm để chỉ tìm kiếm trường nghệ sĩ:

# 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

Cấu trúc đối sánh truy vấn cho phép chúng tôi chỉ tìm kiếm một trường cụ thể (trong trường hợp này là nghệ sĩ). Bây giờ, nếu chúng ta truy vấn lại các bài hát bằng "genesis" (hãy thử bằng cách tải https://localhost:3000/songs?query=genesis ), chúng tôi sẽ chỉ nhận được các bài hát của ban nhạc "Genesis", và không nhận được các bài hát có "genesis" trong tiêu đề của họ. Nếu chúng ta muốn truy vấn nhiều trường, thường xảy ra trường hợp này, chúng ta có thể sử dụng truy vấn nhiều kết hợp:

# app/models/concerns/searchable.rb

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

  self.__elasticsearch__.search(params)
end

Lọc

Điều gì sẽ xảy ra nếu chúng ta chỉ muốn tìm kiếm trong số các bài hát rock? Sau đó, chúng ta cần lọc theo thể loại! Điều này sẽ làm cho tìm kiếm của chúng tôi phức tạp hơn một chút, nhưng đừng lo lắng — chúng tôi sẽ giải thích mọi thứ từng bước!

  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

Từ khóa mới đầu tiên là bool, chỉ là một cách để kết hợp nhiều truy vấn thành một. Trong trường hợp của chúng tôi, chúng tôi đang kết hợp mustfilter . Cái đầu tiên (must ) đóng góp vào điểm số và chứa cùng một truy vấn mà chúng tôi đã sử dụng trước đây. Cái thứ hai (filter ) không đóng góp vào điểm số, nó chỉ thực hiện những gì nó nói:lọc ra các tài liệu không phù hợp với truy vấn. Chúng tôi muốn lọc hồ sơ của mình theo thể loại, vì vậy chúng tôi sử dụng cụm từ truy vấn.

Điều quan trọng cần lưu ý là filter-term kết hợp không liên quan gì đến tìm kiếm toàn văn. Nó chỉ là một bộ lọc thông thường theo giá trị chính xác, giống như cách mà WHERE mệnh đề hoạt động trong SQL (WHERE genre = 'rock' ). Thật tốt khi biết cách sử dụng term lọc, nhưng chúng tôi sẽ không cần nó ở đây.

Ghi điểm

Các kết quả tìm kiếm được sắp xếp theo _score cho thấy một mục có liên quan như thế nào đối với một tìm kiếm cụ thể. Điểm càng cao chứng tỏ tài liệu càng phù hợp. Bạn có thể đã nhận thấy rằng khi chúng tôi tìm kiếm từ genesis , kết quả đầu tiên xuất hiện là bài hát của Grimes, trong khi tôi thực sự hứng thú hơn với ban nhạc Genesis. Vì vậy, chúng ta có thể thay đổi cơ chế tính điểm để quan tâm hơn đến lĩnh vực nghệ sĩ? Có, chúng tôi có thể, nhưng để làm được điều đó, trước tiên chúng tôi cần điều chỉnh truy vấn của mình:

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

    self.__elasticsearch__.search(params)
  end

Truy vấn này về cơ bản tương đương với truy vấn trước đây ngoại trừ việc nó đang sử dụng từ khóa bool, đây chỉ là một cách để kết hợp nhiều truy vấn thành một. Chúng tôi sử dụng should , chứa ba truy vấn riêng biệt (một truy vấn cho mỗi trường):về cơ bản chúng được kết hợp bằng cách sử dụng OR logic. Nếu chúng ta sử dụng must thay vào đó, chúng sẽ được kết hợp bằng cách sử dụng hợp lý AND. Tại sao chúng ta cần một đối sánh riêng cho mỗi trường? Đó là bởi vì bây giờ chúng ta có thể chỉ định thuộc tính boost, là hệ số nhân điểm từ truy vấn cụ thể:

  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

Những thứ khác bằng nhau, điểm của chúng tôi sẽ cao hơn năm lần miễn là truy vấn phù hợp với nghệ sĩ. Hãy thử genesis truy vấn lại, với https://localhost:3000/songs?query=genesis , và bạn sẽ thấy các bài hát của ban nhạc Genesis xuất hiện đầu tiên. Ngọt ngào!

Đánh dấu

Một tính năng hữu ích khác của Elasticsearch là có thể đánh dấu kết quả phù hợp trong tài liệu, cho phép người dùng hiểu rõ hơn tại sao một kết quả cụ thể lại xuất hiện trong tìm kiếm.

Trong HTML, có một thẻ HTML đặc biệt cho thẻ đó và Elasticsearch có thể tự động thêm thẻ đó.

Hãy mở searchable.rb quan tâm lại và thêm một từ khóa mới:

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

highlight mới trường chỉ định trường nào sẽ được đánh dấu. Chúng tôi chọn tất cả chúng. Bây giờ, nếu chúng ta tải https://localhost:3000/query=genesis , chúng ta sẽ thấy một trường mới được gọi là "tô sáng" chứa các trường tài liệu với các cụm từ phù hợp được bao bọc trong em thẻ.

Để biết thêm về cách đánh dấu, vui lòng tham khảo hướng dẫn chính thức.

Fuzziness

Được rồi, điều gì sẽ xảy ra nếu chúng tôi viết nhầm benesis thay vì genesis ? Điều này sẽ không trả về bất kỳ kết quả nào, nhưng chúng tôi có thể nói với Elasticsearch ít kén chọn hơn và cho phép tìm kiếm mờ, vì vậy nó sẽ hiển thị genesis kết quả.

Đây là cách nó được thực hiện. Chỉ cần thay đổi truy vấn nghệ sĩ từ { match: { artist: { query: query, boost: 5 } }} thành { match: { artist: { query: query, boost: 5, fuzziness: "AUTO" } }} . Cơ chế mờ chính xác có thể được cấu hình. Vui lòng tham khảo tài liệu chính thức để biết thêm chi tiết.

Tiếp theo ở đâu?

Tôi hy vọng bài viết này đã thuyết phục bạn rằng Elasticsearch là một công cụ mạnh mẽ có thể và nên được sử dụng khi bạn cần thực hiện một tìm kiếm không tầm thường. Nếu bạn đã sẵn sàng tìm hiểu thêm, đây là một số liên kết hữu ích:

Tài nguyên

  • Tham chiếu Elasticsearch chính thức
  • Viên ngọc Ruby
  • Đá quý Rails
  • Một cuốn sách rất hay chứa nhiều kiến ​​thức thực tế
  • Xây dựng tính năng tự động hoàn thành

Đá quý thay thế

  • Tìm kiếm
  • Chewy