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

Amazon Location Service のジオコーダを試してみた

はじめに

Amazon Location Service が6月に一般公開されました。
主な機能として以下の5つがあります。

  • 地図
  • ジオコーディング(住所や場所の名前を緯度経度に変換する)やリバースジオコーディング(緯度経度を住所に変換する)
  • 出発地から目的地までのルート計算
  • 位置情報デバイスのトラッキング
  • ジオフェンシング(設定した領域への出入りを検出する)

このうちのジオコーディングを試してみました。

ジオコーディングを AWS CLI から利用する

今回は AWS CLI からジオコーダを呼び出します。Location Service は新しいサービスなので、aws コマンドがインストール済みの場合は aws help で Location がサポートされているバージョンかどうかを確認し、サポートされていない場合は最新版にアップデートしておきます。

Place Index 作成

Place Index はジオコーダやリバースジオコーダを利用する際に必要となるリソースです。いくつかの設定項目を指定して Place Index を作成しておき、その Place Index を起点にジオコーダを呼び出す仕組みになっています。

以下のコマンドで、データソースとして Esri、料金プランとしてリクエスト数ベースを利用する Place Index を ExamplePlaceIndex という名前で呼び出せるようになります。

$ aws location \
>   create-place-index \
>   --data-source "Esri" \
>   --description "for investigation Esri" \
>   --index-name "ExamplePlaceIndex" \
>   --pricing-plan "RequestBasedUsage"

{
    "CreateTime": "2021-06-12T13:46:09.170000+00:00",
    "IndexArn": "arn:aws:geo:us-west-2:495463288701:place-index/ExamplePlaceIndex",
    "IndexName": "ExamplePlaceIndex"
}

Place Index 確認

以下のコマンドで、作成した Place Index の内容を確認できます。

$ aws location list-place-indexes
{
    "Entries": [
        {
            "CreateTime": "2021-06-12T13:46:09.170000+00:00",
            "DataSource": "Esri",
            "Description": "for investigation Esri",
            "IndexName": "ExamplePlaceIndex",
            "PricingPlan": "RequestBasedUsage",
            "UpdateTime": "2021-06-12T13:46:09.170000+00:00"
        }
    ]
}

ジオコーディング

ジオコーダを利用するときは search-place-index-for-text というサブコマンドを使います。

$ aws location search-place-index-for-text \
--index-name ExamplePlaceIndex \
--text "大阪市中央区" \
--max-results 5
{
    "Results": [
        {
            "Place": {
                "Country": "JPN",
                "Geometry": {
                    "Point": [
                        135.50988413000005,
                        34.681143990000066
                    ]
                },
                "Label": "大阪府大阪市中央区",
                "Municipality": "中央区",
                "Region": "大阪府"
            }
        }
    ],
    "Summary": {
        "DataSource": "Esri",
        "MaxResults": 10,
        "ResultBBox": [
            135.50988413000005,
            34.681143990000066,
            135.50988413000005,
            34.681143990000066
        ],
        "Text": "大阪市中央区"
    }
}

「大阪市中央区」の緯度経度が(34.681143990000066,135.50988413000005)であるという応答です。この座標を OpenStreetMap で表示すると、確かに大阪市中央区役所であることが確認できます。
https://www.openstreetmap.org/#map=19/34.681143990000066/135.50988413000005/

データソースとして HERE を使う Place Index 作成

Amazon Location Service ではデータソースとして EsriHERE が利用できます。ここではデータソースとして HERE を指定して、Esri のときとどのような違いがあるかを調べてみます。

$ aws location \
  create-place-index \
  --data-source "Here" \
  --description "for investigation HERE" \
  --index-name "ExamplePlaceIndexHERE" \
  --pricing-plan "RequestBasedUsage"
$ aws location list-place-indexes
{
    "Entries": [
        {
            "CreateTime": "2021-06-12T13:46:09.170000+00:00",
            "DataSource": "Esri",
            "Description": "for investigation Esri",
            "IndexName": "ExamplePlaceIndex",
            "PricingPlan": "RequestBasedUsage",
            "UpdateTime": "2021-06-12T13:46:09.170000+00:00"
        },
        {
            "CreateTime": "2021-06-12T14:12:51.610000+00:00",
            "DataSource": "Here",
            "Description": "for investigation HERE",
            "IndexName": "ExamplePlaceIndexHERE",
            "PricingPlan": "RequestBasedUsage",
            "UpdateTime": "2021-06-12T14:12:51.610000+00:00"
        }
    ]
}

HERE でジオコーディング

Esri のときと同じく「大阪市中央区」でジオコーディングします。

