rails 添加外键_如何在Rails后端中添加功能强大的搜索引擎

欧阳学真
2023-12-01

rails 添加外键

by Domenico Angilletta

通过多梅尼科·安吉列塔(Domenico Angilletta)

In my experience as a Ruby on Rails Developer, I often had to deal with adding search functionality to web applications. In fact, almost all applications I worked on at some point needed search engine capabilities, while many of them had a search engine as the most important core functionality.

以Ruby on Rails开发人员的经验,我经常不得不处理向Web应用程序添加搜索功能的问题。 实际上,几乎在某个时候我从事的所有应用程序都需要搜索引擎功能,而其中许多应用程序都将搜索引擎作为最重要的核心功能。

Many applications we use everyday would be useless without a good search engine at their core. For example, on Amazon, you can find a particular product among the more than 550 million products available on the site in a matter of a few seconds — all thanks to a fulltext search combined with category filters, facets, and a recommendation system.

如果没有良好的搜索引擎,我们每天使用的许多应用程序将毫无用处。 例如,在亚马逊上,您可以在几秒钟内在网站上可用的5.5亿种产品中找到特定的产品,这全都归功于全文搜索,类别过滤器,构面和推荐系统。

On Airbnb, you can search for an apartment by combining a geospatial search with filters on house characteristics, like dimension, price, available dates, and so on.

在Airbnb上,您可以通过将地理空间搜索与房屋特征(例如尺寸,价格,可用日期等)过滤器结合使用来搜索公寓。

And Spotify, Netflix, Ebay, Youtube…all of them rely heavily on a search engine.

还有Spotify,Netflix,Ebay,Youtube ...它们都严重依赖搜索引擎。

In this article, I will describe how to develop a Ruby on Rails 5 API backend with Elasticsearch. According to DB Engines Ranking, Elasticsearch is currently the most popular open source search platform.

在本文中,我将描述如何使用Elasticsearch开发Ruby on Rails 5 API后端。 根据数据库引擎排名 ,Elasticsearch是当前最受欢迎的开源搜索平台。

This article will not go into the details of Elasticsearch and how it compares to its competitors like Sphinx and Solr. Instead, it will be a step-by-step guide on how to implement a JSON API Backend with Ruby on Rails and Elasticsearch, using a Test Driven Development approach.

本文将不讨论Elasticsearch的细节以及它如何与Sphinx和Solr等竞争对手进行比较。 相反,它将是有关如何使用“测试驱动开发”方法使用Ruby on Rails和Elasticsearch来实现JSON API后端的分步指南。

This article will cover:

本文将介绍:

  1. Elasticsearch Setup for test, development, and production enviornments

    用于测试,开发和生产环境的Elasticsearch设置
  2. Ruby on Rails Test Environment Setup

    Ruby on Rails测试环境设置
  3. Model indexing with Elasticsearch

    使用Elasticsearch进行模型索引
  4. Search API endpoint

    搜索API端点

As in my previous article, How to boost your performance with serverless architecture, I will cover everything in a step-by-step tutorial. Then you can try it out yourself and have a simple working example on which to build something more complex.

就像我上一篇文章如何通过无服务器架构提高性能一样 ,我将在分步教程中介绍所有内容。 然后,您可以自己尝试一下,并有一个简单的工作示例在其上构建更复杂的内容。

The example application will be a Movie search engine. It will have a single JSON API endpoint that allows you to make a fulltext search on Movie titles and overviews.

该示例应用程序将是电影搜索引擎。 它将具有一个JSON API端点,使您可以对电影标题和概述进行全文搜索。

1. Elasticsearch设置 (1. Elasticsearch Setup)

Elasticsearch is a distributed, RESTful search and analytics engine capable of solving a growing number of use cases. As the heart of the Elastic Stack, it centrally stores your data so you can discover the expected and uncover the unexpected. — www.elastic.co/products/elasticsearch

Elasticsearch是一个分布式的RESTful搜索和分析引擎,能够解决越来越多的用例。 作为Elastic Stack的核心,它集中存储您的数据,以便您发现期望的数据并发现意外的数据。 — www.elastic.co/products/elasticsearch

