カテゴリー: テクノロジー

Laravel APIがアクセスできなくなるとき

筋肉をバキバキにしてもコロナ禍だと海にいく予定がありませんよね〜。(そもそもぷよぷよ)
マエダです。

みんな大好きPHP LaravelでAPIを開発した際に急にアクセスできなくなることがありました。
これはLaravelが「429 (Too many requests)」のエラーを出していたためでした。

なぜこのエラーが発生するかと調査したところ1ThrottleRequestsという機能、スロットル、つまりリクエストの絞り機能がLaravelにはあり、デフォルトで1分間に同一IPアドレス・同一ドメインに対して60リクエストとなるように設定されていたためでした。

app/Http/Kernel.php

protected $middlewareGroups = [
'web' => [

],
'api' => [

'throttle:60,1',
],
];

1分間60リクエストを緩和するか、コメントアウトして制限しないようにするとエラーは解消されます。
そもそも制限にかからないようリクエスト回数を減らすことができないか見直すことが重要です。

みんな大好きAWSにて、ELB + EC2なシステム構成ではIPアドレスについても考慮が必要です。
ThrottleRequestsは、上述の通りで同一IPアドレスを以下のようにハッシュ値でカウントアップしています。

sha1($route->getDomain().'|'.$request->ip())

そのため正しくLaravelでIPアドレスが確認できること($request->ip())が必要です。
Nginx + php-fpmの構成などではNginxで以下設定をしましょう。

/etc/nginx/conf.d/realip.conf

set_real_ip_from 172.30.0.0/16; # ELBのIPアドレス
real_ip_header X-Forwarded-For;

※ 設定変更後は、設定反映のためNginxをreloadしましょう。

Nginxの設定は弊社メンバーの以下記事も参照してみてください。

知らないと損をするNginx設定


LaravelのEloquentとコードの共通化に便利なTrait

ここ数年Laravelを使っているのですがとても味わい深いフレームワークです。普通に使う分には意識せずコードはかけますしこういった機能はないのか?と思って探してみると大体見つかりどんどん書き方が変わってきます。

今回は開発を続けていく上で同じようなコードが分散していかないようにする為の方法の1つを書き留めておきます。リレーションがたくさんできていくと同じコードを書いてしまうことがあります。最初はコピペで良くても後から振る舞いが少し変えるためには同じようなコードを全部修正する必要がでてきますし確認も大変です。

下記はブログとブログの記事の作成者の名前を取得する例のコードです。

class Blog extends Model
{
    use UserTrait;
}

class BlogEntry extends Model
{
    use UserTrait;
}

trait UserTrait
{
    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function getUserNameAttribute()
    {
        if ($this->user) {
            // Userが存在するならユーザー名を返却
            return $this->user->name;
        }
        // Userが存在しない場合
        return '';
    }
}

こちら作成者が存在すれば名前をなければブランクを返すという簡単な流れです。Blog,BlogEntryともにuser_idで連結されており他にも同じようにuser_idによる連結がされる場合traitをuseするだけでいいので修正が楽です。こちらのいい所は途中でユーザーがいない場合ブランクではなく「退会されました」の文字列に連携するなどが容易なのに加えあとでユーザーに属性が追加されても呼び出す元は変更せずUserのTraitさえ変更すれば容易に追加できる点です。他にもAttribute以外にももちろんscopeやbootなど色々使えるでしょうしおすすめです。


AWS SESからS3へ受信してからのメール転送

東京オリンピックでの選手の活躍すごかったですね。(まだ開催されてないときに書いてます。)
マエダです。

みんな大好きAWSでメール受信はしたりしますよね。
https://aws.amazon.com/jp/premiumsupport/knowledge-center/ses-receive-inbound-emails/

S3にメールファイルが保存されたことをトリガーにpythonでメール転送する君を作成しました。
過去にどこからか(覚えておらず。。すみません。)参照させていただいたコードがpython2.7で動作するものだったのですが「AWS Lambda end of support for Python 2.7」なんてお知らせいただいたのでpython3.8で動作するように修正してみました。

