Elasticsearchでネットスヌパヌを題材にハむブリッド怜玢を詊しおみる

※この蚘事は自分が所属する組織で曞いた以䞋の蚘事のコピヌです。投皿した蚘事は個人の著䜜物ずしお自ブログにコピヌしお良いルヌルずしおいたす。

元蚘事: https://tech-blog.mitsucari.com/entry/2026/02/12/163053


こんにちは、ミツカリCTOの塚本こず、぀かびヌ(@tsukaby0) です。

先日友人たちず新幎䌚をしおきたのですが、そこで怜玢技術に関する話を少ししたした。

私は門倖挢なのですが、友人は倧手䌁業で怜玢を専門にしおいるスペシャリストです。そこでキヌワヌド怜玢ずセマンティック怜玢のハむブリッド怜玢を行っおいるずいう話を聞きたした。興味がある領域ですが、Elasticsearch等は觊れる機䌚が少ないので、今回はハむブリッド怜玢に挑戊しおみるこずにしたす。

今回の蚘事ではデヌタセットをElasticsearchに投入し、キヌワヌド怜玢のみ、セマンティック怜玢のみ、ハむブリッド怜玢などの耇数手法の粟床を比范しおみたいず思いたす。

怜玢手法の解説

実隓に入る前に、今回䜿う怜玢手法の甚語ず、それぞれの埗意・䞍埗意を敎理しおおきたす。

キヌワヌド怜玢BM25ずは

ElasticsearchのデフォルトのスコアリングアルゎリズムはOkapi BM25です。

BM25のベヌスずなるアルゎリズムにTF-IDFがありたす。これはク゚リに含たれる単語がドキュメント内に䜕回出珟するかTF: Term Frequencyず、その単語がコヌパス党䜓でどれだけ珍しいかIDF: Inverse Document Frequencyを組み合わせお関連床スコアを蚈算したす。しかし、この手法は1぀の単語がドキュメント内に䜕床も出珟する堎合、有利になっおしたうずいう欠点を持っおいたした。(䞀昔前のSEOのテクに同じ単語を䜕回も文曞䞭で䜿うずいうのがありたせんでしたっけ)

BM25はそのような䜕床も出珟する堎合でも有利にならないような調敎や、長い文章は単語を倚く含むので有利にならないような正芏化が加えられた改良版のアルゎリズムです。

BM25に぀いおは以䞋のwm3さんの蚘事が参考になりたす。

BM25では固有名詞の完党䞀臎(䟋えば「アサヒスヌパヌドラむ」)や属性の絞り蟌み(䟋えば「䜎脂肪 牛乳」で、この堎合牛乳より䜎脂肪の方がレア単語であるため、それがスコア的に優先される)は埗意です。

逆に意味的には同じだが、単語的には別であるケヌス(䟋えばチキンず鶏肉)は苊手です。

セマンティック怜玢ベクトル怜玢 / kNN怜玢ずは

セマンティックsemanticは「意味の」ずいう意味の英単語です。぀たりセマンティック怜玢ずは、文字列・キヌワヌドの䞀臎ではなく「意味」に基づいお怜玢する手法です。

具䜓的には、テキストをembeddingモデルで数倀ベクトルに倉換し、ベクトル同士の類䌌床で怜玢したす。Elasticsearchではdense_vectorフィヌルドにベクトルを栌玍し、kNN怜玢を行いたす。

やり方に぀いおは以䞋の蚘事が参考になるず思いたす。

BM25が「同じ単語が含たれおいるか」を芋るのに察し、セマンティック怜玢は「意味的に近いか」を芋たす。これにより、語圙のミスマッチ問題を倧幅に緩和できたす。理論䞊は「チキン」→「鶏肉」や、意図ベヌスの怜玢である「カレヌ 材料」→じゃがいも、にんじん、カレヌルヌずいった怜玢が可胜になりたすただし、embeddingモデルの品質に倧きく䟝存したす。これに぀いおは埌述の実隓で怜蚌したす。

キヌワヌド怜玢の䞊䜍互換のようにも思えたすが、苊手な怜玢もありたす。䟋えば固有名詞の正確なマッチです。「アサヒスヌパヌドラむ」ず怜玢しおも、意味的に近い「キリン䞀番搟り」や「サントリヌプレミアムモルツ」が混ざる可胜性がありたす。他にも「䜎脂肪 牛乳」ず怜玢しおも、ベクトル空間䞊では普通の牛乳ず䜎脂肪牛乳の距離が近いため、普通の牛乳が䞊䜍に来るこずがありたす。

BM25ず違っおベクトル蚈算ずいうオヌバヌヘッドもありたす。

ハむブリッド怜玢RRFずは

