[アップデート] AWS Entity Resolution の Advanced ルールタイプがリアルタイムマッチングに対応したので試してみた

[アップデート] AWS Entity Resolution の Advanced ルールタイプがリアルタイムマッチングに対応したので試してみた

AWS Entity Resolutionの Advanced ルールタイプがリアルタイムマッチングに対応したので、GenerateMatchId API での動作を実際に検証してみました。
2026.07.29

クラウド事業統括本部の石川です。AWS Entity Resolution の高度な(Advanced)マッチングワークフローがリアルタイムマッチングに対応しましたので、リアルタイムマッチングということなので AWS CLI で実際に試してみました。

https://aws.amazon.com/jp/about-aws/whats-new/2026/07/aws-entity-resolution/

AWS Entity Resolutionは、めちゃくちゃニーズがある割に、いまいち知名度が低いサービスです。この記事をきっかけに少しでも認知されるといいな。

AWS Entity Resolution とは

AWS Entity Resolution は、複数のアプリケーションやデータストアに分散した顧客・製品・ビジネス・医療関連のレコードを照合し、同一エンティティとして紐付けるマネージドサービスです。マッチしたレコードの集合には Match ID という一意の識別子が付与されます。

このアップデートで、これまでバッチ処理専用だった Advanced ルールタイプのマッチングワークフローが、GenerateMatchId API によるリアルタイムマッチングに対応しました。アナウンスによると、従来は数分から数時間かかっていた複雑なルールセットの評価が、ミリ秒単位で完了するとのことです。

有効化は、マッチングワークフローの resolutionTechniques にある enableRealTimeMatching パラメータを true に設定するだけとされています。

https://docs.aws.amazon.com/entityresolution/latest/userguide/generate-match-id.html

ところが、この GenerateMatchId のドキュメントを読むと、同一ページに次の 2 つの記述が並んでいます。

  • GenerateMatchId API を呼び出すには、事前に StartMatchingJob API でルールベースマッチングワークフローを 1 回成功させておく必要がある
  • enableRealTimeMatchingtrue に設定したワークフローは、StartMatchingJob API で実行できない

このままでは、Advanced リアルタイムワークフローでは GenerateMatchId を永久に呼べないことになります。実際にどうなるのかを含めて、東京リージョンで検証しました。

ルールタイプとリアルタイムマッチング

ルールベースマッチングのワークフローを作成するときは、SimpleAdvanced のいずれかのルールタイプを選びます。ルールタイプは作成後に変更できません。

https://docs.aws.amazon.com/entityresolution/latest/userguide/creating-matching-workflow-rule-based.html

公式ドキュメントの比較表から、主な違いを抜粋します。

項目 Advanced ルールタイプ Simple ルールタイプ
入力フィールドと match key の対応 1 対 1 複数列を同一 match key にまとめられる
Exact マッチとファジーマッチ 両方をサポート Exact マッチのみ
演算子 AND、OR、括弧 AND のみ
バッチワークフロー サポート サポート
増分ワークフロー サポート サポート

Advanced ルールタイプでは、condition という文字列でマッチング条件を記述します。使用できる関数は次のとおりです。

  • Exact 系: Exact(matchKey)ExactManyToMany(matchKey, matchKey, ...)
  • Fuzzy 系: Cosine(matchKey, threshold)Levenshtein(matchKey, threshold)Soundex(matchKey)

これらを AND / OR / 括弧で組み合わせます。今回のアップデートでリアルタイム対応したのは、このうち Exact 系の関数を組み合わせたルールセットです。

なお、比較表には「Supports real-time workflows」の行があり、本記事執筆時点では Advanced が No のまま更新されていません。今回のアップデートが反映されていないようですので、GenerateMatchId のページを参照するのが正確です。

やってみた

前提条件

  • 検証リージョン: ap-northeast-1(東京)
  • AWS CLI: aws-cli/2.36.9
  • Python: 3.14.0 / boto3: 1.43.14(レイテンシ計測でのみ使用)
  • 事前に用意するもの: 入出力用の S3 バケット、AWS Glue のデータベースとテーブル

検証データの準備

顧客データを模した CSV を用意し、S3 に配置して Glue テーブルとして登録します。電話番号を 2 列(phonealt_phone)持たせているのは、後段で ExactManyToMany の挙動を確認するためです。

customers.csv
unique_id,first_name,last_name,email,phone,alt_phone
c-001,Taro,Yamada,taro.yamada@example.com,090-1111-2222,03-1000-0001
c-002,Hanako,Suzuki,hanako.suzuki@example.com,090-5555-6666,03-2000-0002
c-003,Jiro,Tanaka,jiro.tanaka@example.com,070-7777-8888,03-3000-0003
c-004,Saburo,Sato,saburo.sato@example.com,080-9999-0000,03-4000-0004
c-005,Shiro,Takahashi,shiro.takahashi@example.com,090-2222-3333,03-9000-0009

