Claude CodeでSolrを移行する — OpenSearchへのマッピング変換と結果照合
schema.xmlのfieldType・copyField・analyzerをOpenSearchのマッピングへ写し、同じクエリの上位10件を両方で取って差を見る手順です。Migration Assistantの対象範囲も押さえます。
SolrからOpenSearchへの移行で手間がかかるのは、データ転送より「schema.xmlの意味をマッピングへ写し、検索結果が変わっていないか確かめる」部分です。
Claude Codeには、この写経と検証スクリプトの下書きを任せられます。一方で、結果が同じとみなしてよいかの判定は人間の仕事です。この記事は、判定材料を件数と上位10件のIDに絞る流れを、公式ドキュメントの仕様に沿って組みます。
最初の分岐: Migration Assistantで運ぶか、マッピングを手で作るか
OpenSearchのMigration Assistantには、Solrを移行元にするワークフローがあります。対応するのはApache Solr 6.x〜9.x(SolrCloudもスタンドアロンも可)から、OpenSearch 1.x・2.x・3.xへの移行です。
仕組みは、Solrのバックアップ(Luceneのセグメントファイル)をSolrReaderが読み、schema.xmlのフィールド型をOpenSearchのマッピングへ翻訳し、文書をバルクで投入する流れです。翻訳される型は次の8種です。
| Solrのフィールド型 | OpenSearchのマッピング |
|---|---|
solr.TextField | OpenSearchのマッピングtext |
solr.StrField | OpenSearchのマッピングkeyword |
solr.IntPointField | OpenSearchのマッピングinteger |
solr.LongPointField | OpenSearchのマッピングlong |
solr.FloatPointField | OpenSearchのマッピングfloat |
solr.DoublePointField | OpenSearchのマッピングdouble |
solr.BoolField | OpenSearchのマッピングboolean |
solr.DatePointField | OpenSearchのマッピングdate |
移行されない対象も明記されています。Solrプラグイン、ZooKeeperの設定、クエリのトラフィックです。Capture and Replay(ライブトラフィックの再生)はSolr向けには使えません。移行中も書き込みが続くシステムでは、バックフィルの間だけ元のSolrへの書き込みを止める前提になります。
つまり、Migration Assistantを使っても、copyField・dynamicField・analyzerの扱いと、アプリ側のクエリの書き換えは残ります。Claude Codeの出番はそこです。
移行先がElasticsearchの場合、Solrを移行元にした手順はMigration Assistantの説明に載っていません。マッピングとanalyzerは手で作る形になります。ElasticsearchにもdynamicテンプレートとカスタムAnalyzerの仕組みがあり、次節以降の考え方は同じです。
Claude Codeに渡す前の準備
Claude Codeは、コードベースを読み、ファイルを編集し、コマンドを実行します。公式の使い方集には、変更を書き込む前に計画だけ出させるplan modeがあります。claude --permission-mode planで起動するか、セッション中にShift+Tabで切り替えます。
schema.xmlの変換方針を決める最初の1回は、このモードで案だけ見るのが安全です。plan modeの使い方はPlanモードの解説にあります。
CLAUDE.mdには、リポジトリを読めば分かることは書かず、Claudeが自力で見つけられない約束事だけを書きます。次は、この移行向けの例です。
# Solr→OpenSearch移行
- 変換元: solr/conf/schema.xml と solrconfig.xml(読み取り専用)
- 変換先: opensearch/mappings/ 配下にJSONで置く
- 検証: scripts/compare.sh <クエリ一覧> で上位10件のIDを比較する
- 本番のSolrとOpenSearchには書き込まない(検証は読み取りのみ)
- 判定が割れた項目は結果を捨てずに diff-report.txt に残すCLAUDE.mdに書いた行をClaudeが守るかは、毎ターンのClaudeの判断です。公式のベストプラクティスも、例外を許さないルールはhookやdenyルールへ移すよう勧めています。本番へ書き込むAPIを止めたいなら、CLAUDE.mdの1行に頼らず、権限の拒否側に入れる方法があります。
schema.xmlをマッピングへ写す
fieldTypeとfieldの対応
型の対応は前節の表が基準です。表に無い型(TrieIntFieldなどの非推奨型や、独自のfieldType)は自動では翻訳されません。Claude Codeにschema.xmlを読ませて「表にある型」と「表に無い型」に振り分けさせると、手作業の範囲がはっきりします。
uniqueKeyは文書の識別子になるフィールドです。Solrの仕様では、解析されない型でなければならず、*PointField型も使えません。Migration AssistantがuniqueKeyをOpenSearchの_idにどう写すかは、移行の説明ページに記載がありません。移行後に_countの値と、サンプルIDの引き直しで確かめてください。
copyFieldはcopy_toに置き換わる
SolrのcopyFieldは、複数フィールドの内容を1つの検索用フィールドに集める用途でよく使われます。
<copyField source="title" dest="text"/>
<copyField source="body" dest="text"/>OpenSearchでは、コピー元側のフィールドにcopy_toを書きます。
{
"mappings": {
"properties": {
"title": { "type": "text", "copy_to": "text_all" },
"body": { "type": "text", "copy_to": "text_all" },
"text_all": { "type": "text" }
}
}
}挙動の近い点と違う点は、次のとおりです。
| 観点 | SolrのcopyField | OpenSearchのcopy_to |
|---|---|---|
| コピーされるもの | SolrのcopyField解析前の値 | OpenSearchのcopy_to生の値(解析後のトークンではない) |
| 連鎖 | SolrのcopyField不可。コピー先からさらにコピーはできない | OpenSearchのcopy_to不可。元のフィールドから直接複数へ書く |
| 上限 | SolrのcopyFieldmaxCharsでコピーする文字数を制限できる | OpenSearchのcopy_to該当ページにmaxChars相当の記載はない |
| ワイルドカード | SolrのcopyFieldsourceとdestにアスタリスクを使える | OpenSearchのcopy_toマッピングのフィールド単位で指定する |
maxChars付きのcopyFieldがあるスキーマでは、全文がtext_allに入って索引が膨らむ可能性があります。Claude Codeにはschema.xmlからmaxCharsを使っている行を一覧化させておきます。移行後の検索結果に差が出たとき、原因の切り分けが速くなります。
dynamicFieldはdynamic_templatesに置き換わる
<dynamicField name="*_i" type="int" .../>のような定義は、OpenSearchではdynamic_templatesに写します。
{
"mappings": {
"dynamic_templates": [
{
"ints": {
"match": "*_i",
"mapping": { "type": "integer" }
}
}
]
}
}OpenSearchの仕様で押さえておく点が3つあります。
- テンプレートは配列の順に評価され、最初に一致したものが適用される。具体的な条件のテンプレートを先に置く
- すでにマッピングにあるフィールドには適用されない
- 値が
nullや空配列のフィールドは、最初に値が入るまでマッピングされず、テンプレートも評価されない
analyzerの移し替え
SolrのfieldTypeは、<analyzer type="index">と<analyzer type="query">に分けて、tokenizerとfilterを順に並べます。OpenSearchでは、settings.analysisにカスタムanalyzerを定義し、フィールド側でanalyzerとsearch_analyzerを指定します。カスタムanalyzerは、文字フィルター(0個以上)、tokenizer(1個)、トークンフィルター(0個以上)の組み合わせです。
Solr公式の例では、インデックス時にlowercaseとシノニムなどを適用し、クエリ時はlowercaseだけを適用します。シノニムを検索側に置く構成を、OpenSearchで書くと次の形になります(例示用で、シノニムの規則は仮のものです)。
{
"settings": {
"analysis": {
"filter": {
"my_syn": {
"type": "synonym_graph",
"synonyms": ["sofa, couch"]
}
},
"analyzer": {
"name_index": {
"tokenizer": "standard",
"filter": ["lowercase"]
},
"name_search": {
"tokenizer": "standard",
"filter": ["lowercase", "my_syn"]
}
}
}
},
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "name_index",
"search_analyzer": "name_search"
}
}
}
}注意したいのは、シノニムを置く側です。OpenSearchのmatchクエリの仕様では、フィールドの検索側analyzerにsynonym_graphフィルターがあるとき、クエリの語がシノニムへ展開されます。Solrの例のようにインデックス時にシノニムを展開していたスキーマを、そのまま写すと挙動が変わります。どちらに置くかは、シノニムを更新したときに再インデックスが要るかどうかも左右するので、人が決めてください。
Solrには、ワイルドカードやプレフィックス検索向けのmultitermanalyzerもあります。OpenSearchのカスタムanalyzerの説明には、ワイルドカード・プレフィックス検索用のanalyzerを別に指定する項目がありません。この種のクエリがあるなら、OpenSearch側でも同じ語を投げ、件数と_analyzeのトークンを確かめます。
analyzerの確認はトークン列で行う
analyzerを書き終えたら、同じ文字列をOpenSearchの_analyzeに通します。
curl -s -XGET "http://localhost:9200/my_index/_analyze" \
-H 'Content-Type: application/json' \
-d '{"analyzer":"name_search","text":"Sofa BED"}'期待するトークン列は、Solr側のanalyzer定義から人が書き出します。Claude Codeには「このトークン表と_analyzeの出力を突き合わせるスクリプト」を書かせると、確認を何度でも繰り返せます。
検索結果の差を見る
型とanalyzerが写せても、クエリの書き方が違えば結果は変わります。Migration Assistantの説明でも、クエリ層はアプリ側で書き換える前提です。
solrconfigのクエリパーサーがedismaxなら、qf(検索対象フィールドと重み)はmulti_matchのfieldsに写せます。multi_matchのフィールドでもtitle^4のように^で重みを付けます。typeを指定しなければbest_fieldsで、最も合ったフィールドのスコアを使います。
AND/ORの扱いにも差があります。Solrのmmは、q.opがANDでない限り既定が0%(すべての語が任意)です。OpenSearchのmatchはoperatorでandとorを選びます。この既定値の取り違えが、件数の差として最初に現れます。
比較の手順は次のとおりです。
- 実際に使われている検索語を、ログから100〜300件ほど抜き出す
- 各語をSolrとOpenSearchの両方に投げ、上位10件のIDを取る
- 一致した件数と、順位が入れ替わった語を一覧にする
- 差分が出た語だけをClaude Codeに読ませ、原因(analyzer・重み・演算子)の候補を挙げさせる
比較スクリプトの骨格は、次のようなものになります(例示用。フィールド名・URLは自分の環境に置き換えます)。
while read -r q; do
s=$(curl -s -G "http://solr:8983/solr/items/select" \
--data-urlencode "defType=edismax" --data-urlencode "qf=title^4 body" \
--data-urlencode "q=$q" --data-urlencode "fl=id" --data-urlencode "rows=10" \
| jq -r '.response.docs[].id' | paste -sd, -)
o=$(curl -s "http://opensearch:9200/items/_search" \
-H 'Content-Type: application/json' \
-d "$(jq -n --arg q "$q" '{size:10,_source:["id"],query:{multi_match:{query:$q,fields:["title^4","body"]}}}')" \
| jq -r '.hits.hits[]._source.id' | paste -sd, -)
[ "$s" = "$o" ] || echo "$q | solr=$s | os=$o"
done < queries.txt > diff-report.txt出力はdiff-report.txtに溜まります。公式の使い方集にはコマンドの出力をclaude -pへパイプで渡す例があるので、差分の分類も同じ形で頼めます。
head -50 diff-report.txt | claude -p "上位10件が食い違うクエリの共通点を分類して"claude -pの権限とCIでの扱いは、非対話実行の解説に整理してあります。スコアの絶対値は両エンジンで比べても意味がないので、見るのはIDの集合と順位だけに絞ります。
件数の照合は別に行います。Migration Assistantのコンソールからは、console clusters curl target /<collection>/_countで移行先の文書数を取れます。Solr側のnumFoundと合わせてください。型の変換結果を件数と最大値で突き合わせる考え方は、MySQLからPostgreSQLへの移行や、データ移行の項目マッピング表の記事と同じです。
移行作業でつまずく箇所
落とし穴
バックアップは取れるはずなのにBACKUPが失敗する
Solr 8.11のDockerイメージでは、S3バックアッププラグインのファイルが2つのディレクトリに分かれています。sharedLibが片方だけを指すと、Solrはエラーなしで起動し、BACKUPのたびにClassNotFoundExceptionが出ます。solr-s3-repositoryのjarを、AWS SDKのjarと同じディレクトリへコピーしてください。
Migration Assistantの手順書が挙げる、症状の出にくい設定ミスは次のとおりです。
sharedLibは、Solr 8ではカンマ区切りのリストではなく単一のディレクトリを指定するsolr.xmlの${VAR:default}が読むのはJavaのシステムプロパティで、Dockerの環境変数(-e)ではない。値はSOLR_OPTS=-Dkey=valueで渡す- スタンドアロンSolrの
solr.xmlは、標準のDockerイメージでは/var/solr/data/solr.xmlが正しい配置先で、/opt/solr/server/solr/solr.xmlは-Dsolr.solr.home=/var/solr/dataで起動したときは無視される - 稼働中のSolrのデータファイルは直接コピーしない。セグメントのマージや未コミットの変更でバックアップが壊れることがある
- シャードのサイズ上限(
maxShardSizeBytes)の既定は80 GiB。それを超えるシャードは、バックフィルの前にforce-mergeか分割で小さくする
最初は1つのコレクションだけで流し、型の翻訳結果と件数を確かめてから全体へ広げるのが、手順書の勧める順序です。
まとめ
Solr→OpenSearch移行の難所は、データを運ぶ工程より、copyField・dynamicField・analyzerの意味を写す作業と、クエリの書き換えにあります。Migration Assistantは型の翻訳と文書の転送を担い、クエリ層は担いません。
Claude Codeには、schema.xmlの振り分けと比較スクリプト、差分の分類を任せます。「同じ結果とみなす」基準は、上位10件のIDの一致率のような数字で人が先に決めておくと、判定がぶれません。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。
- docs.opensearch.org/latest/migration-assistant/solr-migration
- docs.opensearch.org/latest/migration-assistant/solr-migration/solr-backfill-gu…
- docs.opensearch.org/latest/migration-assistant/playbook-solr-8.11-to-opensearc…
- solr.apache.org/guide/solr/latest/indexing-guide/copy-fields.html
- docs.opensearch.org/latest/mappings/mapping-parameters/copy-to
- code.claude.com/docs/en/common-workflows