According to DB-Engines’ Ranking of Search Engines, Elasticsearch is by far the most popular search engine platform today (as of April 2018). And it has been since the end of 2015, when Amazon announced the launch of AWS Elasticsearch Service, a way to start an Elasticsearch cluster from the AWS Management console.

根据DB-Engines的搜索引擎排名,Elasticsearch是迄今为止(截至2018年4月)最受欢迎的搜索引擎平台。 自2015年底以来, 亚马逊宣布推出AWS Elasticsearch Service ,这是一种从AWS管理控制台启动Elasticsearch集群的方法。

Elasticsearch is opensource. You can download your preferred version from their website and run it wherever you want. While I suggest using the AWS Elasticsearch Service for production environments, I prefer having Elasticsearch running on my local machine for testing and developing.

Elasticsearch是开源的。 您可以从他们的网站下载首选版本,然后在任何地方运行它。 虽然我建议在生产环境中使用AWS Elasticsearch Service,但我更喜欢让Elasticsearch在本地计算机上运行以进行测试和开发。

Let’s begin by downloading the (currently) most recent Elasticsearch Version (6.2.3) and unzip it. Open a terminal and run

首先,下载(当前)最新的Elasticsearch版本(6.2.3)并将其解压缩。 打开终端并运行

$ wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.2.3.zip

$ unzip elasticsearch-6.2.3.zip

Alternatively, you can download Elasticsearch from your browser here and unzip it with your preferred program.

或者,您可以从此处的浏览器下载Elasticsearch,并使用首选程序将其解压缩。

2.测试环境设置 (2. Test Environment Setup)

We are going to build a backend application with Ruby on Rails 5 API. It will have one model that represents Movies. Elasticsearch will index it, and that will be searchable through an API endpoint.

我们将使用Ruby on Rails 5 API构建一个后端应用程序。 它将有一个代表电影的模型。 Elasticsearch将为其编制索引,并且可以通过API端点进行搜索。

First of all, let’s create a new rails application. In the same folder you downloaded Elasticsearch before, run the command for generating a new rails app. If you are new to Ruby on Rails, please refer to this starting guide to setup your environment first.

首先,让我们创建一个新的Rails应用程序。 在您之前下载Elasticsearch的相同文件夹中,运行命令以生成新的Rails应用程序。 如果您不熟悉Ruby on Rails,请首先参考本入门指南来设置您的环境。

$ rails new movies-search --api; cd movies-search

When using the “api” option, all the middleware used primarily for browser applications is not included. Exactly what we want. Read more about it directly on the ruby on rails guide.

使用“ api”选项时,不包括所有主要用于浏览器应用程序的中间件。 正是我们想要的。 直接在ruby on rails指南上阅读有关它的更多信息。

Now let’s add all the Gems we will need. Open your Gemfile and add the following code:

现在,让我们添加所需的所有宝石。 打开您的Gemfile并添加以下代码:

# Gemfile

...
# Elasticsearch integration
gem 'elasticsearch-model'
gem 'elasticsearch-rails'

group :development, :test do
  ...
  # Test Framework
  gem 'rspec'
  gem 'rspec-rails'
end

group :test do
  ...
  # Clean Database between tests
  gem 'database_cleaner'
  # Programmatically start and stop ES for tests
  gem 'elasticsearch-extensions'
end
...

We are adding two Elasticsearch Gems that will provide all necessary methods to index our model and run search queries on it. rspec, rspec-rails, database_cleaner, and elasticsearch-extensions are used for testing.

我们将添加两个Elasticsearch Gems,它们将提供所有必要的方法来为模型建立索引并在其上运行搜索查询。 rspec,rspec-rails,database_cleaner和elasticsearch-extensions用于测试。

After saving your Gemfile, run bundle install to install all added Gems.

保存Gemfile后,运行bundle install安装所有添加的Gems。

Now let’s configure Rspec by running the following command:

现在,通过运行以下命令来配置Rspec:

rails generate rspec:install

This command will create a spec folder and add spec_helper.rb and rails_helper.rb to it. They can be used to customize rspec to your application needs.