S3 バケットを作成して CSV を配置します。

% aws s3 mb s3://er-rt-blog-123456789012 --region ap-northeast-1
make_bucket: er-rt-blog-123456789012

% aws s3 cp customers.csv s3://er-rt-blog-123456789012/input/customers.csv
upload: ./customers.csv to s3://er-rt-blog-123456789012/input/customers.csv

続いて Glue のデータベースとテーブルを作成します。テーブル定義では、ヘッダー行を読み飛ばすために skip.header.line.count を指定しています。

table-input.json
{
  "Name": "customers",
  "TableType": "EXTERNAL_TABLE",
  "Parameters": { "classification": "csv", "skip.header.line.count": "1" },
  "StorageDescriptor": {
    "Columns": [
      {"Name": "unique_id",  "Type": "string"},
      {"Name": "first_name", "Type": "string"},
      {"Name": "last_name",  "Type": "string"},
      {"Name": "email",      "Type": "string"},
      {"Name": "phone",      "Type": "string"},
      {"Name": "alt_phone",  "Type": "string"}
    ],
    "Location": "s3://er-rt-blog-123456789012/input/",
    "InputFormat": "org.apache.hadoop.mapred.TextInputFormat",
    "OutputFormat": "org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat",
    "SerdeInfo": {
      "SerializationLibrary": "org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe",
      "Parameters": { "field.delim": "," }
    }
  }
}
% aws glue create-database \
  --database-input '{"Name":"er_rt_blog"}'

% aws glue create-table \
  --database-name er_rt_blog \
  --table-input file://table-input.json

% aws glue get-table --database-name er_rt_blog --name customers \
  --query 'Table.{Name:Name,Location:StorageDescriptor.Location}'
{
    "Name": "customers",
    "Location": "s3://er-rt-blog-123456789012/input/"
}

IAM サービスロールの作成

AWS Entity Resolution が入力元の Glue テーブルと出力先の S3 バケットにアクセスするためのサービスロールを作成します。

信頼ポリシーでは entityresolution.amazonaws.com からの sts:AssumeRole を許可します。混乱した代理人問題(confused deputy problem)を避けるため aws:SourceAccountaws:SourceArn の条件を付けますが、ロール作成時点ではワークフローの ARN が確定していないため、aws:SourceArn はワイルドカードを指定します。

trust-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "Service": "entityresolution.amazonaws.com" },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": { "aws:SourceAccount": "123456789012" },
        "ArnLike": { "aws:SourceArn": "arn:aws:entityresolution:ap-northeast-1:123456789012:matchingworkflow/*" }
      }
    }
  ]
}

権限ポリシーには、Glue Data Catalog の読み取りと S3 の入出力に必要なものだけを含めます。

permissions-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "GluePermissions",
      "Effect": "Allow",
      "Action": ["glue:GetDatabase","glue:GetTable","glue:GetPartition","glue:GetPartitions","glue:GetSchema","glue:GetSchemaVersion","glue:BatchGetPartition"],
      "Resource": [
        "arn:aws:glue:ap-northeast-1:123456789012:catalog",
        "arn:aws:glue:ap-northeast-1:123456789012:database/er_rt_blog",
        "arn:aws:glue:ap-northeast-1:123456789012:table/er_rt_blog/customers"
      ]
    },
    {
      "Sid": "S3InputPermissions",
      "Effect": "Allow",
      "Action": ["s3:GetObject","s3:ListBucket","s3:GetBucketLocation"],
      "Resource": ["arn:aws:s3:::er-rt-blog-123456789012","arn:aws:s3:::er-rt-blog-123456789012/*"],
      "Condition": {"StringEquals": {"s3:ResourceAccount": ["123456789012"]}}
    },
    {
      "Sid": "S3OutputPermissions",
      "Effect": "Allow",
      "Action": ["s3:PutObject","s3:ListBucket","s3:GetBucketLocation"],
      "Resource": ["arn:aws:s3:::er-rt-blog-123456789012","arn:aws:s3:::er-rt-blog-123456789012/*"],
      "Condition": {"StringEquals": {"s3:ResourceAccount": ["123456789012"]}}
    }
  ]
}

ロールを作成し、以降の手順で使う ROLE_ARN に ARN を格納しておきます。

% ROLE_ARN=$(aws iam create-role \
  --role-name er_rt_blog_role \
  --assume-role-policy-document file://trust-policy.json \
  --query 'Role.Arn' --output text)

% aws iam put-role-policy \
  --role-name er_rt_blog_role \
  --policy-name er-access \
  --policy-document file://permissions-policy.json

% echo "${ROLE_ARN}"
arn:aws:iam::123456789012:role/er_rt_blog_role

スキーママッピングの作成

Advanced ルールタイプでは、各入力フィールドを一意の match key にマッピングする必要があります。ただし groupName でグループ化したフィールドは、同じ match key を共有できます。氏名は姓名の 2 列を 1 つの match key にまとめたいので、グループ化を使います。

