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

[Solr]同じ内容のドキュメントの重複を防ぐ(De-Duplication)

はじめに

Solrでは基本的にIDフィールドの値でドキュメントを区別しているため、IDが異なれば同じ内容のドキュメントでも別々にインデックスされます。同じ内容のドキュメントの重複を防ぎたい場合はDe-Duplicationの機能を利用します。

De-Duplication の設定

De-Duplication を利用するためには、updateRequestProcessorChain に SignatureUpdateProcessor を組み込みます。

ここでは例として大阪の施設情報を利用します。以下のような文書構造になっています。

[
  {
    "id": "官公庁!1",
    "type": "官公庁",
    "area": "住之江区",
    "name": "軽自動車検査協会大阪主管事務所",
    "address": "住之江区南港東3-4-62"
  }
]

solrconfig.xml に以下を追加します。

   <updateRequestProcessorChain name="dedupe">
     <processor class="solr.processor.SignatureUpdateProcessorFactory">
       <bool name="enabled">true</bool>
       <str name="signatureField">signature</str>
       <bool name="overwriteDupes">true</bool>
       <str name="fields">type,name</str>
       <str name="signatureClass">solr.processor.Lookup3Signature</str>
     </processor>
     <processor class="solr.LogUpdateProcessorFactory" />
     <processor class="solr.RunUpdateProcessorFactory" />
   </updateRequestProcessorChain>
  <requestHandler name="/update" class="solr.UpdateRequestHandler" >
    <lst name="defaults">
      <str name="update.chain">dedupe</str>
    </lst>
  </requestHandler>

SignatureUpdateProcessor は、指定されたフィールドのハッシュ値を計算して一致すれば同一ドキュメントとみなすという動きになります。以下の3種類から選んで signatureClass プロパティで指定します。

  • MD5Signature
    • 128ビットのハッシュ
  • Lookup3Signature
    • 64ビットのハッシュ。MD5Signatureよりも高速
  • TextProfileSignature
    • 多少の曖昧さを許す

fieldsプロパティで、どのフィールドが同じなら同一のドキュメントとみなすかを指定します。
上の例では type と name が同一なら同じドキュメントとしました。

signatureField はハッシュ値を格納するフィールドを指定するものです。

overwriteDupes をtrue に設定すると、ドキュメントが同一と判定された場合に新しい方で古い方を上書きします。

実行例

上記の設定をした状態で以下のドキュメントをインデックスします。

[
  {
    "id": "官公庁!1",
    "type": "官公庁",
    "area": "住之江区",
    "name": "軽自動車検査協会大阪主管事務所",
    "address": "住之江区南港東3-4-62"
  }
]

検索結果は以下の通りです。signature フィールドが自動的に付与されています。

{
  "response":{"numFound":1,"start":0,"numFoundExact":true,"docs":[
      {
        "id":"官公庁!1",
        "type":"官公庁",
        "area":"住之江区",
        "name":"軽自動車検査協会大阪主管事務所",
        "address":"住之江区南港東3-4-62",
        "signature":"e3e630e5c046e6d3",
        "_version_":1701175515816132608}]
  }}

次に以下のドキュメントをインデックスします。名前と種別は同じで住所が変更になったという設定です。

[
  {
    "id": "官公庁!2",
    "type": "官公庁",
    "area": "港区",
    "name": "軽自動車検査協会大阪主管事務所",
    "address": "港区築港4-10-3"
  }
]

2番目のドキュメントをインデックスした後の検索結果は以下の通りです。

{
  "response":{"numFound":1,"start":0,"numFoundExact":true,"docs":[
      {
        "id":"官公庁!2",
        "type":"官公庁",
        "area":"港区",
        "name":"軽自動車検査協会大阪主管事務所",
        "address":"港区築港4-10-3",
        "signature":"e3e630e5c046e6d3",
        "_version_":1701175675284619264}]
  }}

期待通り上書きされています。idも新しいものになっています。

ちなみに、overwriteDupes = false で設定した場合には以下のようになりました。

{
  "response":{"numFound":2,"start":0,"numFoundExact":true,"docs":[
      {
        "id":"官公庁!1",
        "type":"官公庁",
        "area":"住之江区",
        "name":"軽自動車検査協会大阪主管事務所",
        "address":"住之江区南港東3-4-62",
        "signature":"e3e630e5c046e6d3",
        "_version_":1701175515816132608},
      {
        "id":"官公庁!2",
        "type":"官公庁",
        "area":"港区",
        "name":"軽自動車検査協会大阪主管事務所",
        "address":"港区築港4-10-3",
        "signature":"e3e630e5c046e6d3",
        "_version_":1701175519813304320}]
  }}

「上書きしない」というのは古い方のドキュメントがそのまま残るのかと思っていましたが、同じシグネチャのドキュメントが重複してインデックスされるということでした。


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 を削除しました。