v-crn Code Log

主に備忘録

ValueError: Integers to negative integer powers are not allowed.[Python][Numpy]

エラー内容

以下のように、負の数を指数とした累乗の計算でエラーが出た。

for num in np.arange(-5, 5):
  print(10**num)

""" ValueError: Integers to negative integer powers are not allowed.
"""

「負の整数による整数のべき乗は許されない」とのこと。

解決策

Python標準のrangeメソッドを使う。

for num in range(-5, 5):
  print(10**num)

numpyでは負の整数による整数のべき乗はできない。

逆に言えば、整数でなければnumpyでも期待通りの動作をする:

for num in np.arange(-5, 5):
  print(10.**num)

numpyは整数型の癖の強さに定評がある。

Python 3.x - python エラー|teratail

SQL何もわからんな人へ捧げる『SQL Bolt』

SQLBolt - Learn SQL - Introduction to SQL

SQLをWeb上で学べる教材。記述したクエリの結果がリアルタイムに出力される。

以下、コードサンプルといくつかの演習問題に対する解答例。簡単すぎる問題は省略。

SQL Lesson 3: Queries with constraints (Pt. 2)

-- 1. Find all the Toy Story movies
SELECT * FROM movies
where title
  like '%Toy Story%'
;

-- 3. Find all the movies (and director) not directed by John Lasseter
SELECT * FROM movies
where director
    not like '%John Lasseter%'
;

SQL Review: Simple SELECT Queries

SELECT * FROM north_american_cities
where Country like 'United States'
order by latitude ASC;

SQL Lesson 7: OUTER JOINs

-- Find the list of all buildings that have employees
SELECT distinct e.building
FROM employees e
join Buildings b
on b.building_name = e.building
;

-- List all buildings and the distinct employee roles in each building (including empty buildings)
SELECT distinct
    b.building_name,
    e.role
FROM Buildings b
left outer join employees e
on b.building_name = e.building
;

SQL Lesson 8: A short note on NULLs

SELECT column, another_column, …
FROM mytable
WHERE column IS/IS NOT NULL
AND/OR another_condition
AND/OR …;

Exercise

-- Find the name and role of all employees who have not been assigned to a building
SELECT * FROM employees
where building is null
;

-- Find the names of the buildings that hold no employees
SELECT * 
FROM buildings b
    left outer join employees e
    on e.building = b.building_name
where
    e.building is null
;

SQL Lesson 9: Queries with expressions

-- Example query with expressions
SELECT particle_speed / 2.0 AS half_particle_speed
FROM physics_data
WHERE ABS(particle_position) * 10.0 > 500;

-- Select query with expression aliases
SELECT col_expression AS expr_description, …
FROM mytable;

-- Example query with both column and table name aliases
SELECT column AS better_column_name, …
FROM a_long_widgets_table_name AS mywidgets
INNER JOIN widget_sales
  ON mywidgets.id = widget_sales.widget_id;

Exercise

-- List all movies and their combined sales in millions of dollars
SELECT
    m.title,
    (b.domestic_sales + b.international_sales) / 1000000
FROM movies m
left outer join Boxoffice b
on b.movie_id = m.id
;

-- List all movies that were released on even number years
SELECT
    m.title
FROM movies m
left outer join Boxoffice b
on b.movie_id = m.id
where m.year % 2 = 0
;

SQL Lesson 10: Queries with aggregates (Pt. 1)

-- Select query with aggregate functions over all rows
SELECT AGG_FUNC(column_or_expression) AS aggregate_description, …
FROM mytable
WHERE constraint_expression;

Exercise

-- Find the longest time that an employee has been at the studio
SELECT max(years_employed)
FROM employees
;

-- For each role, find the average number of years employed by employees in that role
SELECT role, avg(years_employed)
FROM employees
group by role
;

-- Find the total number of employee years worked in each building
SELECT building, sum(years_employed)
FROM employees
group by building
;

SQL Lesson 11: Queries with aggregates (Pt. 2)

-- Select query with HAVING constraint
SELECT group_by_column, AGG_FUNC(column_expression) AS aggregate_result_alias, …
FROM mytable
WHERE condition
GROUP BY column
HAVING group_condition;

Did you know? If you aren't using the GROUP BY clause, a simple WHERE clause will suffice.

Exercise

-- Find the number of Artists in the studio (without a HAVING clause)
SELECT count(*)
FROM employees
where role = 'Artist'
;

-- Find the number of Employees of each role in the studio
SELECT role,count(*)
FROM employees
group by role
;

-- Find the total number of years employed by all Engineers
SELECT role,sum(Years_employed)
FROM employees
group by role
having role = 'Engineer'
;