schema-fields.json
[
  {"fieldName":"unique_id",  "type":"UNIQUE_ID"},
  {"fieldName":"first_name", "type":"NAME_FIRST",    "groupName":"name", "matchKey":"name"},
  {"fieldName":"last_name",  "type":"NAME_LAST",     "groupName":"name", "matchKey":"name"},
  {"fieldName":"email",      "type":"EMAIL_ADDRESS", "matchKey":"email"},
  {"fieldName":"phone",      "type":"PHONE_NUMBER",  "matchKey":"phone"},
  {"fieldName":"alt_phone",  "type":"PHONE_NUMBER",  "matchKey":"altphone"}
]

% aws entityresolution create-schema-mapping \
  --schema-name er_rt_blog_customers \
  --mapped-input-fields file://schema-fields.json
{
    "schemaName": "er_rt_blog_customers",
    "schemaArn": "arn:aws:entityresolution:ap-northeast-1:123456789012:schemamapping/er_rt_blog_customers",
    "mappedInputFields": [
        {
            "fieldName": "unique_id",
            "type": "UNIQUE_ID"
        },
        {
            "fieldName": "first_name",
            "type": "NAME_FIRST",
            "groupName": "name",
            "matchKey": "name"
        },
        {
            "fieldName": "last_name",
            "type": "NAME_LAST",
            "groupName": "name",
            "matchKey": "name"
        },
        {
            "fieldName": "email",
            "type": "EMAIL_ADDRESS",
            "matchKey": "email"
        },
        {
            "fieldName": "phone",
            "type": "PHONE_NUMBER",
            "matchKey": "phone"
        },
        {
            "fieldName": "alt_phone",
            "type": "PHONE_NUMBER",
            "matchKey": "altphone"
        }
    ]
}

phonealt_phone はどちらも PHONE_NUMBER 型ですが、match key を別にしておけば問題なく作成できました。実際に作成したスキーママッピングです。

20260729-aws-entity-resolution-rules-rt-match-1

ワークフロー作成でつまずいた点

続いて、Advanced ルールタイプかつ enableRealTimeMatching を有効にしたワークフローを作成します。

resolution-advanced.json
{
  "resolutionType": "RULE_MATCHING",
  "ruleConditionProperties": {
    "rules": [
      { "ruleName": "Rule1", "condition": "Exact(email) AND Exact(name)" },
      { "ruleName": "Rule2", "condition": "(Exact(name) AND Exact(phone)) OR ExactManyToMany(phone, altphone)" }
    ]
  },
  "enableRealTimeMatching": true
}

入力元には作成済みの Glue テーブルとスキーママッピングを、出力先には S3 バケットを指定します。

input-source.json
[
  {
    "inputSourceARN": "arn:aws:glue:ap-northeast-1:123456789012:table/er_rt_blog/customers",
    "schemaName": "er_rt_blog_customers",
    "applyNormalization": true
  }
]
output-source.json
[
  {
    "outputS3Path": "s3://er-rt-blog-123456789012/output/",
    "applyNormalization": true,
    "output": [
      {"name": "unique_id",  "hashed": false},
      {"name": "first_name", "hashed": false},
      {"name": "last_name",  "hashed": false},
      {"name": "email",      "hashed": false},
      {"name": "phone",      "hashed": false},
      {"name": "alt_phone",  "hashed": false}
    ]
  }
]

ところが、ここでエラーが返されました。

% aws entityresolution create-matching-workflow \
  --workflow-name er_rt_blog_advanced \
  --input-source-config file://input-source.json \
  --output-source-config file://output-source.json \
  --resolution-techniques file://resolution-advanced.json \
  --role-arn "${ROLE_ARN}"

aws: [ERROR]: An error occurred (AccessDeniedException) when calling the CreateMatchingWorkflow operation: The service does not have access to read your data in Glue. Please check IAM role for the service to read your data.

IAM ロールには Glue の読み取り権限と S3 の読み書き権限を付与済みでしたが、解消しません。原因は AWS Lake Formation でした。

% aws lakeformation get-data-lake-settings
{
    "DataLakeSettings": {
        "DataLakeAdmins": [
            {
                "DataLakePrincipalIdentifier": "arn:aws:iam::123456789012:role/cm-ishikawa.satoru"
            },
            {
                "DataLakePrincipalIdentifier": "arn:aws:iam::123456789012:role/service-role/AmazonSageMakerAdminIAMExecutionRole_1"
            }
        ],
        "ReadOnlyAdmins": [],
        "CreateDatabaseDefaultPermissions": [],
        "CreateTableDefaultPermissions": [],
        "Parameters": {
            "CROSS_ACCOUNT_VERSION": "1",
            "SET_CONTEXT": "FALSE"
        },
        "AllowExternalDataFiltering": false,
        "AllowFullTableExternalDataAccess": true,
        "ExternalDataFilteringAllowList": []
    }
}

