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

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}]}]}]
  }}

MDN の動作サンプルを改造して遊ぶ

先日、MDN Web Docs で調べものをしているときに、
たまたま面白そうなサンプルを見つけました。


今日は、それを使って遊んでみたいと思います。


その前に、MDN Web Docs について分からない人向けに簡単に説明すると、
Mozilla.org による、Web技術者向けのドキュメントが集まったウェブサイトです。

MDN Web Docs (以前は MDN — Mozilla Developer Network と呼ばれていました) は、ウェブ技術とウェブを支えるソフトウェア、 CSS、 HTML、 JavaScript などについて学ぶための進化し続ける学習プラットフォームです。

MDN Web Docs について – MDN プロジェクト | MDN (mozilla.org)


私の中で、辞書変わりに常に開いておきたいサイトランキング1位に輝いています。
人によっては、読んでるだけで楽しい! ワクワクする! という噂もあります。


今回取り上げるサンプルと、元になるコードは以下にあります。
KeyboardEvent.code – Web APIs | MDN (mozilla.org)


“Try it out” と書かれた文字の下に、
黒い正方形と、その中にオレンジの二等辺三角形が描画されています。


コードを見ると、
オレンジの二等辺三角形は spaceship という名前のようです。
黒い四角は world。


私がつけたわけではありません。
が、ここに世界と、宇宙船が誕生したわけです。


ここからコードを追加し、
スペースキーを押すとショットを撃てるようにしたいと思います。


一部抜粋ですが、こんな感じに。

const spaceship = document.getElementById("spaceship");
const shot      = document.getElementById("shot");
const world     = document.querySelector('.world');
let isFiring    = false

function refresh() {
	let x = position.x - (shipSize.width/2);
	let y = position.y - (shipSize.height/2);
	let transform = "translate(" + x + " " + y + ") rotate(" + angle + " 15 15)";
	spaceship.setAttribute("transform", transform);

	if (isFiring) {
		let rad = angle * (Math.PI / 180);
		shotPosition.x += (Math.sin(rad) * shotMoveRate);
		shotPosition.y -= (Math.cos(rad) * shotMoveRate);
		let sx = shotPosition.x + (shipSize.width / 2);
		let sy = shotPosition.y + (shipSize.height / 2);
		shot.setAttribute("transform", 'translate(' + sx + ' ' + sy +')');

		if (isOutOfArea(sx, sy)) {
			isFiring = false;
		}
	} else {
		shotPosition.x = x;
		shotPosition.y = y;
	}
}
function isOutOfArea(x, y) {
	return (x < -1 || x > world.clientWidth || y < -1 || y > world.clientHeight);
}
setInterval(() => {
	refresh();
}, 33);


MDN のコードは、あくまでも KeyboardEvent のサンプルなので、
キーボードを押した際にしか処理が行われないようになっています。


これはこれで省エネなのですが、
この世界の神がキーボードを押している間しか時間が進みませんので、
いろいろと不都合です。

定期的に画面を描画し直すように変更しました。

次に、スペースキーで shot を発射するための処理を入れるのですが、
この宇宙船は、2次元平面上で回転します。

宇宙船の向きに合わせて前方に発射しましょう。

宇宙船と角度を合わせて、
座標を移動させるだけです。

無事、撃てるようになったら、
世界の領域を越えたショットを消すようにします。

宇宙の外に迷惑をかけてはいけません。


ここまでの動作サンプルを置いておきます。
下の枠内をクリックすると読み込みます。

クリックで読み込み

推奨環境は Google Chrome、MS Edge です。


気付いた人もいらっしゃるかもしれませんが、
angle の値を使いまわしているため、
ショットを撃ったあとに回転するとショットがカーブします。
これはこれで面白いかと思い放置しています。


自機が動いてショットを撃つ。

これだけでも、
なんとなくゲームのような片鱗が見えてきたのではないでしょうか。

360度全方位の2Dシューティングゲーム (画面は3D) が
何年か前にありましたが、
ものすごく頑張ればそれに近いものが作れるかもしれません。


MDN にはいろいろな動作サンプルがあります。
あなたの求めるものが意外なところから見つかるかもしれません。