import boto3
import email import re ORIGIN_TO = "(受信メールアドレス)" FORWARD_TO = "(転送先メールアドレス)" SES_REGION = "(SESリージョン)" S3_BUCKET = "(S3バケット名)" def parse_mail(raw_message): replaced_message = raw_message.replace(ORIGIN_TO, FORWARD_TO) replaced_message = re.sub("From:.+?\n", "From: %s\r\n" % ORIGIN_TO, replaced_message) replaced_message = re.sub("Return-Path:.+?\n", "Return-Path: %s\r\n" % ORIGIN_TO, replaced_message) return replaced_message def send_mail(message): ses = boto3.client('ses', region_name=SES_REGION) ses.send_raw_email( Source = ORIGIN_TO, Destinations=[ FORWARD_TO ], RawMessage={ 'Data': message } ) def lambda_handler(event, context): try: s3_key = event['Records'][0]['s3']['object']['key'] s3 = boto3.client('s3') response = s3.get_object( Bucket = S3_BUCKET, Key = s3_key ) raw_message = response['Body'].read().decode('utf-8') message = parse_mail(raw_message) send_mail(message) except Exception as e: print(e)

AWSをたのしもう。


[Solr] Nested Documents での部分更新

はじめに

以前はできなかった入れ子構造のドキュメントの部分的な更新が Solr 8.1 からはできるようになりました。この記事では入れ子構造のドキュメントの部分的な更新の動作を確認します。

更新前の状態

以下のプレイリストのデータを Solr に投入します。

[{
    "id":"list_1",
    "title_t":"list1",
    "songs":[
	{
	    "id":"l1!song1",
	    "title_t":"title1",
	    "artist_t":"artist1",
	    "trackNum_i":1
	},
	{
	    "id":"l1!song2",
	    "title_t":"title2",
	    "artist_t":"artist2",
	    "trackNum_i":2
	}
    ]
},
{
    "id":"list_2",
    "title_t":"list2",
    "songs":[
	{
	    "id":"l2!song1",
	    "title_t":"title3",
	    "artist_t":"artist3",
	    "trackNum_i":1
	},
	{
	    "id":"l2!song2",
	    "title_t":"title1",
	    "artist_t":"artist1",
	    "trackNum_i":2
	}
    ]
}
]

プレイリスト全体のデータで更新

以前はこのタイプの更新しかサポートされていませんでした。

$ cat update.json 
[{
    "id":"list_1",
    "title_t":"LIST1",
    "songs":[
	{
	    "id":"l1!song1",
	    "title_t":"TITLE1",
	    "artist_t":"ARTIST1",
	    "trackNum_i":1
	},
	{
	    "id":"l1!song2",
	    "title_t":"TITLE2",
	    "artist_t":"ARTIST2",
	    "trackNum_i":2
	}
    ]
}]
$ curl --user solr:SolrRocks 'http://localhost:8983/solr/nestedDocuments/update?commit=true&wt=json' --data-binary @update.json -H 'Content-Type: application/json'
{
  "responseHeader":{
    "rf":1,
    "status":0,
    "QTime":90}}
$ curl -s 'http://localhost:8983/solr/nestedDocuments/select' -d 'omitHeader=true' --data-urlencode 'q={!parent which="id:list_1 -_nest_path_:*"}' -d 'fl=*,[child]'
{
  "response":{"numFound":1,"start":0,"numFoundExact":true,"docs":[
      {
        "id":"list_1",
        "title_t":"LIST1",
        "_version_":1698469478122127360,
        "songs":[
          {
            "id":"l1!song1",
            "title_t":"TITLE1",
            "artist_t":"ARTIST1",
            "trackNum_i":1,
            "_version_":1698469478122127360},
          
          {
            "id":"l1!song2",
            "title_t":"TITLE2",
            "artist_t":"ARTIST2",
            "trackNum_i":2,
            "_version_":1698469478122127360}]}]
  }}

タイトルとアーティスト名を大文字にしてみました。

曲データだけを更新

_root_ フィールドで親ドキュメント(この場合プレイリストデータ)の id を指定します。

$ cat update2.json 
[
    {
	"id":"l1!song1",
	"_root_":"list_1",
	"title_t":{set:"title1"},
	"artist_t":{set:"artist1"}
    }
]
$ curl --user solr:SolrRocks 'http://localhost:8983/solr/nestedDocuments/update?commit=true&wt=json' --data-binary @update2.json -H 'Content-Type: application/json'
{
  "responseHeader":{
    "rf":1,
    "status":0,
    "QTime":131}}