CreateDatabaseDefaultPermissionsCreateTableDefaultPermissions が空になっています。この状態のアカウントでは、新規作成したデータベースやテーブルに IAMAllowedPrincipals の既定権限が付与されないため、IAM ポリシーだけではアクセスできず、Lake Formation 側の権限付与が別途必要になります。

Entity Resolution のサービスロールに Lake Formation の権限を付与します。

% aws lakeformation grant-permissions \
  --principal "DataLakePrincipalIdentifier=${ROLE_ARN}" \
  --resource '{"Database":{"CatalogId":"123456789012","Name":"er_rt_blog"}}' \
  --permissions DESCRIBE

% aws lakeformation grant-permissions \
  --principal "DataLakePrincipalIdentifier=${ROLE_ARN}" \
  --resource '{"Table":{"CatalogId":"123456789012","DatabaseName":"er_rt_blog","Name":"customers"}}' \
  --permissions SELECT DESCRIBE

改めてワークフローを作成すると、今度は成功しました。

% aws entityresolution create-matching-workflow \
  --workflow-name er_rt_blog_advanced \
  --input-source-config file://input-source.json \
  --output-source-config file://output-source.json \
  --resolution-techniques file://resolution-advanced.json \
  --role-arn "${ROLE_ARN}"
{
    "workflowName": "er_rt_blog_advanced",
    "workflowArn": "arn:aws:entityresolution:ap-northeast-1:123456789012:matchingworkflow/er_rt_blog_advanced",
    "inputSourceConfig": [
        {
            "inputSourceARN": "arn:aws:glue:ap-northeast-1:123456789012:table/er_rt_blog/customers",
            "schemaName": "er_rt_blog_customers",
            "applyNormalization": true
        }
    ],
    "outputSourceConfig": [
        {
            "outputS3Path": "s3://er-rt-blog-123456789012/output/",
            "output": [
                {
                    "name": "unique_id",
                    "hashed": false
                },
                {
                    "name": "first_name",
                    "hashed": false
                },
                {
                    "name": "last_name",
                    "hashed": false
                },
                {
                    "name": "email",
                    "hashed": false
                },
                {
                    "name": "phone",
                    "hashed": false
                },
                {
                    "name": "alt_phone",
                    "hashed": false
                }
            ],
            "applyNormalization": true
        }
    ],
    "resolutionTechniques": {
        "resolutionType": "RULE_MATCHING",
        "ruleConditionProperties": {
            "rules": [
                {
                    "ruleName": "Rule1",
                    "condition": "Exact(email) AND Exact(name)"
                },
                {
                    "ruleName": "Rule2",
                    "condition": "(Exact(name) AND Exact(phone)) OR ExactManyToMany(phone, altphone)"
                }
            ]
        },
        "enableRealTimeMatching": true
    },
    "roleArn": "arn:aws:iam::123456789012:role/er_rt_blog_role"
}

ExactExactManyToManyAND / OR / 括弧で組み合わせた条件が、そのまま受け付けられています。今回のアップデートの中核部分が確認できました。

実際に作成したマッピングルールです。

20260729-aws-entity-resolution-rules-rt-match-2

StartMatchingJob なしで GenerateMatchId を呼んでみる

冒頭で触れたドキュメントの矛盾を確かめます。StartMatchingJob を一度も実行していない状態で、いきなり GenerateMatchId を呼び出します。

rec-rt001.json
[{"inputSourceARN":"arn:aws:glue:ap-northeast-1:123456789012:table/er_rt_blog/customers",
  "uniqueId":"rt-001",
  "recordAttributeMap":{
    "first_name":"Taro","last_name":"Yamada","email":"taro.yamada@example.com",
    "phone":"090-1111-2222","alt_phone":"03-1000-0001"}}]
% aws entityresolution generate-match-id \
  --workflow-name er_rt_blog_advanced \
  --records file://rec-rt001.json \
  --processing-type CONSISTENT
{
    "matchGroups": [
        {
            "records": [
                {
                    "inputSourceARN": "arn:aws:glue:ap-northeast-1:123456789012:table/er_rt_blog/customers",
                    "recordId": "rt-001"
                }
            ],
            "matchId": "314059b866ec4e2fa22bd6ff2bcc808b",
            "matchRule": "NoRule"
        }
    ],
    "failedRecords": []
}

成功しました。バッチジョブを一度も動かしていなくても GenerateMatchId は呼び出せます。既存のマッチ相手がいないため matchRuleNoRule となり、新しい Match ID が採番されました。

ドキュメントにある「事前に StartMatchingJob を成功させておく必要がある」という注記は、Advanced リアルタイムワークフローには当てはまらないようです。

マッチングの判定を確認する

同じ氏名・メールアドレスで、電話番号だけが異なるレコードを投入します。Rule1(Exact(email) AND Exact(name))で一致するはずです。