此命令将创建一个spec文件夹,并将spec_helper.rbrails_helper.rb添加到其中。 它们可用于自定义rspec以满足您的应用程序需求。

In this case, we will add a DatabaseCleaner block to rails_helper.rb so that each each test will run in an empty database. Moreover we will modify spec_helper.rb in order to start an Elasticsearch test server each time the test suite is started, and shut it down again once the test suite has finished.

在这种情况下,我们将一个DatabaseCleaner块添加到rails_helper.rb中,以便每个测试都将在一个空数据库中运行。 此外,我们将修改spec_helper.rb以便在每次启动测试套件时启动Elasticsearch测试服务器,并在测试套件完成后再次将其关闭。

This solution is based on Rowan Oulton’s article Testing Elasticsearch in Rails. Many claps for him!

该解决方案基于Rowan Oulton的文章“ 在Rails中测试Elasticsearch” 。 为他鼓掌!

Let’s start with DatabaseCleaner. Inside spec/rails_helper.rb add the following code:

让我们从DatabaseCleaner开始。 在spec / rails_helper.rb内部,添加以下代码:

# spec/rails_helper.rb
...
RSpec.configure do |config|
  ...
  
config.before(:suite) do
    DatabaseCleaner.strategy = :transaction
    DatabaseCleaner.clean_with(:truncation)
  end
  
config.around(:each) do |example|
    DatabaseCleaner.cleaning do
      example.run
    end
  end
end

Next, let’s think about the Elasticsearch test server setup. We need to add some configuration files so that Rails knows where to find our Elasticsearch executable. It will also tell it on which port we want it to run, based on the current environment. To do so, add a new configuration yaml to your config folder:

接下来,让我们考虑一下Elasticsearch测试服务器的设置。 我们需要添加一些配置文件,以便Rails知道在哪里可以找到我们的Elasticsearch可执行文件。 它还会根据当前环境告诉它要在哪个端口上运行。 为此,将一个新的配置yaml添加到您的config文件夹中:

# config/elasticsearch.yml

development: &default
  es_bin: '../elasticsearch-6.2.3/bin/elasticsearch'
  host: 'http://localhost:9200'
  port: '9200'
test:
  es_bin: '../elasticsearch-6.2.3/bin/elasticsearch'
  host: 'http://localhost:9250'
  port: '9250'
staging:
  <<: *default
production:
  es_bin: '../elasticsearch-6.2.3/bin/elasticsearch'
  host: 'http://localhost:9400'
  port: '9400'

If you did not create the rails application in the same folder where you downloaded Elasticsearch, or if you are using a different version of Elasticsearch, you will need to adjust the es_bin path here.

如果未在下载Elasticsearch的文件夹中创建Rails应用程序,或者使用的是Elasticsearch的其他版本,则需要在此处调整es_bin路径。

Now add a new file to your initializers folder that will read from the configuration we just added:

现在,将一个新文件添加到您的initializers文件夹中,该文件将从我们刚刚添加的配置中读取:

# config/initializers/elasticsearch.rb

if File.exists?("config/elasticsearch.yml")
   config = YAML.load_file("config/elasticsearch.yml")[Rails.env].symbolize_keys
   Elasticsearch::Model.client = Elasticsearch::Client.new(config)
end

And finally let’s change spec_helper.rb to include the Elasticsearch test setup. This means start and stop an Elasticsearch test server and create/delete Elasticsearch indexes for our Rails model.

最后,我们更改spec_helper.rb以包含Elasticsearch测试设置。 这意味着启动和停止Elasticsearch测试服务器,并为我们的Rails模型创建/删除Elasticsearch索引。

# spec/spec_helper.rb

require 'elasticsearch/extensions/test/cluster'
require 'yaml'

RSpec.configure do |config|
  ...
  # Start an in-memory cluster for Elasticsearch as needed
  es_config = YAML.load_file("config/elasticsearch.yml")["test"]
  ES_BIN = es_config["es_bin"]
  ES_PORT = es_config["port"]
  