SQL Lesson 12: Order of execution of a Query

-- Complete SELECT query
SELECT DISTINCT column, AGG_FUNC(column_or_expression), …
FROM mytable
    JOIN another_table
      ON mytable.column = another_table.column
    WHERE constraint_expression
    GROUP BY column
    HAVING constraint_expression
    ORDER BY column ASC/DESC
    LIMIT count OFFSET COUNT;

Exercise

-- Find the number of movies each director has directed
SELECT m.director, count(*)
FROM movies m
    left outer join boxoffice b
    on b.movie_id = m.id
group by director
order by director
;

-- Find the total domestic and international sales that can be attributed to each director
SELECT m.director, sum(domestic_sales+international_sales)
FROM movies m
    left outer join boxoffice b
    on b.movie_id = m.id
group by director
order by director
;

SQL Lesson 13: Inserting rows

-- Insert statement with values for all columns
INSERT INTO mytable
VALUES (value_or_expr, another_value_or_expr, …),
       (value_or_expr_2, another_value_or_expr_2, …),
       …;
-- Insert statement with specific columns
INSERT INTO mytable
(column, another_column, …)
VALUES (value_or_expr, another_value_or_expr, …),
      (value_or_expr_2, another_value_or_expr_2, …),
      …;

SQL Lesson 14: Updating rows

-- Update statement with values
UPDATE mytable
SET column = value_or_expr, 
    other_column = another_value_or_expr, 
    …
WHERE condition;

Exercise

-- The director for A Bug's Life is incorrect, it was actually directed by John Lasseter
update movies
set director = 'John Lasseter'
where title = "A Bug's Life"
  • 文字列の中でシングルクォーテーション「'」を使う場合は全体の括弧はダブルクォーテーション「"」を使う

SQL Lesson 15: Deleting rows

-- Delete statement with condition
DELETE FROM mytable
WHERE condition;
delete from movies
where director = 'Andrew Stanton'

SQL Lesson 16: Creating tables

-- Create table statement w/ optional table constraint and default value
CREATE TABLE IF NOT EXISTS mytable (
    column DataType TableConstraint DEFAULT default_value,
    another_column DataType TableConstraint DEFAULT default_value,
    …
);

SQL Lesson 17: Altering tables

-- Altering table to add new column(s)
ALTER TABLE mytable
ADD column DataType OptionalTableConstraint
    DEFAULT default_value;

-- Altering table to remove column(s)
ALTER TABLE mytable
DROP column_to_be_deleted;

-- Altering table name
ALTER TABLE mytable
RENAME TO new_table_name;
-- Add a column named Aspect_ratio with a FLOAT data type to store the aspect-ratio each movie was released in.
alter talbe movies
add Aspect_ratio float;

-- Add another column named Language with a TEXT data type to store the language that the movie was released in. Ensure that the default for this language is English.
alter table movies
add Language text
default 'English';

SQL Lesson 18: Dropping tables

-- Drop table statement
DROP TABLE IF EXISTS mytable;

SQL Lesson X: To infinity and beyond!

graduation

これでクリアです。お疲れさまでした!

Octave 5.1.0でpauseの挙動がおかしい

CouseraのStanford University Machine Learningクラス2週目の課題ex1「2.1 Plotting the Data」において、説明に従ってplotData.mを編集し、ex1を実行すると、Enterキーを押してもpauseのまま動かなくなる事案に遭遇した。

Running warmUpExercise ... 
5x5 Identity Matrix: 
ans =

Diagonal Matrix

   1   0   0   0   0
   0   1   0   0   0
   0   0   1   0   0
   0   0   0   1   0
   0   0   0   0   1

Program paused. Press enter to continue.

調べてみたところ、どうやらOctave 5.1.0ではpause関数にバグあることに原因があるようだ。

下記の記事を参考に、新たにpause関数を定義することで解決した。