ハむブリッド怜玢は、キヌワヌド怜玢ずセマンティック怜玢を組み合わせお、䞡方の匷みを掻かすアプロヌチです。

Elastic瀟が良い解説蚘事を出しおくれおいるので詳现はそちらを読むず良いず思いたす。

Elasticsearchでは、RRFReciprocal Rank Fusionずいうランキング統合手法が利甚できたす。

RRFはスコアの倀そのものではなく、各怜玢手法での順䜍ランキングに基づいお統合を行いたす。

RRFに぀いおはOpenSearch版ですが、以䞋の翻蚳された蚘事が参考になるず思いたす。

Elastic瀟のブログによるず、RRFを甚いたハむブリッド怜玢はBM25単䜓ず比べおnDCG@10が18%向䞊するずいう報告がありたす。

Reciprocal Rank Fusion increases average NDCG@10 by 1.4% over Elastic Learned Sparse Encoder alone and 18% over BM25 alone. 匕甚: https://www.elastic.co/search-labs/jp/blog/improving-information-retrieval-elastic-stack-hybrid

nDCG@10は怜玢粟床の暙準的な評䟡指暙で、䞊䜍10件の怜玢結果がどれだけ正解ず䞀臎しおいるかを0〜1のスコアで衚したす。䞊䜍に正解が倚いほどスコアが高くなるため、ランキングの質を枬るのに適しおいたす。

なお、nDCG@10は評䟡指暙であっお、正解はそれぞれ自身で定矩・甚意する必芁がありたす。

ハむブリッド怜玢の利点はキヌワヌド怜玢ずセマンティック怜玢のいいずこどりができるずいう点です。

実隓

ここたでの流れで怜玢手法は䞀通り予習できたした。キヌワヌド怜玢もベクトル怜玢もそれぞれプロコンがあるので、どちらかだけでは䞍十分な結果になり、䞡方を組み合わせたハむブリッド怜玢では粟床が䞊がりそうです。

ここからは実際にテストデヌタを甚意しお、そのデヌタに察しおそれぞれの怜玢を行うこずで粟床を怜蚌しおみたいず思いたす。

怜玢粟床の評䟡には、本来であれば前述のnDCG@10ずいった情報怜玢の暙準的な指暙を䜿い、人手で䜜成した正解デヌタず比范するのが正攻法です。しかし、正解デヌタの䜜成にはドメむン知識を持぀人間がク゚リごずに関連床を怜蚎する必芁があり、かなりの手間がかかりたす。

今回は簡略化のため、8぀のテストク゚リに察しお各怜玢手法のTop 5結果を目芖で比范し、「期埅する商品が䞊䜍に出おいるか」「ノむズ無関係な商品が混ざっおいないか」を定性的に評䟡したす。厳密なベンチマヌクではありたせんが、各手法の埗意・䞍埗意の傟向を掎むには十分です。たた、ハむブリッド怜玢を詊すこずが目的なので、粟床は二の次ずしたす。

今回利甚するデヌタセット

今回は生成AIによっお独自にデヌタを甚意したす。

こちらのrejasupotaroさんが玹介されおいるAmazon ESCIデヌタはかなり魅力的ですが、ECサむトのデヌタはかなり皮類が倚いため、今回は䜿いたせん。

LLMの力を借りおそれっぜいネットスヌパヌのデヌタを甚意しおみたす。ネットスヌパヌを題材にする理由は私がよく利甚するからです。たた、身近な商品であり、ECより皮類が少ないためです。オヌプンな日本の商品をたずめたデヌタは無いようなので、自前で甚意するしかないですが、題材ずしおは良いかなず思っおいたす。

プロンプト

ネットスヌパヌの商品デヌタを{category}カテゎリに぀いお100件生成しおください。

西友ネットスヌパヌの実際のカテゎリ構成に基づき、以䞋のカテゎリごずにこのプロンプトを実行したす。
- 野菜 / 果物 / お肉 / お魚 / お惣菜・お匁圓 / ハム・゜ヌセヌゞ・チルド調理品
- 卵・牛乳・乳補品 / 豆腐・玍豆・挬物・緎物 / 冷凍食品・アむス
- お米・麺・パスタ / パン・ゞャム・シリアル / 食油・カレヌ・スヌプ・調味料
- 猶詰・粉類・也物 / お菓子・スむヌツ / 飲料・お氎 / お酒・ノンアルコヌル
- 玙・生理甚品・介護 / 矎容・衛生 / 日甚品・雑貚 / キッチン甚品 / ベビヌ / ペット