rec-rt002.json
[{"inputSourceARN":"arn:aws:glue:ap-northeast-1:123456789012:table/er_rt_blog/customers",
  "uniqueId":"rt-002",
  "recordAttributeMap":{
    "first_name":"Taro","last_name":"Yamada","email":"taro.yamada@example.com",
    "phone":"080-0000-1111","alt_phone":"03-5000-0005"}}]
% aws entityresolution generate-match-id \
  --workflow-name er_rt_blog_advanced \
  --records file://rec-rt002.json \
  --processing-type CONSISTENT
{
    "matchGroups": [
        {
            "records": [
                {
                    "inputSourceARN": "arn:aws:glue:ap-northeast-1:123456789012:table/er_rt_blog/customers",
                    "recordId": "rt-002"
                }
            ],
            "matchId": "314059b866ec4e2fa22bd6ff2bcc808b",
            "matchRule": "Rule1"
        }
    ],
    "failedRecords": []
}

rt-001 と同じ matchId が返り、matchRule には一致したルール名 Rule1 が入っています。どのルールで名寄せされたのかがレスポンスで分かるため、マッチング結果の根拠を追跡できます。

ExactManyToMany の挙動を確認する

ExactManyToMany は、公式ドキュメントに「指定した match key のすべての組み合わせを評価する」と説明されています。実際にフィールドをまたいだ一致でもマッチするのかを確認します。

検証には 3 件のレコードを使います。03-1000-0001 という電話番号を、レコードごとに phonealt_phone のどちらに置くかを変えるのが仕掛けです。

レコード 氏名・メール phone alt_phone 狙い
rt-003 Ichiro Suzuki 03-1000-0001 03-6000-0006 起点となるグループを作る
rt-004 Kenji Watanabe 03-1000-0001 03-7000-0007 rt-003 と phone 同士が一致
rt-005 Yuki Nakamura 050-1111-0000 03-1000-0001 rt-003 の phonealt_phone が一致

3 件とも氏名とメールアドレスはすべて別人にしてあります。こうしておけば Rule1(Exact(email) AND Exact(name))も Rule2 の前半(Exact(name) AND Exact(phone))も成立しないため、マッチした場合は ExactManyToMany が効いたと断定できます。

rec-rt003.json
[{"inputSourceARN":"arn:aws:glue:ap-northeast-1:123456789012:table/er_rt_blog/customers",
  "uniqueId":"rt-003",
  "recordAttributeMap":{
    "first_name":"Ichiro","last_name":"Suzuki","email":"ichiro.suzuki@example.com",
    "phone":"03-1000-0001","alt_phone":"03-6000-0006"}}]
rec-rt004.json
[{"inputSourceARN":"arn:aws:glue:ap-northeast-1:123456789012:table/er_rt_blog/customers",
  "uniqueId":"rt-004",
  "recordAttributeMap":{
    "first_name":"Kenji","last_name":"Watanabe","email":"kenji.watanabe@example.com",
    "phone":"03-1000-0001","alt_phone":"03-7000-0007"}}]
rec-rt005.json
[{"inputSourceARN":"arn:aws:glue:ap-northeast-1:123456789012:table/er_rt_blog/customers",
  "uniqueId":"rt-005",
  "recordAttributeMap":{
    "first_name":"Yuki","last_name":"Nakamura","email":"yuki.nakamura@example.com",
    "phone":"050-1111-0000","alt_phone":"03-1000-0001"}}]

まず rt-003 を投入します。この rt-003 の phone03-1000-0001 で、これは rt-001 の alt_phone と同じ値です。狙いどおりなら rt-001 のグループに合流するはずでした。

% aws entityresolution generate-match-id \
  --workflow-name er_rt_blog_advanced \
  --records file://rec-rt003.json \
  --processing-type CONSISTENT
{
    "matchGroups": [
        {
            "records": [
                {
                    "inputSourceARN": "arn:aws:glue:ap-northeast-1:123456789012:table/er_rt_blog/customers",
                    "recordId": "rt-003"
                }
            ],
            "matchId": "a16dd2dc5159466994f924810321a373",
            "matchRule": "NoRule"
        }
    ],
    "failedRecords": []
}

ところが合流せず、新しいグループが作られました。この点は後述します。ここでは、この 0adc112c033c4549a19679bd66b8e54d を基準に話を進めます。

次に rt-004 を投入します。rt-003 と phone 同士が一致するケースです。

% aws entityresolution generate-match-id \
  --workflow-name er_rt_blog_advanced \
  --records file://rec-rt004.json \
  --processing-type CONSISTENT

{
    "matchGroups": [
        {
            "records": [
                {
                    "inputSourceARN": "arn:aws:glue:ap-northeast-1:123456789012:table/er_rt_blog/customers",
                    "recordId": "rt-004"
                }
            ],
            "matchId": "a16dd2dc5159466994f924810321a373",
            "matchRule": "Rule2"
        }
    ],
    "failedRecords": []
}