$ aws location search-place-index-for-text \
--index-name ExamplePlaceIndexHERE \
--text "大阪市中央区" \
--max-results 5 
{
    "Results": [
        {
            "Place": {
                "Country": "JPN",
                "Geometry": {
                    "Point": [
                        135.50988,
                        34.68114
                    ]
                },
                "Label": "大阪府大阪市中央区",
                "Municipality": "大阪市",
                "Neighborhood": "中央区",
                "Region": "大阪府"
            }
        },
        {
            "Place": {
                "Country": "JPN",
                "Geometry": {
                    "Point": [
                        135.5014,
                        34.66891
                    ]
                },
                "Label": "大阪府大阪市中央区道頓堀1丁目6ミナミ (難波)",
                "Municipality": "大阪市",
                "Neighborhood": "中央区",
                "Region": "大阪府"
            }
        },
        {
            "Place": {
                "AddressNumber": "27",
                "Country": "JPN",
                "Geometry": {
                    "Point": [
                        135.50398,
                        34.69353
                    ]
                },
                "Label": "大阪府大阪市北区中之島1丁目1-27大阪市中央公会堂",
                "Municipality": "大阪市",
                "Neighborhood": "北区",
                "Region": "大阪府"
            }
        },
        {
            "Place": {
                "Country": "JPN",
                "Geometry": {
                    "Point": [
                        135.48485,
                        34.69745
                    ]
                },
                "Label": "大阪府大阪市福島区福島7丁目4ジェイホッパーズ大阪ゲストハウス",
                "Municipality": "大阪市",
                "Neighborhood": "福島区",
                "Region": "大阪府"
            }
        },
        {
            "Place": {
                "Country": "JPN",
                "Geometry": {
                    "Point": [
                        135.50791,
                        34.68903
                    ]
                },
                "Label": "大阪府大阪市中央区伏見町1丁目1ホテルブライトンシティ大阪北浜",
                "Municipality": "大阪市",
                "Neighborhood": "中央区",
                "Region": "大阪府"
            }
        }
    ],
    "Summary": {
        "DataSource": "Here",
        "MaxResults": 5,
        "ResultBBox": [
            135.48485,
            34.66891,
            135.50988,
            34.69745
        ],
        "Text": "大阪市中央区"
    }
}

sri の場合は max-results を 5 で指定していても応答は1件だけでしたが、HERE の場合は 住所が大阪市中央区でないPOIでもアグレッシブに応答しており、ジオコーダとしての性格がかなり異なることが分かります。


[Solr]TextProfileSignatureによるDe-Duplication

はじめに

前回の記事で取り上げた De-Duplication ではハッシュの計算方法として、厳密には一致しなくてもほぼ同内容のドキュメントを同一として扱うためのTextProfileSignature が利用できます。Solr のドキュメントでは以下のように書かれています。

Fuzzy hashing implementation from Apache Nutch for near duplicate detection. It’s tunable but works best on longer text.

https://solr.apache.org/guide/8_8/de-duplication.html

どのくらい Fuzzy でも大丈夫なのか興味があったので調べてみました。

TextProfileSignature クラス

TextProfileSignature クラスの JavaDoc に詳しい説明がありました。

  • 文字と数字以外を取り除いて小文字に統一する
  • ソースを見ると、この判定には Character.isLetterOrDigit() が使われています。
  • 空白区切りでトークンに分割する
  • MIN_TOKEN_LEN(デフォルト2)より短いトークンを捨てる
  • 各トークンの出現回数をカウントする
  • 足きり用の QUANT を計算する。QUANT = QUANT_RATE * 最頻出のトークンの出現回数 (QUANT_RATEのデフォルト0.01)
  • QUANT が2より小さい場合は QUANT = 2 とする。ただし、2回以上出現したトークンが存在しない場合は QUANT = 1 とする。
    • すべてのトークンが1回ずつしか出現しなかった場合は足きりせず全部使うということ
    • ソースを見ると QUANT_RATE * 再頻出のトークンの出現回数 を四捨五入している。つまり、QUANT_RATE が デフォルトの 0.01 であれば、再頻出のトークンの出現回数が250までは QUANT = 2 (1回しか出現しないトークンは捨てられる)となる。
  • QUANT よりも小さい出現回数のトークンを捨てる
  • 残ったトークンを出現回数順に並べて MD5 ハッシュを計算する

ちなみに、空白文字で区切ってトークンを作るという処理なので、日本語のドキュメントにはあまり有効ではなさそうで、日本語ドキュメントで曖昧な De-Deplication をするためには、Tokenizer と連携する ProfileSignature を実装する必要がありそうです。

実験

実験のため、TextProfileSignature を呼び出す簡単なプログラムを作りました。

短いドキュメントでも効果がわかりやすいように、QUANT_RATE は 1 としています。これなら、再頻出のトークンの出現回数が2ならQUANTは2、再頻出のトークンの出現回数が2ならQUANTは3となります。

'I have an apple'  8b821c9e763bb2fc567d473996cfde4a
'I have an apple.' 8b821c9e763bb2fc567d473996cfde4a

記号の有無はハッシュ値に影響を与えません。

'an apple I have' 8b821c9e763bb2fc567d473996cfde4a

トークンの出現回数が同じなら、語順はハッシュ値に影響を与えません。

'I have the apple' 9526cdfcde3ddfad02a0691d564f30ac

トークンが別のものに変わるとハッシュ値も変化します。

'I have apple. I have apple.' 5d5a0ce2d6dc15618d873d5572c4eb5e
'I have a apple. I have the apple.' 5d5a0ce2d6dc15618d873d5572c4eb5e

QUANTが2になるので、1回しか出現しない ‘a’ ‘the’ の有無はハッシュ値に影響を与えません。

'I have an apple. I have an apple. I have the apple.' d95062c38e38e90b1c34b009bf434cda
'I have the apple. I have the apple. I have an apple.' d95062c38e38e90b1c34b009bf434cda

QUANTが3になるので、2回しか出現しない ‘a’ ‘the’ の有無はハッシュ値に影響を与えません。


[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など色々使えるでしょうしおすすめです。