config.before :all, elasticsearch: true do
    Elasticsearch::Extensions::Test::Cluster.start(command: ES_BIN, port: ES_PORT.to_i, nodes: 1, timeout: 120)  unless Elasticsearch::Extensions::Test::Cluster.running?(command: ES_BIN, on: ES_PORT.to_i)
  end
  
# Stop elasticsearch cluster after test run
  config.after :suite do
    Elasticsearch::Extensions::Test::Cluster.stop(command: ES_BIN, port: ES_PORT.to_i, nodes: 1) if Elasticsearch::Extensions::Test::Cluster.running?(command: ES_BIN, on: ES_PORT.to_i)
  end
  
# Create indexes for all elastic searchable models
  config.before :each, elasticsearch: true do
    ActiveRecord::Base.descendants.each do |model|
      if model.respond_to?(:__elasticsearch__)
        begin
          model.__elasticsearch__.create_index!
          model.__elasticsearch__.refresh_index!
        rescue => Elasticsearch::Transport::Transport::Errors::NotFound
          # This kills "Index does not exist" errors being written to console
        rescue => e
          STDERR.puts "There was an error creating the elasticsearch index for #{model.name}: #{e.inspect}"
        end
      end
    end
  end
  
# Delete indexes for all elastic searchable models to ensure clean state between tests
  config.after :each, elasticsearch: true do
    ActiveRecord::Base.descendants.each do |model|
      if model.respond_to?(:__elasticsearch__)
        begin
          model.__elasticsearch__.delete_index!
        rescue => Elasticsearch::Transport::Transport::Errors::NotFound
          # This kills "Index does not exist" errors being written to console
        rescue => e
          STDERR.puts "There was an error removing the elasticsearch index for #{model.name}: #{e.inspect}"
        end
      end
    end
  end
  
end

We have defined four blocks:

我们定义了四个块:

  1. a before(:all) block that starts an Elasticsearch test server, unless it is already running

    除非之前已经在运行,否则它将启动Elasticsearch测试服务器的before(:all)块
  2. an after(:suite) block that stops the Elasticsearch test server, if it is running

    一个after(:suite)块,它将停止Elasticsearch测试服务器(如果正在运行)
  3. a before(:each) block that creates a new Elasticsearch index for each model that is configured with Elasticsearch

    一个before(:each)块,该块为使用Elasticsearch配置的每个模型创建一个新的Elasticsearch索引
  4. an after(:each) block that deletes all Elasticsearch indexes

    after(:each)块,该块删除所有Elasticsearch索引

Adding elasticsearch: true ensures that only tests tagged with elasticsearch will run these blocks.

添加elasticsearch:true可以确保只有标记有elasticsearch的测试才能运行这些块。

I find that this setup works great when you run all your tests once, for example before a deploy. On the other hand, if you’re using a test driven development approach and you run your tests very often, then you will probably need to modify this configuration slightly. You do not want to start and stop your Elasticsearch test server at each test run.

我发现当您一次运行所有测试(例如在部署之前)时,此设置非常有用。 另一方面,如果您使用的是测试驱动的开发方法,并且经常运行测试,则可能需要稍微修改此配置。 您不想在每次测试运行时启动和停止Elasticsearch测试服务器。

In this case, you could comment out the after(:suite) block where the Test Server is stopped. You can shut it down manually, or using a script, whenever you don’t need it anymore.

在这种情况下,您可以注释掉测试服务器停止的after(:suite)块。 您可以在不需要的时候手动关闭它,也可以使用脚本关闭它。

require 'elasticsearch/extensions/test/cluster'
es_config = YAML.load_file("config/elasticsearch.yml")["test"]
ES_BIN = es_config["es_bin"]
ES_PORT = es_config["port"]
Elasticsearch::Extensions::Test::Cluster.stop(command: ES_BIN, port: ES_PORT.to_i, nodes: 1)

3.使用Elasticsearch建立模型索引 (3. Model indexing with Elasticsearch)

Now we start implementing our Movie Model with search capabilities. We use a Test Driven Development approach. This means that we write tests first, see them fail, and then write code to make them pass.