$ curl -s 'http://localhost:8983/solr/nestedDocuments/select' -d 'omitHeader=true' --data-urlencode 'q={!parent which="id:list_1 -_nest_path_:*"}' -d 'fl=*,[child]'
{
  "response":{"numFound":1,"start":0,"numFoundExact":true,"docs":[
      {
        "id":"list_1",
        "title_t":"LIST1",
        "_version_":1698470257771937792,
        "songs":[
          {
            "id":"l1!song1",
            "title_t":"title1",
            "artist_t":"artist1",
            "trackNum_i":1,
            "_version_":1698470257771937792},
          
          {
            "id":"l1!song2",
            "title_t":"TITLE2",
            "artist_t":"ARTIST2",
            "trackNum_i":2,
            "_version_":1698470257771937792}]}]
  }}

l1!song1の曲名とアーティスト名を小文字に戻しました。

プレイリストに曲を追加

親ドキュメントの id を指定して、songs フィールドに対する add を実行します。

$ cat add.json 
[
    {
	"id":"list_1",
	"_root_":"list_1",
	"songs":{"add":{"id":"l1!song3",
		       "title_t":"title4",
		       "artist_t":"artist4",
		       "trackNum_i":3
		      }}
    }
]
$ curl --user solr:SolrRocks 'http://localhost:8983/solr/nestedDocuments/update?commit=true&wt=json' --data-binary @add.json -H 'Content-Type: application/json'
{
  "responseHeader":{
    "rf":1,
    "status":0,
    "QTime":67}}
$ curl -s 'http://localhost:8983/solr/nestedDocuments/select' -d 'omitHeader=true' --data-urlencode 'q={!parent which="id:list_1 -_nest_path_:*"}' -d 'fl=*,[child]'
{
  "response":{"numFound":1,"start":0,"numFoundExact":true,"docs":[
      {
        "id":"list_1",
        "title_t":"LIST1",
        "_version_":1698470937227165696,
        "songs":[
          {
            "id":"l1!song1",
            "title_t":"title1",
            "artist_t":"artist1",
            "trackNum_i":1,
            "_version_":1698470937227165696},
          
          {
            "id":"l1!song2",
            "title_t":"TITLE2",
            "artist_t":"ARTIST2",
            "trackNum_i":2,
            "_version_":1698470937227165696},
          
          {
            "id":"l1!song3",
            "title_t":"title4",
            "artist_t":"artist4",
            "trackNum_i":3,
            "_version_":1698470937227165696}]}]
  }}

プレイリスト list_1 に3曲目を追加しました。

プレイリストから曲を削除

親ドキュメントの id を指定して、songs フィールドに対する remove を実行します。

$ cat remove.json 
[
    {
	"id":"list_1",
	"_root_":"list_1",
	"songs":{"remove":{"id":"l1!song3"}}
    }
]
$ curl --user solr:SolrRocks 'http://localhost:8983/solr/nestedDocuments/update?commit=true&wt=json' --data-binary @remove.json -H 'Content-Type: application/json'
{
  "responseHeader":{
    "rf":1,
    "status":0,
    "QTime":50}}
$ curl -s 'http://localhost:8983/solr/nestedDocuments/select' -d 'omitHeader=true' --data-urlencode 'q={!parent which="id:list_1 -_nest_path_:*"}' -d 'fl=*,[child]'
{
  "response":{"numFound":1,"start":0,"numFoundExact":true,"docs":[
      {
        "id":"list_1",
        "title_t":"LIST1",
        "_version_":1698471464250900480,
        "songs":[
          {
            "id":"l1!song1",
            "title_t":"title1",
            "artist_t":"artist1",
            "trackNum_i":1,
            "_version_":1698471464250900480},
          
          {
            "id":"l1!song2",
            "title_t":"TITLE2",
            "artist_t":"ARTIST2",
            "trackNum_i":2,
            "_version_":1698471464250900480}]}]
  }}

先程追加した l1!song3 を削除しました。


Solr 8.8で入れ子構造の文書をインデックスする