octave pause; not working properly. Forces to ctrl+c :( : octave

ex1.mの適当な箇所に次の内容を追記。

function pause
  input('','s');
end

これでEnterキーを押すごとに処理が進行する。

Rubyで変数が定義されているか確認する

defined?

Rubyで変数が定義されているか確認するにはdefined?メソッドを使用する。defined?は引数として入れた変数やメソッドが定義済みであれば式の種別を表す文字列を返す。未定義であればnilを返す。

>> a = 1
>> defined? a
=> "local-variable"

>> hoge = nil
>> defined? hoge
=> "local-variable"

>> defined? poke
=> nil

>> defined? puts
=> "method"

>> defined? defined?
Traceback (most recent call last):
SyntaxError ((irb):10: syntax error, unexpected end-of-input)
defined? defined?
                 ^

参考

クラス/メソッドの定義 (Ruby 2.6.0)

ruby on rails - How to check if a variable exists with a value without "undefined local variable or method"? - Stack Overflow

ControllerのActionが表示するページごとにSCSSを設定する | Rails

app/views/layouts/application.html.erbのbodyタグにERBで次のように記述する。

<body class='<%= "#{controller.controller_name}_#{controller.action_name}" %>'>

こうしておくと、たとえばStaticPagesControllerのHomeアクションのViewファイル「home.html.erb」に対してclass="static_pages_home"とクラスが付帯される。よって、このアクションが表示するページをSCSSで修飾したい場合はSCSSファイルにおいて

.static_pages_home {}

という形で括弧の中を編集すれば良い。

Railsアプリに包含・除外検索機能を実装する

目次

はじめに

Twitter似のRailsアプリに投稿検索機能を実装したので、この折に自分の理解した内容をまとめます。
実装する検索機能は包含検索(検索語が含まれる曖昧検索)、除外検索(先頭にマイナス記号(-)を付けた検索語が含まれない曖昧検索)で、複数の単語を入れる場合は間にスペースを入力するものとします。
曖昧検索とは、検索語を少なくとも一部に含む要素を抽出する検索方式のことです。部分一致検索とも呼びます。

前提

  • Ruby 2.5.3
  • Rails 5.2.3
  • pagy 3.4.1(ページング用のgem)

検索メソッド

contentカラムにtextデータが入ったPostモデルを想定します。
対象のモデルに次のように検索用のクラスメソッドを定義します。

app/models/post.rb

def self.search_with(keywords)
  return none if keywords.blank?

  keywords = keywords.split(/[[:blank:]]+/).select(&:present?)
  excluding_terms, including_terms =
    keywords.partition { |keyword| keyword.start_with?('-') && keyword.length >= 2 }

  | including_terms = [including_terms.map { | term | "%#{term}%" }.join]                    |
  | excluding_terms = [excluding_terms.map { | term | "%#{term.delete_prefix('-')}%" }.join] |

  return Saying.where('content NOT LIKE ?', excluding_terms) if including_terms.select(&:present?).empty?

  Post.where('content LIKE ?', including_terms)
        .where('content NOT LIKE ?', excluding_terms)
end

nil回避のためのreturn none

return none if keywords.blank?は検索欄に単語が入力されていない状態で検索が実行された場合に早期リターンするための一文です。ここで注意するのはreturn if keywords.blank?ではなく、noneを返しているという点です。これによりコントローラーでページングに使うpagyメソッドの引数が、検索結果0件の場合でもnilではなく#<ActiveRecord::Relation []>となるようにしています。

複数の検索語を配列に格納する

keywords = keywords.split(/[[:blank:]]+/).select(&:present?)について。

  • split(/[[:blank:]]+/):スペース区切りで各単語を配列の要素として格納する
  • select(&:present?):配列からnilと空文字列("")以外の要素を抽出する(reject(&:blank?)でも同じ結果が得られる)

使用例からわかるように、"''"は残ってしまうことに注意です。

包含検索語と除外検索語を別々の配列に格納する

excluding_terms, including_terms =
    keywords.partition { |keyword| keyword.start_with?('-') && keyword.length >= 2 }

上のコードは、keywordsの要素のうちマイナス記号から始まる2文字以上の要素をexcluding_termsに、それ以外をincluding_termsに格納しています。

  • a, b = enum.partition {|item| block }blockの戻り値が真の要素を左辺第一項の配列に、偽の要素を左辺第二項の配列に格納する
  • string.start_with?:stringオブジェクトの中身が引数の文字列から始まるならtrue、それ以外はfalseを返す

検索語を含んだ配列を曖昧検索の形式に合わせて整形する

この節が本記事の肝となる部分です。 まずRailsにおける曖昧検索の形式を以下に示します。

# 検索語が単数の場合
モデル.where("カラム名 LIKE '%検索語%'")

# 検索語が複数の場合
モデル.where("カラム名 LIKE", ['%検索語1%%検索語2%...%検索語N%'])

検索語が複数の場合、条件は要素数1のstring配列となることに注意してください。この形式に適合するように、各検索語を%で囲み結合した文字列の配列に整形します。

| including_terms = [including_terms.map { | term | "%#{term}%" }.join]                    |
| excluding_terms = [excluding_terms.map { | term | "%#{term.delete_prefix('-')}%" }.join] |
  • array.map {|item| block }:要素の数だけblockを実行し、その戻り値を集めた配列を返す
  • join:引数に指定した文字列を区切りに要素を結合した文字列を返す
  • delete_prefix:引数に指定した接頭語を削除する

where likeで曖昧検索

postsテーブルのcontentカラムのうち、including_terms内の文字列を含んでいるものを抽出し、そこからexcluding_terms内の文字列を含んでいるものを除外します。

return Post.where('content NOT LIKE ?', excluding_terms) if including_terms.select(&:present?).empty?

Post.where('content LIKE ?', including_terms)
    .where('content NOT LIKE ?', excluding_terms)

return文の箇所は検索語にマイナス記号の接頭辞が付いたものしかない場合の処理です。 その場合、including_termsは[""]であり、Post.where('content LIKE ?', including_terms)の戻り値は#<ActiveRecord::Relation []>となります。つまり、検索結果は該当0件です。ここではPost全体から除外検索語を含まないものを抽出したいので、上述のようなreturn文の分岐を入れているわけです。

実行例

定義の流れに沿ってコードを実行した例を下に載せておきます。

例1:検索結果0件

>> keywords = " ''  - -- love -you"
=> " ''  - -- love -you"

>> keywords.split(/[[:blank:]]+/)
=> ["", "''", "-", "--", "love", "-you"]

>> keywords = keywords.split(/[[:blank:]]+/).select(&:present?)
=> ["''", "-", "--", "love", "-you"]

>> excluding_terms, including_terms = keywords.partition { |keyword| keyword.start_with?('-') && keyword.length >= 2 }
=> [["--", "-you"], ["''", "-", "love"]]

>> including_terms = [including_terms.map { |term| "%#{term}%" }.join]
=> ["%''%%-%%love%"]

>> excluding_terms = [excluding_terms.map { |term| "%#{term.delete_prefix('-')}%" }.join]
=> ["%-%%you%"]

>> Post.where('content LIKE ?', including_terms).where('content NOT LIKE ?', excluding_terms)
  Post Load (2.2ms)  SELECT  "posts".* FROM "posts" WHERE (content LIKE '%''''%%-%%love%') AND (content NOT LIKE '%-%%you%') ORDER BY "posts"."created_at" DESC LIMIT ?  [["LIMIT", 11]]
=> #<ActiveRecord::Relation []>

例2:検索結果1件

>> keywords = " -love , '  you   "
=> " -love , '  you   "

>> keywords = keywords.split(/[[:blank:]]+/).select(&:present?)
=> ["-love", ",", "'", "you"]

>> excluding_terms, including_terms = keywords.partition { |keyword| keyword.start_with?('-') && keyword.length >= 2 }
=> [["-love"], [",", "'", "you"]]

>> including_terms = [including_terms.map { |term| "%#{term}%" }.join]=> ["%,%%'%%you%"]

>> excluding_terms = [excluding_terms.map { |term| "%#{term.delete_prefix('-')}%" }.join]
=> ["%love%"]

>> Post.where('content LIKE ?', including_terms).where('content NOT LIKE ?', excluding_terms)
Post Load (3.9ms)  SELECT  "posts".* FROM "posts" WHERE (content LIKE '%,%%''%%you%') AND (content NOT LIKE '%love%') ORDER BY "posts"."created_at" DESC LIMIT ?  [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Post id: 38, content: "Goodnight, my darlings, I'll see you tomorrow.", user_id: 3, created_at: "2019-08-18 15:09:05", updated_at: "2019-08-22 03:09:05", picture: nil>

以上で検索メソッドについての説明はおしまいです。

Routes

URLが/posts/searchとなるようにcollectionを使ってsearchアクションへのルーティングを行います。

config/routes.rb

resources :posts, only: %i[create destroy] do
  collection do
    get 'search'
  end
end

Controller

検索フォームから送信されたキーワードをモデルの検索メソッドの引数として処理させます。その戻り値をページングした上でView側に渡すためのインスタンス変数に格納します。

app/controllers/posts_controller.rb

def search
  return if params[:keywords].blank?

  @keywords = params[:keywords]
  @pagy_results, @search_results = pagy(post.search_with(@keywords), items: 30)

  respond_to do |format|
    format.html { redirect_back(fallback_location: request.referer) }
    format.js
  end
end

検索フォーム

routesで設定した検索アクションのパスをform_withのurlにセットします。text_fieldに:keywordsを設定することも忘れずに!

app/views/posts/_search_posts_form.html.erb

<%= form_with url: search_posts_path, method: :get do |f| %>
  <%= f.text_field :keywords, placeholder: "Search", class: "form-control" %>
  <%= f.submit value="&#128269;".html_safe, class: "btn btn-default" %>
<% end %>
<% if @search_results %>
  <div class="search_results">
    <h5>Search Terms: <%= @keywords %></h5>
    <ol class="posts">
      <%= render @search_results %>
    </ol>
    <%== pagy_bootstrap_nav(@pagy_results) if @pagy_results.pages > 1 %>
  </div>
<% end %>

app/views/home.html.erb

<div id="search_posts">
  <%= render 'posts/search_posts_form' %>
</div>

ページを再読み込みしなくても検索結果が表示されるようにコントローラーのアクションと同じ名前のjs.erbファイルを用意し、次のように編集します。

app/views/posts/search.js.erb

$("#search_posts").html("<%= j(render 'posts/search_posts_form') %>");

SCSSでdisplay: inline-flexを指定すると、検索欄と検索ボタンが横並びに表示されます。

app/assets/stylesheets/custom.css.scss

#search_posts {
  form {
    display: inline-flex;
    width: 100%;
    margin-top: 20px;
    margin-bottom: 10px;

    input[type="text"] {
      margin: 0 0 0 0;
    }

    input[type="submit"] {
      padding: 5px;
    }
  }

  h5 {
    text-align: left;
  }
}

テスト

統合テスト

まずテスト用のfixtureファイルを適当に用意します。

test/fixtures/posts.yml

one:
  content: "ぼくの青春はぼくから逃れてゆく。病気というのはそのことだ。 -- アルベール・カミュ『手帖』"
  created_at: <%= 10.minutes.ago %>
  user: john

two:
  content: "光陰虚しく渡ることなかれ -- 道元『正法眼蔵随聞記』"
  created_at: <%= 20.minutes.ago %>
  user: john

most_recent:
  content: "This is the most recent post."
  created_at: <%= Time.zone.now %>
  user: john

ants:
  content: "Oh, is that what you want? Because that's how you get ants!"
  created_at: <%= 2.years.ago %>
  user: user_0

rails g integration_test searchで統合テストを作成し、次のように編集しました。

test/integration/search_test.rb

require 'test_helper'

class SearchTest < ActionDispatch::IntegrationTest
  def setup
    @existent_valid_terms = ['-', '--', '---', '-is', 'this is --   ', '『', '\'', '-,']
    @non_existent_valid_terms = ['くぁwせdrftgyふじこlp', 'WTF!?', '\n']
    @invalid_terms = ['', '              ', nil]
  end

  test 'should display posts after searching with existent valid terms' do
    @existent_valid_terms.each do |term|
      get root_path
      assert_select 'div#search_posts'
      assert_no_match 'search_results', @response.body
      get search_posts_path, xhr: true, params: { keywords: term }
      assert_response :success
      assert_match 'search_results', @response.body
      assert_match 'post', @response.body
      assert_select 'li'
    end
  end

  test 'should not display any posts after searching with non-existent valid terms' do
    @non_existent_valid_terms.each do |term|
      get root_path
      assert_no_match 'search_results', @response.body
      get search_posts_path, xhr: true, params: { keywords: term }
      assert_response :success
      assert_match 'search_results', @response.body
      assert_select 'li', false
    end
  end

  test 'should not display search results after searching with invalid terms' do
    @invalid_terms.each do |term|
      get root_path
      assert_select 'div#search_posts'
      assert_no_match 'search_results', @response.body
      get search_posts_path, xhr: true, params: { keywords: term }
      assert_response :success
      assert_no_match 'search_results', @response.body
    end
  end
end

setupを見るとわかるように
@existent_valid_termsには検索語として有効で検索結果に該当するものがある語を、
@non_existent_valid_termsには検索語として有効で検索結果に該当するものがない語を、
@invalid_termsには検索語として無効な語が格納されます。

このテストでは、setupで定義したそれぞれの配列を使って検索したときに期待する結果が@response.bodyに含まれていることや投稿を一覧で表示するリストのタグ<li>の有無を確認しています。

実際に検索してみた様子

次に示す表の検索語を使って画面から実際に検索をかけてみます。

検索語 期待する検索結果
ice 「ice」を含む投稿
-cream 「cream」を含まない投稿
ice-cream 「ice-cream」を含む投稿
ice -cream 「ice」を含む、かつ「cream」を含まない投稿

無事、期待する検索結果を得ることができました。

おわりに

もっとエレガントでセキュアなやり方があれば是非教えてください。

参考

railsで複数ワードでの検索機能(or)とマイナス検索機能(-)を実装してみる - Qiita