最後に rt-005 です。alt_phone だけが rt-003 の phone と一致する、フィールドをまたいだケースになります。

% aws entityresolution generate-match-id \
  --workflow-name er_rt_blog_advanced \
  --records file://rec-rt005.json \
  --processing-type CONSISTENT

{
    "matchGroups": [
        {
            "records": [
                {
                    "inputSourceARN": "arn:aws:glue:ap-northeast-1:123456789012:table/er_rt_blog/customers",
                    "recordId": "rt-005"
                }
            ],
            "matchId": "a16dd2dc5159466994f924810321a373",
            "matchRule": "Rule2"
        }
    ],
    "failedRecords": []
}

rt-004 も rt-005 も rt-003 と同じ matchId が返りました。phone 同士の一致だけでなく、phonealt_phone という異なるフィールド間の一致でも Rule2 として同一グループに合流しています。同じ人物が、あるシステムでは携帯電話を主番号に、別のシステムでは副番号に登録しているようなケースを、1 つのルールで拾えることになります。

ウォーターフォールの落とし穴

ここで、想定と異なる挙動に遭遇しました。

Rule1 ですでに成立しているマッチグループ(rt-001 と rt-002 のグループ)に対して、Rule2 の条件を満たすレコードを投入しても、そのグループに合流しなかったのです。

rt-006 として、rt-001 と phone が完全に一致するレコードを投入しました。Rule2 の ExactManyToMany(phone, altphone) が成立するはずです。

rec-rt006.json
[{"inputSourceARN":"arn:aws:glue:ap-northeast-1:123456789012:table/er_rt_blog/customers",
  "uniqueId":"rt-006",
  "recordAttributeMap":{
    "first_name":"Sora","last_name":"Kimura","email":"sora.kimura@example.com",
    "phone":"090-1111-2222","alt_phone":"03-8000-0008"}}]
% aws entityresolution generate-match-id \
  --workflow-name er_rt_blog_advanced \
  --records file://rec-rt006.json \
  --processing-type CONSISTENT
{
    "matchGroups": [
        {
            "records": [
                {
                    "inputSourceARN": "arn:aws:glue:ap-northeast-1:123456789012:table/er_rt_blog/customers",
                    "recordId": "rt-006"
                }
            ],
            "matchId": "de731cd48996490193443b0a78a139c0",
            "matchRule": "NoRule"
        }
    ],
    "failedRecords": []
}

合流せず、新しい Match ID が採番されました。rt-002 と phone が一致するレコード(rt-008)でも同じ結果でした。クロスフィールド・同一フィールドのいずれでも、計 3 回とも合流しませんでした。

rec-rt008.json
[{"inputSourceARN":"arn:aws:glue:ap-northeast-1:123456789012:table/er_rt_blog/customers",
  "uniqueId":"rt-008",
  "recordAttributeMap":{
    "first_name":"Mei","last_name":"Inoue","email":"mei.inoue@example.com",
    "phone":"080-0000-1111","alt_phone":"03-8888-8888"}}]
% aws entityresolution generate-match-id \
  --workflow-name er_rt_blog_advanced \
  --records file://rec-rt008.json \
  --processing-type CONSISTENT
{
    "matchGroups": [
        {
            "records": [
                {
                    "inputSourceARN": "arn:aws:glue:ap-northeast-1:123456789012:table/er_rt_blog/customers",
                    "recordId": "rt-008"
                }
            ],
            "matchId": "6c517f2edf114df0a494c8f23ec0a61e",
            "matchRule": "NoRule"
        }
    ],
    "failedRecords": []
}

一方で、Rule1 が成立していないグループ(matchRuleNoRule のまま作られたグループ)に対しては、Rule2 で問題なく合流できます。実際、rt-006 のグループには、その後投入した rt-007 が Rule2 で合流しました。

rec-rt007.json
[{"inputSourceARN":"arn:aws:glue:ap-northeast-1:123456789012:table/er_rt_blog/customers",
  "uniqueId":"rt-007",
  "recordAttributeMap":{
    "first_name":"Rin","last_name":"Kobayashi","email":"rin.kobayashi@example.com",
    "phone":"03-9999-9999","alt_phone":"090-1111-2222"}}]
% aws entityresolution generate-match-id \
  --workflow-name er_rt_blog_advanced \
  --records file://rec-rt007.json \
  --processing-type CONSISTENT
{
    "matchGroups": [
        {
            "records": [
                {
                    "inputSourceARN": "arn:aws:glue:ap-northeast-1:123456789012:table/er_rt_blog/customers",
                    "recordId": "rt-007"
                }
            ],
            "matchId": "de731cd48996490193443b0a78a139c0",
            "matchRule": "Rule2"
        }
    ],
    "failedRecords": []
}

検証したレコードの帰属をまとめると次のようになります。