はじめに

以前書いた「Solrで入れ子構造の文書をインデックスする」という記事で対象としていたのはSolr 7.5でしたが、現時点での最新版のSolr 8.8ではいろいろと変わった部分があるので改めて採り上げてみます。

Nested Documents 機能について Solr 7.5 から 8.8 で変わった点

Solr 7.5 の頃と変わったのは以下の点です。

  • 3層以上の階層を持てるようになった
  • どの階層のドキュメントかを示すフィールドを明示的に定義しなくても良くなった。階層構造を保持する _nest_path_ フィールドを Solr が自動的に管理するようになったため
  • 入れ子構造内の特定のドキュメントだけを部分的に更新できるようになった

スキーマ設定

Nested Documents を扱うためのスキーマ設定は以下の通りです。

_root_ フィールド (必須)

<field name="_root_" type="string" indexed="true" stored="false" docValues="false" />

インデックス作成時にSolrが自動的に作成するフィールドです。入れ子になったドキュメントはルートドキュメント(一番の先祖にあたるドキュメント)のidフィールドの値を _root_ フィールドの値として持ちます。

_nest_path_ フィールド (オプション)

<fieldType name="_nest_path_" class="solr.NestPathField" />
<field name="_nest_path_" type="_nest_path_" />

入れ子構造のルートドキュメント以外のドキュメントにSolrが自動的に付与するフィールドです。このフィールドは検索結果を取得するときに ChildDocTransformer で子ドキュメント以下の階層構造を扱うのに使われます。もしこのフィールドが無い場合はすべての子孫がフラットなリストとして扱われます。

_nest_path_ フィールドを使わない場合は、それぞれの階層を区別するための自前のフィールドを定義することが推奨されます。(手間が掛かるだけなので、素直に _nest_path_ フィールドを使った方が良さそうです)

_nest_parent_ フィールド (オプション)

<field name="_nest_parent_" type="string" indexed="true" stored="true" />

ルートドキュメント以外のドキュメントにSolrが自動的に付与するフィールドです。親ドキュメントのidフィールドの値を持ちます。

制限

  • スキーマ内で各フィールド名の定義は1回だけ。親と子で同じフィールド名を別のフィールドタイプで定義するようなことはできない
  • すべてのドキュメントタイプで共通するフィールドにだけ require を使うことができる
  • 階層を問わず、すべてのドキュメントは id フィールドにユニークな値を持たなければならない

例: プレイリスト

以下のようなプレイリスト情報を Solr に与えます。
(_default の configset でコレクションを作っていればスキーマレスでインデックス作成できます)

[{
    "id":"list_1",
    "title_t":"list1",
    "songs":[
	{
	    "id":"l1!song1",
	    "title_t":"title1",
	    "artist_t":"artist1",
	    "trackNum_i":1
	},
	{
	    "id":"l1!song2",
	    "title_t":"title2",
	    "artist_t":"artist2",
	    "trackNum_i":2
	}
    ]
},
{
    "id":"list_2",
    "title_t":"list2",
    "songs":[
	{
	    "id":"l2!song1",
	    "title_t":"title3",
	    "artist_t":"artist3",
	    "trackNum_i":1
	},
	{
	    "id":"l2!song2",
	    "title_t":"title1",
	    "artist_t":"artist1",
	    "trackNum_i":2
	}
    ]
},
{
    "id":"list_3",
    "title_t":"list3",
    "sublist":[
        {
            "id":"l3!sublist1",
            "title_t":"sublist1",
            "songs":[
	            {
	                "id":"l3!song1",
	                "title_t":"title4",
	                "artist_t":"artist4",
	                "trackNum_i":1
	            },
	            {
	                "id":"l3!song2",
	                "title_t":"title2",
	                "artist_t":"artist2",
	                "trackNum_i":2
	            }
            ]
        },
        {
            "id":"l3!sublist2",
            "title_t":"sublist2",
            "songs":[
	            {
	                "id":"l3!song3",
	                "title_t":"title1",
	                "artist_t":"artist1",
	                "trackNum_i":1
	            },
	            {
	                "id":"l3!song4",
	                "title_t":"title3",
	                "artist_t":"artist3",
	                "trackNum_i":2
	            }
            ]
        }
    ]
}]