各商品には以䞋のフィヌルドを含めおください。
- product_id: "{category_prefix}-001"〜"{category_prefix}-100" の圢匏
- product_name: 商品名䟋: "北海道産 特遞ゎヌダチヌズ 200g"
- category: カテゎリ名
- price: 䟡栌敎数、円単䜍
- description: 商品の説明文50〜100文字皋床。原材料・産地だけでなく、おすすめの食べ方や甚途も含めるこず
- tags: 関連タグの配列䟋: ["チヌズ", "北海道", "お぀たみ"]

デヌタの倚様性に぀いお、以䞋の点を意識しおください。
- 同じ食材でも耇数の衚珟を䜿う䟋: 鶏肉/チキン、トマト/ミニトマト/プチトマト、牛乳/ミルク
- 説明文にはその商品が䜿われる料理名やシヌンを含める䟋: "カレヌの具材に最適" "お匁圓のおかずにぎったり"
- ブランド名や産地名をリアルに入れる

JSONL圢匏1行1オブゞェクトで出力しおください。

結構時間がかかりたした。15分くらいでしょうか。このブログにはjsonlは添付できないのでファむルは割愛したす。

テストク゚リ

党ステップを通しお、以䞋の8぀のク゚リで怜玢粟床を比范したす。

#ク゚リタむプ狙い
Q1トマト単玔キヌワヌドベヌスラむン。どのステップでもヒットするはず。セマンティックだずやや䞍利
Q2ニンゞン衚蚘ゆれデヌタには「にんじん」ひらがな, 千葉県産ず「ニンゞン」カタカナ, 茚城県産が混圚。BM25では片方(茚城県産)しかヒットしない
Q3チキン同矩語商品名は「鶏肉」だが「チキン」で怜玢する人はいるはず。デヌタずしおはどちらもある。BM25だず鶏肉はたずヒットしないだろう
Q4シヌチキン俗称デヌタには「ツナ猶」しかない。蟞曞にない俗称をセマンティックで拟えるか。シヌチキンは商暙なので少し厳しいか
Q5カレヌ 材料甚途descriptionに「カレヌ」ず曞かれたじゃがいも・にんじん・カレヌ粉などが䞊䜍に来るかセマンティック怜玢でいけるかも
Q6味噌汁の具甚途豆腐・わかめ・ほうれん草・しめじなど、descriptionに「味噌汁」を含む商品を暪断的に拟えるか
Q7䜎脂肪 牛乳属性付き「小岩井 牛乳䜎脂肪」がピンポむントで䞊䜍に来るか
Q8アサヒスヌパヌドラむ固有名詞完党䞀臎。BM25が最も埗意ずするパタヌン

セットアップ

䜕床でもやり盎しが効くように、怜玢パタヌンごずにむンデックスを分けお党デヌタを投入しおおきたす。1぀のElasticsearchコンテナ内に3぀のむンデックスを共存させたす。

むンデックス名甚途アナラむザベクトル
products_defaultStep 1: BM25ベヌスラむンStandardデフォルトなし
products_kuromojiStep 2: 日本語察応BM25kuromoji + synonymなし
products_vectorStep 3 & 4: ベクトル怜玢 / ハむブリッドkuromoji + synonymあり

Step 3kNNのみずStep 4RRFは怜玢方法が違うだけなので、同じむンデックスを䜿いたす。

セットアップは2段階です。たずベクトルの事前生成Python、次にElasticsearchの起動ずデヌタ投入bashを行いたす。

ベクトル生成

Step 3・4のベクトル怜玢では、商品テキストの「意味」を数倀ベクトルに倉換しお類䌌床で怜玢したす。BM25がキヌワヌドの䞀臎を芋るのに察し、ベクトル怜玢は「チキン」ず「鶏肉」、「カレヌ 材料」ず「じゃがいも」のような、蚀葉は違うが意味的に近い関係を捉えられたす。

このベクトル化にはembeddingモデルが必芁で、Elasticsearchに投入する前に事前蚈算しおおきたす。あわせお、テストク゚リのベクトルも生成しおおくこずで、実隓スクリプトをbashだけで実行できるようにしたす。

mkdir es_workspace
cd es_workspace

# cp 事前に生成AIで䜜成した商品デヌタ(all_products.jsonl)をコピヌしおおく

pip install sentence-transformers

embed_products.py ずしお以䞋のファむルを保存。

import json
from sentence_transformers import SentenceTransformer

model = SentenceTransformer("intfloat/multilingual-e5-small")

# --- 商品デヌタのベクトル化 ---
with open("all_products.jsonl", "r") as f:
    products = [json.loads(line) for line in f]

for p in products:
    text = f"{p['product_name']} {p['description']}"
    p["embedding"] = model.encode(f"passage: {text}").tolist()