ルールベースマッチングは、公式ドキュメントでウォーターフォール(階層型)のルール適用であると説明されています。優先度の高いルールで成立したマッチグループは、優先度の低いルールでは再評価されない、という挙動と整合します。

https://docs.aws.amazon.com/entityresolution/latest/userguide/glossary.html

したがって、優先度の高いルールで確実に拾いたい条件を先に置き、緩い条件を後ろに置くというルール順序の設計は、リアルタイムマッチングでもそのまま効いてきます。逆に言えば、後ろのルールで拾えるはずと考えて設計すると、期待した名寄せが起きないことがあります。

制約を確認する

ドキュメントに記載されている制約を、実際にエラーとして再現させます。

まず、Fuzzy 関数を含む条件で enableRealTimeMatching を有効にしたワークフローを作成してみます。

rec-rt007.json
{
  "resolutionType": "RULE_MATCHING",
  "ruleConditionProperties": {
    "rules": [
      { "ruleName": "Rule1", "condition": "Exact(email) AND Levenshtein(name, 2)" }
    ]
  },
  "enableRealTimeMatching": true
}
% aws entityresolution create-matching-workflow \
  --workflow-name er_rt_blog_fuzzy \
  --input-source-config file://input-source.json \
  --output-source-config file://output-source.json \
  --resolution-techniques file://resolution-fuzzy-rt.json \
  --role-arn "${ROLE_ARN}"

aws: [ERROR]: An error occurred (ValidationException) when calling the CreateMatchingWorkflow operation: The enableRealTimeMatching parameter doesn't support fuzzy matching functions (Cosine, Levenshtein, Soundex) or EmptyValues=Ignore. Update your rules to use only exact matching functions (Exact or ExactManyToMany) without EmptyValues=Ignore.

想定どおり拒否されました。エラーメッセージが制約の内容をそのまま伝えてくれるので分かりやすいです。

次に、リアルタイムマッチングを有効にしたワークフローに対してバッチジョブを実行してみます。

% aws entityresolution start-matching-job --workflow-name er_rt_blog_advanced

aws: [ERROR]: An error occurred (ValidationException) when calling the StartMatchingJob operation: You can't run matching jobs on this workflow because real time matching is enabled. Create a workflow without real-time matching to run matching jobs, or use the GenerateMatchId API instead.

こちらも想定どおりです。「use the GenerateMatchId API instead」と明記されており、冒頭の矛盾に対する答えがエラーメッセージ側に書かれていました。リアルタイム用ワークフローとバッチ用ワークフローは、用途ごとに分けて作る設計になります。

processingType は CONSISTENT のみ

GenerateMatchId には processingType というパラメータがあり、公式ドキュメントでは次の 3 種類が説明されています。

processingType コンソール表示 特性
CONSISTENT(デフォルト) Consistent 精度が最も高く、応答時間は相対的に遅い
EVENTUAL Background 初回応答が速く、更新レコードはバックグラウンドで保存される
EVENTUAL_NO_LOOKUP Quick ID generation 既存 Match ID を検索せず新規採番する最速の方式

3 種類の応答時間を比較しようとしたところ、EVENTUAL でエラーになりました。レコードは既出の rec-rt001.json を流用し、--processing-type だけを変えています。

% aws entityresolution generate-match-id \
  --workflow-name er_rt_blog_advanced \
  --records file://rec-rt001.json \
  --processing-type EVENTUAL

aws: [ERROR]: An error occurred (ValidationException) when calling the GenerateMatchId operation: GenerateMatchId requires CONSISTENT processing type for advanced rule-based workflows. Set the processing type to CONSISTENT.

EVENTUAL_NO_LOOKUP でも同じエラーです。

% aws entityresolution generate-match-id \
  --workflow-name er_rt_blog_advanced \
  --records file://rec-rt001.json \
  --processing-type EVENTUAL_NO_LOOKUP

aws: [ERROR]: An error occurred (ValidationException) when calling the GenerateMatchId operation: GenerateMatchId requires CONSISTENT processing type for advanced rule-based workflows. Set the processing type to CONSISTENT.

Advanced ルールタイプでは CONSISTENT しか指定できません。 なお、いずれもバリデーションで弾かれるため、レコードは登録されません。

GenerateMatchId のドキュメントには 3 種類が並記されているだけで、Advanced ルールタイプでの制限には触れられていません。応答速度を優先して EVENTUAL を前提に設計していると、実装時につまずくポイントになりそうです。

レイテンシを実測する

CONSISTENT で 10 回連続して呼び出し、応答時間を計測しました。TLS ハンドシェイクや認証情報の解決を計測から除くため、事前にウォームアップを 1 回挟んでいます。毎回異なるレコードを投入したいので、AWS CLI ではなく boto3 のスクリプトを使いました。

bench.py
import boto3
import statistics
import time

REGION = "ap-northeast-1"
WORKFLOW = "er_rt_blog_advanced"
TABLE_ARN = "arn:aws:glue:ap-northeast-1:123456789012:table/er_rt_blog/customers"