现在,我们开始使用搜索功能实现电影模型。 我们使用测试驱动开发方法。 这意味着我们首先编写测试,查看它们是否失败,然后编写代码使它们通过。

First we need to add the movie model which has four attributes: a title (String), an overview (Text), an image_url(String), and an average vote value (Float).

首先,我们需要添加具有四个属性的电影模型:标题(字符串),概述(文本),image_url(String)和平均投票值(浮动)。

$ rails g model Movie title:string overview:text image_url:string vote_average:float

$ rails db:migrate

Now it’s time to add Elasticsearch to our model. Let’s write a test that checks that our model is indexed.

现在是时候将Elasticsearch添加到我们的模型中了。 让我们编写一个测试来检查我们的模型是否已建立索引。

# spec/models/movie_spec.rb
require 'rails_helper'

RSpec.describe Movie, elasticsearch: true, :type => :model do
  it 'should be indexed' do
     expect(Movie.__elasticsearch__.index_exists?).to be_truthy
  end
end

This test will check if an elasticsearch index was created for Movie. Remember that before tests begin, we automatically create an elasticsearch index for all models that respond to the __elasticsearch__ method. That means for all models that include the elasticsearch modules.

该测试将检查是否为Movie创建了Elasticsearch索引。 请记住,在测试开始之前,我们会为响应__elasticsearch__方法的所有模型自动创建一个elasticsearch索引。 这意味着对于所有包含elasticsearch模块的模型。

Run the test to see it fail.

运行测试以查看失败。

bundle exec rspec spec/models/movie_spec.rb

The first time you run this test, you should see that the Elasticsearch Test Server is starting. The test fails, because we didn’t add any Elasticsearch module to our Movie model. Let’s fix that now. Open the model and add the following Elasticsearch to include:

首次运行此测试时,应该看到Elasticsearch Test Server正在启动。 测试失败,因为我们没有将任何Elasticsearch模块添加到我们的Movie模型中。 让我们现在修复它。 打开模型并添加以下Elasticsearch以包括:

# app/models/movie.rb

class Movie < ApplicationRecord
  include Elasticsearch::Model
end

This will add some Elasticsearch methods to our Movie model, like the missing __elasticsearch__ method (which generated the error in the previous test run) and the search method we will use later.

这将为我们的Movie模型添加一些Elasticsearch方法,例如缺少的__elasticsearch__方法(该方法在先前的测试运行中产生了错误)和我们稍后将使用的搜索方法。

Run the test again and see it pass.

再次运行测试,看它通过了。

bundle exec rspec spec/models/movie_spec.rb

Great. We have an indexed movie model.

大。 我们有一个索引电影模型。

By default, Elasticsearch::Model will setup an index with all attributes of the model, automatically inferring their types. Usually this is not what we want. We are now going customize the model index so that it has the following behavior:

默认情况下,Elasticsearch :: Model将使用模型的所有属性设置索引,自动推断其类型。 通常这不是我们想要的。 现在,我们将自定义模型索引,使其具有以下行为:

  1. Only title and overview should be indexed

    仅标题和概述应被索引
  2. Stemming should be used (which means that searching for “actors” should also return movies that contain the text “actor,” and vice-versa)

    应该使用词干(这意味着搜索“演员”也应该返回包含文本“演员”的电影,反之亦然)

We also want our index to be updated each time a Movie is added, updated, or deleted.

我们还希望每次添加,更新或删除电影时都更新索引。

Let’s translate this into tests by adding the following code to movie_spec.rb

通过将以下代码添加到movie_spec.rb ,将其转换为测试

# spec/models/movie_spec.rb
RSpec.describe Movie, elasticsearch: true, :type => :model do
  ...
  
describe '#search' do
    before(:each) do
      Movie.create(
        title: "Roman Holiday",
        overview: "A 1953 American romantic comedy films ...",
        image_url: "wikimedia.com/Roman_holiday.jpg",
        vote_average: 4.0
      )
      Movie.__elasticsearch__.refresh_index!
    end
    it "should index title" do
      expect(Movie.search("Holiday").records.length).to eq(1)
    end
    it "should index overview" do
      expect(Movie.search("comedy").records.length).to eq(1)
    end
    it "should not index image_path" do
      expect(Movie.search("Roman_holiday.jpg").records.length).to eq(0)
    end
    it "should not index vote_average" do
      expect(Movie.search("4.0").records.length).to eq(0)
    end
  end
  