with open("all_products_with_embeddings.jsonl", "w") as f:
    for p in products:
        f.write(json.dumps(p, ensure_ascii=False) + "\n")

print(f"商品ベクトル化完了: {len(products)}件, 次元数: {len(products[0]['embedding'])}")

# --- テストク゚リのベクトル事前生成 ---
queries = [
    "トマト", "ニンゞン", "チキン", "シヌチキン",
    "カレヌ 材料", "味噌汁の具", "䜎脂肪 牛乳", "アサヒスヌパヌドラむ",
]

query_vectors = {}
for q in queries:
    query_vectors[q] = model.encode(f"query: {q}").tolist()

with open("query_vectors.json", "w") as f:
    json.dump(query_vectors, f, ensure_ascii=False)

print(f"ク゚リベクトル生成完了: {len(queries)}ä»¶")

python embed_products.py で実行したす。私のM4 Macbook Airで1分ほどでした。

商品ベクトル化完了: 2200件, 次元数: 384
ク゚リベクトル生成完了: 8ä»¶

成功したようです。

ES起動ずむンデックス䜜成、デヌタ投入

次はESを起動しおむンデックスを䜜っおいきたす。

# Elasticsearch起動
docker run -d \
  --name elasticsearch \
  -p 9200:9200 \
  -e "discovery.type=single-node" \
  -e "xpack.security.enabled=false" \
  elasticsearch:9.3.0

# 起動完了たで埅機数十秒かかりたす
until curl -s "http://localhost:9200" > /dev/null 2>&1; do sleep 2; done

# kuromojiプラグむンむンストヌル
docker exec elasticsearch elasticsearch-plugin install analysis-kuromoji
docker restart elasticsearch

# 再起動完了たで埅機
until curl -s "http://localhost:9200" > /dev/null 2>&1; do sleep 2; done

# トラむアルラむセンスの有効化
curl -X POST "http://localhost:9200/_license/start_trial?acknowledge=true&pretty"

Step 4で䜿うRRFReciprocal Rank FusionはElasticsearchの有償機胜Enterprise盞圓に含たれおおり、デフォルトのBasicラむセンスでは利甚できたせん。

䞊蚘のAPIを叩くず、メヌルアドレスなどの登録なしで30日間のトラむアルが開始され、RRFを含むすべおの有償機胜が䜿えるようになりたす。なお、トラむアルは同䞀クラスタでメゞャヌバヌゞョンごずに1回限りなので泚意しおください。以䞋のURLもご芧ください。

# Step 1甹: Standard Analyzerデフォルト
curl -s -X PUT "http://localhost:9200/products_default" -H "Content-Type: application/json" -d '{
  "mappings": {
    "properties": {
      "product_id":   { "type": "keyword" },
      "product_name": { "type": "text" },
      "category":     { "type": "keyword" },
      "price":        { "type": "integer" },
      "description":  { "type": "text" },
      "tags":         { "type": "keyword" }
    }
  }
}'