client = boto3.client("entityresolution", region_name=REGION)

def record(seq):
    return {
        "inputSourceARN": TABLE_ARN,
        "uniqueId": f"bench-{seq}",
        "recordAttributeMap": {
            "first_name": f"Bench{seq}",
            "last_name": f"User{seq}",
            "email": f"bench{seq}@example.net",
            "phone": f"062-{seq:04d}-0000",
            "alt_phone": f"063-{seq:04d}-0000",
        },
    }

def call(seq):
    started = time.perf_counter()
    client.generate_match_id(
        workflowName=WORKFLOW,
        records=[record(seq)],
        processingType="CONSISTENT",
    )
    return (time.perf_counter() - started) * 1000

call(0)
print("warmup done\n")

latencies = [call(seq) for seq in range(1, 11)]
print("CONSISTENT (n=10) の実測レイテンシ [ms]")
print("  " + "  ".join(f"{ms:.0f}" for ms in latencies))
print(
    f"  min={min(latencies):.0f}"
    f"  median={statistics.median(latencies):.0f}"
    f"  mean={statistics.mean(latencies):.0f}"
    f"  max={max(latencies):.0f}"
)
% python3 bench.py
warmup done

CONSISTENT (n=10) の実測レイテンシ [ms]
  101  107  97  108  115  91  126  78  109  70
  min=70  median=104  mean=100  max=126

中央値で 104 ミリ秒でした。国内のローカル PC からインターネット経由で東京リージョンを呼び出した値のため、ネットワークの往復時間を含んでいます。同一リージョンの Lambda や EC2 から呼び出せば、さらに短くなることが見込まれます。

いずれにせよ、バッチ処理で数分から数時間かかっていた処理が、同期的な API 呼び出し 1 回で返ってくる点は大きな変化です。

考察

今回の検証で得られた知見を整理します。

ドキュメントの矛盾は GenerateMatchId 側が正しい

Advanced リアルタイムワークフローでは、StartMatchingJob を事前に実行する必要はありません。バッチ処理を挟まずに、いきなり GenerateMatchId から使い始められます。リアルタイム用とバッチ用でワークフローを分けて作る、という設計が前提になります。

Advanced リアルタイムで使える条件は限定的

リアルタイム対応したのは Exact 系の関数(ExactExactManyToMany)を AND / OR / 括弧で組み合わせたルールセットのみです。Fuzzy 関数(CosineLevenshteinSoundex)と EmptyValues=Ignore は、ワークフロー作成時点で拒否されます。表記ゆれを吸収したい要件がある場合は、引き続きバッチ処理での運用が必要です。

processingType は CONSISTENT に固定される

これはドキュメントに記載がなく、実行して初めて分かった制約です。EVENTUAL 系による高速化は Simple ルールタイプ向けの選択肢と考えたほうがよさそうです。

ルール順序の設計はリアルタイムでも重要

優先度の高いルールで成立したマッチグループには、優先度の低いルールでは合流できませんでした。ウォーターフォールの適用順序が、そのまま名寄せ結果を左右します。ルール設計時には、どの条件を先に置くかを慎重に検討する必要があります。

Lake Formation が有効なアカウントでは追加の権限付与が必要

IAM ポリシーだけを整えても AccessDeniedException になります。エラーメッセージは「check IAM role」と案内しますが、実際には Lake Formation の DESCRIBE / SELECT 権限が原因でした。Glue Data Catalog を Lake Formation で管理しているアカウントでは、最初にここでつまずく可能性が高いです。

コスト

AWS Entity Resolution のルールベースマッチングは 1,000 レコード処理あたり 0.25 USD で、無料利用枠はありません。リージョンによる価格差もありません。今回の検証は 40 件程度の GenerateMatchId 呼び出しで済んだため、0.01 USD 未満に収まりました。

https://aws.amazon.com/entity-resolution/pricing/

最後に

AWS Entity Resolution の Advanced ルールタイプで、GenerateMatchId API によるリアルタイムマッチングが利用できるようになりました。enableRealTimeMatchingtrue にするだけで、複雑なルールセットを同期的に評価できます。実測では中央値 104 ミリ秒で応答が返りました。

一方で、Fuzzy 関数が使えないこと、processingTypeCONSISTENT に限られること、バッチジョブと併用できないことなど、設計時に押さえておくべき制約もあります。特に processingType の制約は公式ドキュメントに記載がないため、注意が必要です。

不正検知やリアルタイムのアカウント検索のように、厳密な突合をその場で行いたい要件をお持ちの方は、まず既存のルール定義が Exact 系の関数だけで表現できるかを確認するところから始めてみてはいかがでしょうか。

合わせて読みたい

https://dev.classmethod.jp/articles/aws-entity-resolution-rule-base-matching/

https://dev.classmethod.jp/articles/aws-entity-resolution-ml-base-matching/

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事