end

We create a Movie before each test, because we configured DatabaseCleaner so that each test is isolated. Movie.__elasticsearch__.refresh_index! is needed to be sure that the new movie record is immediately available for search.

我们在每个测试之前创建一个Movie,因为我们配置了DatabaseCleaner以便隔离每个测试。 电影.__ elasticsearch __。refresh_index! 需要确保新的电影记录可立即用于搜索。

As before, run the test and see it fail.

和以前一样,运行测试并查看失败。

Seems that our movie is not being indexed. That’s because we didn’t yet tell our model what to do when the movie data changes. Thankfully, this can be fixed by adding another module to our Movie model:

似乎我们的电影没有被编入索引。 那是因为我们还没有告诉我们的模型,当电影数据改变时该怎么做。 幸运的是,可以通过在我们的Movie模型中添加另一个模块来解决此问题:

class Movie < ApplicationRecord
  include Elasticsearch::Model
  include Elasticsearch::Model::Callbacks
end

With Elasticsearch::Model::Callbacks, whenever a movie is added, modified, or deleted, its document on Elasticsearch is also updated.

使用Elasticsearch :: Model :: Callbacks,每当添加,修改或删除电影时,其在Elasticsearch上的文档也会被更新。

Let’s see how the test output changes.

让我们看看测试输出如何变化。

Ok. Now the problem is that our search method also returns queries that match on the attributes vote_average and image_url. To fix that we need to configure the Elasticsearch index mapping. So we need to tell Elasticsearch specifically which model attributes to index.

好。 现在的问题是,我们的搜索方法还返回了与属性poll_averageimage_url匹配的查询。 为了解决这个问题,我们需要配置Elasticsearch索引映射。 因此,我们需要专门告诉Elasticsearch要索引哪些模型属性。

# app/models/movie.rb

class Movie < ApplicationRecord
  include Elasticsearch::Model
  include Elasticsearch::Model::Callbacks
  
# ElasticSearch Index
  settings index: { number_of_shards: 1 } do
    mappings dynamic: 'false' do
      indexes :title
      indexes :overview
    end
  end
end

Run the test again and see it pass.

再次运行测试,看它通过了。

Cool. Now let’s add a stemmer so that there is no difference between “actor” and “actors.” As always, we will first write the test and see it fail.

凉。 现在让我们添加一个词干提取器,以便“角色”和“角色”之间没有区别。 与往常一样,我们将首先编写测试并看到它失败。

describe '#search' do
    before(:each) do
      Movie.create(
        title: "Roman Holiday",
        overview: "A 1953 American romantic comedy films ...",
        image_url: "wikimedia.com/Roman_holiday.jpg",
        vote_average: 4.0
      )
      Movie.__elasticsearch__.refresh_index!
    end
    
...

it "should apply stemming to title" do
      expect(Movie.search("Holidays").records.length).to eq(1)
    end
    
it "should apply stemming to overview" do
      expect(Movie.search("film").records.length).to eq(1)
    end
end

Note that we are testing both ways: Holidays should return also Holiday, and Film should also return Films.

请注意,我们在两种方法上都进行了测试:假日也应返回假日,电影也应返回电影。

To make these tests pass again, we need to modify the index mapping. We’ll do that this time by adding an English analyzer to both fields:

为了使这些测试再次通过,我们需要修改索引映射。 这次我们将在两个字段中添加英语分析器来完成此操作:

class Movie < ApplicationRecord
  include Elasticsearch::Model
  include Elasticsearch::Model::Callbacks
  
# ElasticSearch Index
  settings index: { number_of_shards: 1 } do
    mappings dynamic: 'false' do
      indexes :title, analyzer: 'english'
      indexes :overview, analyzer: 'english'
    end
  end
end

Run your tests again to see them pass.

再次运行测试以查看通过。