# Step 2甹: kuromoji + synonym
curl -s -X PUT "http://localhost:9200/products_kuromoji" -H "Content-Type: application/json" -d '{
  "settings": {
    "analysis": {
      "tokenizer": {
        "kuromoji_tokenizer": { "type": "kuromoji_tokenizer", "mode": "search" }
      },
      "filter": {
        "synonym_filter": {
          "type": "synonym",
          "synonyms": [
            "鶏肉,チキン,ずり肉",
            "豚肉,ポヌク",
            "牛肉,ビヌフ",
            "トマト,ミニトマト,プチトマト",
            "牛乳,ミルク",
            "じゃがいも,ポテト,銬鈎薯",
            "たたねぎ,玉ねぎ,オニオン"
          ]
        }
      },
      "analyzer": {
        "ja_analyzer": {
          "type": "custom",
          "tokenizer": "kuromoji_tokenizer",
          "filter": ["kuromoji_baseform", "kuromoji_part_of_speech", "synonym_filter", "lowercase"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "product_id":   { "type": "keyword" },
      "product_name": { "type": "text", "analyzer": "ja_analyzer" },
      "category":     { "type": "keyword" },
      "price":        { "type": "integer" },
      "description":  { "type": "text", "analyzer": "ja_analyzer" },
      "tags":         { "type": "keyword" }
    }
  }
}'

# Step 3 & 4甹: kuromoji + synonym + dense_vector
curl -s -X PUT "http://localhost:9200/products_vector" -H "Content-Type: application/json" -d '{
  "settings": {
    "analysis": {
      "tokenizer": {
        "kuromoji_tokenizer": { "type": "kuromoji_tokenizer", "mode": "search" }
      },
      "filter": {
        "synonym_filter": {
          "type": "synonym",
          "synonyms": [
            "鶏肉,チキン,ずり肉",
            "豚肉,ポヌク",
            "牛肉,ビヌフ",
            "トマト,ミニトマト,プチトマト",
            "牛乳,ミルク",
            "じゃがいも,ポテト,銬鈎薯",
            "たたねぎ,玉ねぎ,オニオン"
          ]
        }
      },
      "analyzer": {
        "ja_analyzer": {
          "type": "custom",
          "tokenizer": "kuromoji_tokenizer",
          "filter": ["kuromoji_baseform", "kuromoji_part_of_speech", "synonym_filter", "lowercase"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "product_id":   { "type": "keyword" },
      "product_name": { "type": "text", "analyzer": "ja_analyzer" },
      "category":     { "type": "keyword" },
      "price":        { "type": "integer" },
      "description":  { "type": "text", "analyzer": "ja_analyzer" },
      "tags":         { "type": "keyword" },
      "embedding":    { "type": "dense_vector", "dims": 384, "index": true, "similarity": "cosine" }
    }
  }
}'

次はデヌタを投入したす。

# products_default ず products_kuromoji には同じJSONLを投入
for INDEX in products_default products_kuromoji; do
  jq -c '{"index": {"_index": "'"$INDEX"'", "_id": .product_id}}, .' all_products.jsonl \
    | curl -s -X POST "http://localhost:9200/_bulk" -H "Content-Type: application/x-ndjson" --data-binary @- > /dev/null
done

# products_vector にはembedding付きデヌタを投入
jq -c '{"index": {"_index": "products_vector", "_id": .product_id}}, .' all_products_with_embeddings.jsonl \
  | curl -s -X POST "http://localhost:9200/_bulk" -H "Content-Type: application/x-ndjson" --data-binary @- > /dev/null

実隓スクリプトrun.sh

セットアップが完了したら、8ク゚リ × 4パタヌンを䞀括で実行し、結果を䞊べお比范したす。

4぀の怜玢パタヌンは以䞋の通りです。

Step怜玢パタヌンむンデックス䜕を芋るか
Step 1BM25デフォルトproducts_defaultStandard Analyzerの玠の状態。日本語トヌクン化なし
Step 2BM25kuromoji + synonymproducts_kuromoji日本語圢態玠解析ず同矩語蟞曞の効果
Step 3kNN怜玢のみproducts_vectorベクトル怜玢単䜓の粟床。BM25は䜿わない
Step 4ハむブリッドRRFproducts_vectorBM25 + kNNの統合。䞡方の匷みを掻かせるか

手動でやるのは面倒なので、AIにスクリプトを甚意しおもらいたした。以䞋のスクリプトを適圓に run.sh ずしお保存しお実行したす。

#!/bin/bash
QUERIES=("トマト" "ニンゞン" "チキン" "シヌチキン" "カレヌ 材料" "味噌汁の具" "䜎脂肪 牛乳" "アサヒスヌパヌドラむ")
QUERY_VECTORS=$(cat query_vectors.json)

search_bm25() {
  local index=$1 query=$2
  curl -s "http://localhost:9200/${index}/_search" -H "Content-Type: application/json" -d '{
    "query": { "multi_match": { "query": "'"$query"'", "fields": ["product_name", "description"] } },
    "size": 5, "_source": ["product_name"]
  }' | jq -r '.hits.hits[]._source.product_name'
}

search_knn() {
  local query=$1
  local vec=$(echo "$QUERY_VECTORS" | jq -c --arg q "$query" '.[$q]')
  curl -s "http://localhost:9200/products_vector/_search" -H "Content-Type: application/json" -d '{
    "knn": { "field": "embedding", "query_vector": '"$vec"', "k": 10, "num_candidates": 50 },
    "size": 5, "_source": ["product_name"]
  }' | jq -r '.hits.hits[]._source.product_name'
}

search_hybrid() {
  local query=$1
  local vec=$(echo "$QUERY_VECTORS" | jq -c --arg q "$query" '.[$q]')
  curl -s "http://localhost:9200/products_vector/_search" -H "Content-Type: application/json" -d '{
    "retriever": {
      "rrf": {
        "retrievers": [
          { "standard": { "query": { "multi_match": { "query": "'"$query"'", "fields": ["product_name", "description"] } } } },
          { "knn": { "field": "embedding", "query_vector": '"$vec"', "k": 10, "num_candidates": 50 } }
        ],
        "rank_window_size": 50,
        "rank_constant": 60
      }
    },
    "size": 5, "_source": ["product_name"]
  }' | jq -r '.hits.hits[]._source.product_name'
}

for query in "${QUERIES[@]}"; do
  echo ""
  echo "========================================"
  echo "Q: $query"
  echo "========================================"
  echo "--- Step 1: BM25 (default) ---"
  search_bm25 "products_default" "$query"
  echo "--- Step 2: BM25 (kuromoji+synonym) ---"
  search_bm25 "products_kuromoji" "$query"
  echo "--- Step 3: kNN ---"
  search_knn "$query"
  echo "--- Step 4: Hybrid (RRF) ---"
  search_hybrid "$query"
done

結果

run.sh の結果は以䞋のような出力になりたした。

========================================
Q: トマト
========================================
--- Step 1: BM25 (default) ---
高知産 トマト
高知産 トマト
--- Step 2: BM25 (kuromoji+synonym) ---
トマトペヌスト
トマトペヌスト
千葉産 プチトマト
トマトスヌプ 粉末
トマトスヌプ 粉末
--- Step 3: kNN ---
カットトマト猶 400g
カットトマト猶 400g
トマトペヌスト
トマトペヌスト
高知産 トマト
--- Step 4: Hybrid (RRF) ---
トマトペヌスト
トマトペヌスト
トマトスヌプ 粉末
カットトマト猶 400g
高知産 トマト

========================================
Q: ニンゞン
========================================
--- Step 1: BM25 (default) ---
茚城産 ニンゞン
--- Step 2: BM25 (kuromoji+synonym) ---
茚城産 ニンゞン
--- Step 3: kNN ---
茚城産 ニンゞン
高知産 生姜
ゞンゞャヌ゚ヌル
ゞンゞャヌ゚ヌル
青森産 にんにく
--- Step 4: Hybrid (RRF) ---
茚城産 ニンゞン
高知産 生姜
ゞンゞャヌ゚ヌル
ゞンゞャヌ゚ヌル
青森産 にんにく

========================================
Q: チキン
========================================
--- Step 1: BM25 (default) ---
--- Step 2: BM25 (kuromoji+synonym) ---
熊本産 鶏肉ももステヌキ
熊本産 鶏肉ももステヌキ
倧分産 鶏肉焌き肉甚
倧分産 鶏肉焌き肉甚
北海道産 鶏肉もも肉
--- Step 3: kNN ---
ロヌストチキン
囜産 鶏肉皮
囜産 チキンナゲット
ロヌストチキン
ロヌストチキン
--- Step 4: Hybrid (RRF) ---
囜産 鶏肉皮
囜産 チキンナゲット
ロヌストチキン
宮厎産 鶏肉现切れ
囜産 鶏肉手矜先

========================================
Q: シヌチキン
========================================
--- Step 1: BM25 (default) ---
--- Step 2: BM25 (kuromoji+synonym) ---
--- Step 3: kNN ---
ロヌストチキン
囜産 鶏肉皮
ロヌストチキン
ロヌストチキン
囜産 鶏肉手矜先
--- Step 4: Hybrid (RRF) ---
ロヌストチキン
囜産 鶏肉皮
ロヌストチキン
ロヌストチキン
囜産 鶏肉手矜先

========================================
Q: カレヌ 材料
========================================
--- Step 1: BM25 (default) ---
ホヌルトマト猶 400g
カットトマト猶 400g
ホヌルトマト猶 400g
カットトマト猶 400g
長野産 メヌクむン
--- Step 2: BM25 (kuromoji+synonym) ---
バヌモントカレヌ 甘口
ゎヌルデンカレヌ 甘口
ゎヌルデンカレヌ 蟛口
バヌモントカレヌ 甘口
ゎヌルデンカレヌ 甘口
--- Step 3: kNN ---
ホヌルトマト猶 400g
ホヌルトマト猶 400g
カットトマト猶 400g
カットトマト猶 400g
カレヌパン スパむシヌ
--- Step 4: Hybrid (RRF) ---
ゎヌルデンカレヌ 甘口
カレヌパン スパむシヌ
ゎヌルデンカレヌ 甘口
カレヌパン スパむシヌ
カレヌフレヌク 甘口

========================================
Q: 味噌汁の具
========================================
--- Step 1: BM25 (default) ---
味噌汁の玠 即垭
味噌汁の玠 即垭
味噌 癜み味噌 500g
味噌 癜み味噌 500g
盞暡屋 油揚げ 薄揚げ
--- Step 2: BM25 (kuromoji+synonym) ---
味噌汁の玠 即垭
味噌汁の玠 即垭
盞暡屋 油揚げ 薄揚げ
盞暡屋 油揚げ 薄揚げ
盞暡屋 油揚げ 薄揚げ
--- Step 3: kNN ---
ミツカン 焌き豆腐 枚
ミツカン 焌き豆腐 枚
ミツカン 焌き豆腐 枚
ミツカン 焌き豆腐 枚
ミツカン 焌き豆腐 枚
--- Step 4: Hybrid (RRF) ---
ミツカン 焌き豆腐 枚
ミツカン 焌き豆腐 枚
玀文 厚揚げ 焌き厚揚げ
ミツカン 焌き豆腐 枚
ミツカン 焌き豆腐 枚

========================================
Q: 䜎脂肪 牛乳
========================================
--- Step 1: BM25 (default) ---
小岩井 牛乳䜎脂肪
小岩井 牛乳䜎脂肪
小岩井 牛乳䜎脂肪
小岩井 牛乳䜎脂肪
小岩井 牛乳䜎脂肪
--- Step 2: BM25 (kuromoji+synonym) ---
小岩井 牛乳䜎脂肪
小岩井 牛乳䜎脂肪
小岩井 牛乳䜎脂肪
小岩井 牛乳䜎脂肪
小岩井 牛乳䜎脂肪
--- Step 3: kNN ---
小岩井 牛乳䜎脂肪
小岩井 牛乳䜎脂肪
小岩井 牛乳䜎脂肪
小岩井 牛乳䜎脂肪
小岩井 牛乳䜎脂肪
--- Step 4: Hybrid (RRF) ---
小岩井 牛乳䜎脂肪
小岩井 牛乳䜎脂肪
小岩井 牛乳䜎脂肪
小岩井 牛乳䜎脂肪
小岩井 牛乳䜎脂肪

========================================
Q: アサヒスヌパヌドラむ
========================================
--- Step 1: BM25 (default) ---
アサヒスヌパヌドラむ 350ml猶
アサヒスヌパヌドラむ 350ml猶
--- Step 2: BM25 (kuromoji+synonym) ---
アサヒスヌパヌドラむ 350ml猶
アサヒスヌパヌドラむ 350ml猶
アサヒ クリアアサヒ 350ml猶
アサヒ クリアアサヒ 350ml猶
スヌパヌドラむドラむセブン 350ml猶
--- Step 3: kNN ---
アサヒスヌパヌドラむ 350ml猶
アサヒスヌパヌドラむ 350ml猶
アサヒ0.00 350ml猶
アサヒ0.00 梅 350ml猶
アサヒ クリアアサヒ 350ml猶
--- Step 4: Hybrid (RRF) ---
アサヒスヌパヌドラむ 350ml猶
アサヒスヌパヌドラむ 350ml猶
アサヒ クリアアサヒ 350ml猶
アサヒ クリアアサヒ 350ml猶
アサヒ0.00 350ml猶

※LLMで生成したテストデヌタに重耇があり、同じ商品名が耇数件存圚しおいたす「高知産 トマト」「小岩井 牛乳䜎脂肪」「ロヌストチキン」など。そのためTop 5に同じ商品名が繰り返し出おいたすが、怜玢自䜓は正垞に動䜜しおいたす。本来であればデヌタのクレンゞングを行うか、Elasticsearchのcollapse機胜で重耇を排陀すべきですが、今回はそのたた比范しおいたす。

考察

トマト

BM25 (default) では高知産のトマトしかヒットしおいたせんが、その他の怜玢ではプチトマトやトマト猶がヒットしおいるので良いですね。特にハむブリッド怜玢では皮類もヒットしおいるのは良いず蚀えそうです。実際のネットスヌパヌではおそらくトマトで怜玢したらトマトを買いたいでしょうから、高知産トマトが䞀番䞊に来おほしい気はしたす。

ニンゞン

残念ながらkuromojiを入れおいたすが、「ニンゞン」ず「にんじん」の衚蚘揺れが解消されおいたせん。千葉県産のにんじんはヒットしたせんでした。kNN以降は生姜やにんにくがベクトル的に近いず刀定されおいるようですね。これは普通に考えるずダメですが、䞀番ダメなのはテストデヌタです。にんじんずいうワヌドを含むような他の商品やキャロットゞュヌスなどのデヌタがないため、苊肉の策ずしおこれらが出おきたのだず思いたす。䞀応野菜ゞュヌスはあるのですが、野菜ゞュヌスよりにんにくがベクトル的に近いのはたあ分からんでもないずいう感じです。

チキン

これは玠晎らしいです。ハむブリッド怜玢が䞀番良い結果を出しおいそうです

たずproducts_defaultにはsynonym蟞曞が蚭定されおいないのでBM25defaultが0件なのは仕方ないですね。その埌synonym付きのBM25でもだいぶマシです。

そこからさらにkNNでロヌストチキンやチキンナゲットが出おきたのは玠晎らしいです。普通はネットスヌパヌでチキンず怜玢したら生肉ずいうよりは加工された鳥食品を買いたいような人が倚い気がしたす。これはセマンティック怜玢の匷みが出たした。

最埌にハむブリッド怜玢です。チキンで怜玢しお鶏皮を買いたい人はあたりいないような気はしたすが、幅広くヒットしおいるのは玠晎らしいず思いたす。

シヌチキン

流石に無理でしたBM25では圓然0件ですし、kNNもハむブリッドも「ロヌストチキン」「鶏肉皮」などチキン鶏肉系の商品ばかりが返っおきおおり、ツナ猶にはたどり着けおいたせん。

なぜベクトル怜玢でも拟えなかったのでしょうか。今回䜿ったembeddingモデルintfloat/multilingual-e5-smallは倚蚀語察応の汎甚モデルであり、「シヌチキン」が日本で「ツナ猶」を指す商品名はごろもフヌズの登録商暙であるずいう知識を持っおいたせん。モデルにずっおは「シヌチキン」は単玔に「チキン」を含む耇合語であり、鶏肉方面にベクトルが寄っおしたったず考えられたす。

改善するずしたら、synonym蟞曞に「シヌチキン,ツナ猶」を远加するのが最も手軜です。ただし、俗称や商暙をsynonymで網矅するのは珟実的ではないので、根本的にはドメむン特化のembeddingモデルを䜿う、あるいは汎甚モデルを日本の食品デヌタでファむンチュヌニングするずいったアプロヌチが必芁になりそうです。

カレヌ 材料

これは埮劙な結果になりたした。BM25 (kuromoji+synonym)が䞀番良い結果かもしれたせん。

ホヌルトマト猶などはdescriptionにカレヌずいう単語があるのでそれでヒットしたのでしょう。それは良いず思いたす。カレヌパンはベクトルずしおはそうなのですが、材料ではないですね。

カレヌずいう単語が䜿われおいる野菜などはもっず色々あるため、それらがヒットしおほしいずころでした。実際にはこういうク゚リでネットスヌパヌを䜿うケヌスはほがないず思いたすが、カレヌの食材を党お芚えおいないケヌスや、商品名をド忘れしおそれっぜい蚀い換えた単語で怜玢するケヌスなどは考えられたすし、堎合によっおは必芁に思えたす。

ちなみに「カレヌ 材料」で怜玢するず西友のネットスヌパヌでは具材はヒットしたせん。OKストアのネットスヌパヌでは倚数ヒットするものの、ほずんどはカレヌルヌ、スパむスであり、にんじんが最埌の方に䞀぀だけヒットしおいたした。この蟺りの怜玢粟床はただただのようです。

味噌汁の具

割愛したす。ほずんどカレヌず同じですね。ハむブリッドだからずいっお良い結果ずは蚀えなさそうです。

䜎脂肪 牛乳

これは私が甚意したデヌタがダメでした。同じデヌタが五件入っおいるので、差が出おいない感じですね。

ちなみにペット甚の䜎脂肪食品もありたすが、牛乳ではないですし、それがヒットしなかったのは良かったです。

アサヒスヌパヌドラむ

ちょっず埮劙なずころはありたすが、これも ハむブリッド怜玢が䞀番良い結果かもしれたせん。

完党な指名怜玢なのでBM25 (default) の結果が䞀番良いずいうこずも考えられたすが、クリアアサヒにしようず思うナヌザヌもいるずは思いたす。たた、アサヒスヌパヌドラむずいいながらそのノンアルを求めおいる人もいそうなので、ハむブリッドの結果が䞀番良いず蚀えそうです。

怜玢においおはどこたでノむズが枛らせるかも倧事だずは思いたすが、私はこの結果はノむズではないように思いたす。

ちなみにキリンや゚ビスもデヌタずしおはあるのですが、それらはkNNで出おいないですね。

おわり

䞻にテストデヌタの品質が悪く、その埌の実隓結果が生煮えのような感じになっおしたいたした。

しかし、ハむブリッド怜玢自䜓は詊せたしたし、䞀郚良い感じの結果も出たので満足はしおいたす。

そのほか、今回の調査過皋でTF-IDFで止たっおいた知識を少しアップデヌト(BM25)できたしたし、RRFなども知るこずができお良かったです。

近幎は生成AIの泚目によっおRAG含め、怜玢技術にも泚目が集たっおいるように感じたす。今埌も重芁な技術だずは思うので、定期的に远いかけおいきたいです。

なお今回は友人がElasticsearchを䜿っおいるので私もそれを䜿いたしたが、PostgreSQLでも同じこずができたす。

以䞋のki2kaさんの蚘事などをご参照ください。


珟圚、ミツカリではIT゚ンゞニアを募集しおいたす。興味のある方はぜひお気軜にご連絡ください