プレイリスト名が list1 のプレイリストを曲も含めて検索

ヒットしたプレイリストの子ドキュメントを取得するために ChildDocTransformer を利用します。

$ curl 'http://localhost:8983/solr/nestedDocuments/select' -d 'omitHeader=true' --data-urlencode 'q={!parent which="title_t:list1 -_nest_path_:*"}' -d 'fl=*,[child]'
{
  "response":{"numFound":1,"start":0,"numFoundExact":true,"docs":[
      {
        "id":"list_1",
        "title_t":"list1",
        "_version_":1697935145902800896,
        "songs":[
          {
            "id":"l1!song1",
            "title_t":"title1",
            "artist_t":"artist1",
            "trackNum_i":1,
            "_version_":1697935145902800896},
          
          {
            "id":"l1!song2",
            "title_t":"title2",
            "artist_t":"artist2",
            "trackNum_i":2,
            "_version_":1697935145902800896}]}]
  }}

曲名が title1 の曲が含まれるすべてのプレイリストを検索

_nest_path_ フィールドでルート直下の songs の階層を指定しています。 {!parent} 内での -_nest_path_:* は「_nest_path_フィールドを持つもの以外」つまりルートドキュメントだけを対象とするという意味です。

$ curl -s 'http://localhost:8983/solr/nestedDocuments/select' -d 'omitHeader=true' --data-urlencode 'q={!parent which="*:* -_nest_path_:*"}(+_nest_path_:\/songs +title_t:title1)' -d 'fl=*,[child]'
{
  "response":{"numFound":2,"start":0,"numFoundExact":true,"docs":[
      {
        "id":"list_1",
        "title_t":"list1",
        "_version_":1697935145902800896,
        "songs":[
          {
            "id":"l1!song1",
            "title_t":"title1",
            "artist_t":"artist1",
            "trackNum_i":1,
            "_version_":1697935145902800896},
          
          {
            "id":"l1!song2",
            "title_t":"title2",
            "artist_t":"artist2",
            "trackNum_i":2,
            "_version_":1697935145902800896}]},
      {
        "id":"list_2",
        "title_t":"list2",
        "_version_":1697935145926918144,
        "songs":[
          {
            "id":"l2!song1",
            "title_t":"title3",
            "artist_t":"artist3",
            "trackNum_i":1,
            "_version_":1697935145926918144},
          
          {
            "id":"l2!song2",
            "title_t":"title1",
            "artist_t":"artist1",
            "trackNum_i":2,
            "_version_":1697935145926918144}]}]
  }}

サブリストに曲名が title1 の曲を含まれるプレイリストを検索

_nest_path_ フィールドで sublist 階層の下の songs の階層を指定しています。

$ curl -s 'http://localhost:8983/solr/nestedDocuments/select' -d 'omitHeader=true' --data-urlencode 'q={!parent which="*:* -_nest_path_:*"}(+_nest_path_:\/sublist\/songs +title_t:title1)' -d 'fl=*,[child]'
{
  "response":{"numFound":1,"start":0,"numFoundExact":true,"docs":[
      {
        "id":"list_3",
        "title_t":"list3",
        "_version_":1697935153103372288,
        "sublist":[
          {
            "id":"l3!sublist1",
            "title_t":"sublist1",
            "_version_":1697935153103372288,
            "songs":[
              {
                "id":"l3!song1",
                "title_t":"title4",
                "artist_t":"artist4",
                "trackNum_i":1,
                "_version_":1697935153103372288},
              
              {
                "id":"l3!song2",
                "title_t":"title2",
                "artist_t":"artist2",
                "trackNum_i":2,
                "_version_":1697935153103372288}]},
          
          {
            "id":"l3!sublist2",
            "title_t":"sublist2",
            "_version_":1697935153103372288,
            "songs":[
              {
                "id":"l3!song3",
                "title_t":"title1",
                "artist_t":"artist1",
                "trackNum_i":1,
                "_version_":1697935153103372288},
              
              {
                "id":"l3!song4",
                "title_t":"title3",
                "artist_t":"artist3",
                "trackNum_i":2,
                "_version_":1697935153103372288}]}]}]
  }}