Elasticsearch is a very powerful search platform, and we could add a lot of functionalities to our search method. But this is not within the scope of this article. So we will stop here and move on to building the controller part of the JSON API through which the search method is accessed.

Elasticsearch是一个非常强大的搜索平台,我们可以在搜索方法中添加很多功能。 但这不属于本文的范围。 因此,我们将在此处停止,继续构建JSON API的控制器部分,通过该控制器部分可以访问搜索方法。

4.搜索API端点 (4. Search API endpoint)

The Search API we are building should allow users to make a fulltext search on the Movies Table. Our API has a single endpoint defined as follows:

我们正在构建的Search API应该允许用户在Movies Table上进行全文搜索。 我们的API具有单个端点,定义如下:

Url: 
 GET /api/v1/movies
 
Params:
 * q=[string] required
 
Example url:
 GET /api/v1/movies?q=Roma
 
Example response:
[{"_index":"movies","_type":"movie","_id":"95088","_score":11.549209,"_source":{"id":95088,"title":"Roma","overview":"A virtually plotless, gaudy, impressionistic portrait of Rome through the eyes of one of its most famous citizens.", "image_url":"https://image.tmdb.org/t/p/w300/rqK75R3tTz2iWU0AQ6tLz3KMOU1.jpg","vote_average":6.6,"created_at":"2018-04-14T10:30:49.110Z","updated_at":"2018-04-14T10:30:49.110Z"}},...]

Here we are defining our endpoint according to some best practices RESTful API Design:

在这里,我们根据RESTful API设计的一些最佳实践来定义端点:

  1. The URL should encode the object or resource, while the action to take should be encoded by the HTTP method. In this case, the resource is the movies (collection) and we are using the HTTP method GET (because we are requesting data from the resource without producing any side effect). We use URL parameters to further define how this data should be obtained. In this example, q=[string], which specifies a search query. You can read more about how to design RESTful APIs on Mahesh Haldar’s article RESTful API Designing guidelines — The best practices.

    URL应该对对象或资源进行编码,而采取的动作应通过HTTP方法进行编码。 在这种情况下,资源是电影 (集合),并且我们使用HTTP方法GET (因为我们正在从资源中请求数据而不会产生任何副作用)。 我们使用URL参数来进一步定义应如何获取此数据。 在此示例中,q = [string],它指定搜索查询。 您可以在Mahesh Haldar的文章RESTful API设计指南—最佳实践上阅读有关如何设计RESTful API的更多信息。

  2. We also add versioning to our API by adding v1 to our endpoint URL. Versioning your API is very important, because it allows you to introduce new features that are not compatible with previous releases without breaking all clients that were developed for previous versions of your API.

    我们还通过添加v1向API添加版本控制 到我们的端点URL。 对API进行版本控制非常重要,因为它允许您引入与以前的版本不兼容的新功能,而不会破坏为该API的先前版本开发的所有客户端。

Ok. Let’s start implementing.

好。 让我们开始执行。

As always, we begin with failing tests. Inside the spec folder, we will create the folder structure that reflects our API endpoint URL structure. This means controllers →api →v1 →movies_spec.rb

与往常一样,我们从失败的测试开始。 在spec文件夹内,我们将创建反映我们的API端点URL结构的文件夹结构。 这意味着控制器→api→v1→movies_spec.rb

You can do this manually or from your terminal running:

您可以手动执行此操作,也可以从运行以下命令的终端执行此操作:

mkdir -p spec/controllers/api/v1 && 
touch spec/controllers/api/v1/movies_spec.rb

The tests we are going to write here are controller tests. They do not need to check the search logic defined in the model. Instead we will test three things:

我们将在此处编写的测试是控制器测试。 他们不需要检查模型中定义的搜索逻辑。 相反,我们将测试三件事:

  1. A GET request to /api/v1/movies?q=[string] will call Movie.search with [string] as parameter

    对/ api / v1 / movies?q = [string]的GET请求将使用[string]作为参数调用Movie.search
  2. The output of Movie.search is returned in JSON format

    Movie.search的输出以JSON格式返回
  3. A success status is returned

    返回成功状态

A controller test should test controller behavior. A controller test should not fail because of problems in the model .

控制器测试应测试控制器的行为。 控制器测试不应因为模型中的问题而失败。

A controller test should test controller behavior. A controller test should not fail because of problems in the model .

控制器测试应测试控制器的行为。 控制器测试不应因为模型中的问题而失败。

(Prescription 20 — Rails 4 Test Prescriptions. Noel Rappin)

(处方20-Rails 4测试处方。NoelRappin )

Let’s transform this into code. Inside spec/controllers/api/v1/movies_spec.rb add the following code:

让我们将其转换为代码。 在spec / controllers / api / v1 / movies_spec.rb内部添加以下代码:

# spec/controllers/api/v1/movies_spec.rb
require 'rails_helper'
RSpec.describe Api::V1::MoviesController, type: :request do
  # Search for movie with text movie-title
  describe "GET /api/v1/movies?q=" do
    let(:title) { "movie-title"}
    let(:url) { "/api/v1/movies?q=#{title}"}
    
it "calls Movie.search with correct parameters" do
      expect(Movie).to receive(:search).with(title)
      get url
    end
    
it "returns the output of Movie.search" do
      allow(Movie).to receive(:search).and_return({})
      get url
      expect(response.body).to eq({}.to_json)
    end
    
it 'returns a success status' do
      allow(Movie).to receive(:search).with(title)
      get url
      expect(response).to be_successful
    end
  end
end

The test will immediately fail because Api::V1::MoviesController is not defined, so let’s do that first. Create the folder structure as before and add the movies controller.

由于未定义Api :: V1 :: MoviesController,因此测试将立即失败,因此让我们首先进行操作。 像以前一样创建文件夹结构,然后添加电影控制器。

mkdir -p app/controllers/api/v1 && 
touch app/controllers/api/v1/movies_controller.rb

Now add the following code to app/controllers/api/v1/movies_controller.rb:

现在,将以下代码添加到app / controllers / api / v1 / movies_controller.rb

# app/controllers/api/v1/movies_controller.rb
module Api
  module V1
    class MoviesController < ApplicationController
      def index;end
    end
  end
end

It’s time to run our test and see it fail.

现在该运行我们的测试并看到它失败了。

All tests fail because we still need to add a route for the endpoint. Inside config/routes.rb add the following code:

所有测试均失败,因为我们仍然需要为端点添加路由。 在config / routes.rb内部添加以下代码:

# config/routes.rb
Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :movies, only: [:index]
    end
  end
end

Rerun your tests and see what happens.

重新运行测试,看看会发生什么。

The first error tells us we need to add a call to Movie.search inside our controller. The second one complains about the response. Let’s add the missing code to the movies_controller:

第一个错误告诉我们我们需要在控制器内添加对Movie.search的调用。 第二个抱怨回应。 让我们将缺少的代码添加到movie_controller中:

# app/controllers/api/v1/movies_controller.rb
module Api
  module V1
    class MoviesController < ApplicationController
      def index
        response = Movie.search params[:q]
        render json: response
      end
    end
  end
end

Run the test and see if we are done.

运行测试,看看是否完成。

Yup. That’s all. We have completed a really basic backend application that allows users to search a model through API.

对。 就这样。 我们已经完成了一个非常基本的后端应用程序,该应用程序允许用户通过API搜索模型。

You can find the complete code on my GitHub repo here. You can populate your Movie table with some data by running rails db:seed so that you can see the application in action. This will import circa 45k Movies from a Dataset downloaded from Kaggle. Take a look at the Readme for more details.

您可以在我的GitHub存储库中找到完整的代码 。 您可以通过运行rails db:seed来用一些数据填充Movie表,以便可以看到实际的应用程序。 这将从从Kaggle下载数据集中导入大约45k电影。 请参阅自述文件以了解更多详细信息。

If you enjoyed this article, please share it on social media. Thank you!

如果您喜欢这篇文章,请在社交媒体上分享。 谢谢!

翻译自: https://www.freecodecamp.org/news/how-to-add-a-powerful-search-engine-to-your-rails-backend-57bced889032/

rails 添加外键

 类似资料: