MMR検索を使うことで映画レコメンド結果の重複を減らし、多様性を高める具体的なコツがわかります
- まず、MMR検索で上位10件の映画候補から似たもの3件以上は除外してみて。
これだけで同じジャンルばかり並ぶ事態を防げる。1週間後におすすめリスト内のタイトル被りがゼロなら成功。(自分で並べて確認)
- Qdrant×MMR設定時は関連度スコア上位5件+多様性スコア3点以上の作品も必ず混ぜよう。
2025年現在、多様性指標を足すとジャンルや国籍がバラけやすい。翌日、リストに3種類以上ジャンルあるか目視チェックすると良い。
- IMDBデータ処理では、サマリー生成時に異なるキーワード(例:恋愛・冒険・SF)を最低2つ組み合わせて試そう。
...こうすることで似た内容ばかりにならず幅広く拾えるよ。週末までにサマリー欄が単一ワード埋めじゃなければOK。(Excelなどで集計確認)
- `FastEmbed`使うなら最初の10分間だけ主要カテゴリごとにベクトル化して保存スタートしてみて。
...短時間でもカテゴリ別ベクトル増やせば後から絞り込み簡単になる。翌日再実行時、読み込み速度30%アップしたら効果あり。(ログ解析推奨)
MMR検索で重複するベクトル検索結果を防ぐ方法を知る
「Evaluate and Monitor LLM」というワードで検索をかけると、ベクトル検索によって文言が異なるだけで中身はほぼ同じブログ記事ばかりが目立って並ぶ場面に出くわすことがある。そんな状況に対し効果的なのがMaximal Marginal Relevance(MMR)だ。これは各ヒット結果ごとに新規性ある情報が含まれるよう選別する手法であり、同じ内容を幾度も読む無駄な時間を減らす役割を果たしてくれる。このテキストでは、そのMMRの概要やQdrantによる実装方法についてひと通り説明するつもりだ。
ポイントとなる主な流れは次のとおり。
1. まず**ベクトル検索**から始める - まとまった複数の文書をベクトルとして変換し、クエリとの類似度にもとづいて上位k件のみ抜き出す(シンプルな近傍探索の要領)。
2. 次いで**MMR処理**を施す - 先ほどの候補群へMMRアルゴリズムを用い再ランキングする流れ。その過程で
- クエリとの関連度(類似度)
- 既に選ばれているドキュメントとの相違(多様性)
このふたつがちょうどよく釣り合うよう調整されていく。
要は、一旦ベクトル検索によって高関連度な候補集合を作成し、そのなかから重複のない、多様な内容となるようMMR技法によってさらに最終抽出されるということなんだ。
さてMaximal Marginal Relevance(MMR)とは何か──簡単に触れるなら、「一方的な関連性優先」でも「ひたすら多様性狙い」でもなく、その両者のバランス取りを動的に図りつつ最適化していくアルゴリズムだと言える。ま、いいか。他の方法では見落とされそうな小さな差異にも柔軟に反応でき、冗長排除という点で意外と頼もしい存在になっていると思う。
ポイントとなる主な流れは次のとおり。
1. まず**ベクトル検索**から始める - まとまった複数の文書をベクトルとして変換し、クエリとの類似度にもとづいて上位k件のみ抜き出す(シンプルな近傍探索の要領)。
2. 次いで**MMR処理**を施す - 先ほどの候補群へMMRアルゴリズムを用い再ランキングする流れ。その過程で
- クエリとの関連度(類似度)
- 既に選ばれているドキュメントとの相違(多様性)
このふたつがちょうどよく釣り合うよう調整されていく。
要は、一旦ベクトル検索によって高関連度な候補集合を作成し、そのなかから重複のない、多様な内容となるようMMR技法によってさらに最終抽出されるということなんだ。
さてMaximal Marginal Relevance(MMR)とは何か──簡単に触れるなら、「一方的な関連性優先」でも「ひたすら多様性狙い」でもなく、その両者のバランス取りを動的に図りつつ最適化していくアルゴリズムだと言える。ま、いいか。他の方法では見落とされそうな小さな差異にも柔軟に反応でき、冗長排除という点で意外と頼もしい存在になっていると思う。
最大限のマージナルリライバランスがどのように関連性と多様性を保つか理解する
MMR(Maximal Marginal Relevance)は、検索結果における冗長性をなるべく抑えつつも、クエリとの関係もしっかり維持しようという手法だ。似たようなものばかりを列挙するわけではなく、「関連性」と「多様性」の2点のバランスが要になるよ。-「関連性」については、どれくらいクエリにぴたり合致しているかを見る指標で、-「多様性」は既に選んだ情報とどの程度違いがあるかという面ね。
たとえるなら、新しいレストランでメニュー構成を練っている状況かな。一応テーマには沿っているものの、ちょっとずつ具材だけ違うパスタばかり10皿並んでいる状態は避けたいじゃない?ほんの僅かな差では満足できないし。こうした考慮によって、それぞれ加わる新規項目にも意味や価値をちゃんともたせることができる。その過程で重複した情報ばかりになる心配も減らせる。
技術的な側面からみると、MMRは各候補データごとにクエリとの合致度(関連性)だけじゃなくて、すでにセレクト済みのものとの重なり具合(多様性)にも目を配る仕組みなんだ。次に追加すべき項目の決定時には、その2軸がトレードオフ・パラメータλによって一緒くたに計算される。「新しさ」と「近さ」が上手く噛み合ったタイミングで次を選ぶ流れとなっていて、この一連の手順自体は実際には意外とシンプルだったりするんだなあ。ま、いいか。
たとえるなら、新しいレストランでメニュー構成を練っている状況かな。一応テーマには沿っているものの、ちょっとずつ具材だけ違うパスタばかり10皿並んでいる状態は避けたいじゃない?ほんの僅かな差では満足できないし。こうした考慮によって、それぞれ加わる新規項目にも意味や価値をちゃんともたせることができる。その過程で重複した情報ばかりになる心配も減らせる。
技術的な側面からみると、MMRは各候補データごとにクエリとの合致度(関連性)だけじゃなくて、すでにセレクト済みのものとの重なり具合(多様性)にも目を配る仕組みなんだ。次に追加すべき項目の決定時には、その2軸がトレードオフ・パラメータλによって一緒くたに計算される。「新しさ」と「近さ」が上手く噛み合ったタイミングで次を選ぶ流れとなっていて、この一連の手順自体は実際には意外とシンプルだったりするんだなあ。ま、いいか。

数式から分かるMMRアルゴリズムの選定基準を学ぶ
MMR(Maximum Marginal Relevance)は、数式としては以下のように示すことができます。
このとき、**di** は今検討している候補ドキュメント(i番目)、**dj** はすでに選ばれた集合に含まれているもの(j番目)、**q** はクエリベクトル、**R** は既存の選定ドキュメント集合、そして **λ** が多様性を制御するパラメータ(0~1)です。// 0.0が関連重視、1.0なら多様性重視ってことになります。また、**Sim₁**はドキュメントとクエリ間の類似度、もうひとつの **Sim₂** は文書同士の類似度を指しています。
このアルゴリズムは段階的処理で動いていく。最初は一番クエリに関係深い文書をまず拾います。そのあと、それぞれの候補について「どれだけクエリ内容とかみ合うか」と「これまでピックアップした文書とどれくらい被るか」のバランスを見るわけです。ま、その調整にはλ値が用いられていて—
- λ = 0:思い切り異質な文書をなるべく優先
- λ = 1:逆に一途なまでに関連重視となる検索になる
- λ = 0.5:両方ちょうど半分ずつ考慮する感じだね
MMRが必要になった背景ですが、ごく一般的な類似検索だと注意すべき癖があります。一度強く合致する内容を見つけてしまうと、その後もほぼ変化なく極めて近しい文書ばかり次々出てくる傾向があるんですよ。その偏りを回避する目的から生まれました。
このとき、**di** は今検討している候補ドキュメント(i番目)、**dj** はすでに選ばれた集合に含まれているもの(j番目)、**q** はクエリベクトル、**R** は既存の選定ドキュメント集合、そして **λ** が多様性を制御するパラメータ(0~1)です。// 0.0が関連重視、1.0なら多様性重視ってことになります。また、**Sim₁**はドキュメントとクエリ間の類似度、もうひとつの **Sim₂** は文書同士の類似度を指しています。
このアルゴリズムは段階的処理で動いていく。最初は一番クエリに関係深い文書をまず拾います。そのあと、それぞれの候補について「どれだけクエリ内容とかみ合うか」と「これまでピックアップした文書とどれくらい被るか」のバランスを見るわけです。ま、その調整にはλ値が用いられていて—
- λ = 0:思い切り異質な文書をなるべく優先
- λ = 1:逆に一途なまでに関連重視となる検索になる
- λ = 0.5:両方ちょうど半分ずつ考慮する感じだね
MMRが必要になった背景ですが、ごく一般的な類似検索だと注意すべき癖があります。一度強く合致する内容を見つけてしまうと、その後もほぼ変化なく極めて近しい文書ばかり次々出てくる傾向があるんですよ。その偏りを回避する目的から生まれました。
通常の類似度検索と比較してMMRの必要性を見極める
同じ観点が何度となく現れ、そこに新しさや幅が加わることなく、最終的にエコーチェンバーが形成されてしまうのだ。この現象は検索エンジンや文書検索、それからRAGパイプラインといった仕組みで特にはっきり表れることもある。一度提示された視点ばかりが繰り返されることで出力全体が複雑になる。そのため、本来価値が期待される情報量にもかかわらず、中身としては意外と乏しい結果になってしまいがちだ。例えば、「ニューラルネットワークの応用」を広めに調べようとした場合によく起こるのだけど、大抵、画像分類という題材から入り始める複数の記事が続けざまに目につく。ほとんどすべては確かに関連技術の説明ではあるものの、新規性に乏しく、先頭記事以上の発見はほぼ得られない印象を受けやすい。同種の問題はマルチドキュメント要約でもたびたび指摘されているね。違う資料を参照しているにもかかわらず、各所で似通った事実ばかり抜粋され、その分要約も冗漫になり、有用性自体はさほど高まらないこともしばしば。そこで活躍する手法としてMMR(Maximal Marginal Relevance)がある。これは「クエリとの一致」に加えて、「既存内容との差異」も評価基準とする方法なんだよ。こうしたアプローチを使えば、関連性のみならず多様性にも目配せできるわけで、それぞれ付加される文書ごとにちゃんと新しい情報を持ち寄れる。他方で大筋から逸れることなく、本旨への貢献も保つよう工夫しているんじゃないかな…ま、いいか。

Qdrant×MMRで映画レコメンドエンジン構築へ最初に実行すべきこと
Recommendation Search Engine(Qdrantを利用したMMRによる再ランク付けと、LangGraph Chatbotの組み合わせ)は、大きく分けて以下の工程で構成されている。
ステップ1:初期セットアップ
はじめに、MMR機能に必要な各種パッケージをインストールする。ベクトルストレージおよび埋め込み生成にはqdrant-clientとfastembedが鍵となる。さらに大規模言語モデルの協調処理層にはLangGraphを使い、LLM連携にはSambanovaが活躍する。例えば、次のようなインストールコマンドになる。
!pip install qdrant-client fastembed
!pip install langgraph langchain-community
!pip install langchain-sambanova
data = pd.read_csv("movies.csv")
data['summary'] = data[['name', 'genre', 'tagline', 'writers', 'casts']].astype(str).agg(' '.join, axis=1)
df = data[['summary','rank','name','certificate']]
loader = DataFrameLoader(df, page_content_column="summary")
docs = loader.load()
ステップ3:埋め込みモデル初期化
ここではFastEmbedを用い、個々の映画データからベクトル形式への変換処理を担う。FastEmbedはONNX Runtime上で軽快に動作し、大型PyTorch環境が不要でも高速且つ省リソースな計算が可能なのだ。ま、いいか。他にも実装方法はいろいろ存在するが、この組み合わせは設定負荷も低いので使いやすさもある気がするね。
ステップ1:初期セットアップ
はじめに、MMR機能に必要な各種パッケージをインストールする。ベクトルストレージおよび埋め込み生成にはqdrant-clientとfastembedが鍵となる。さらに大規模言語モデルの協調処理層にはLangGraphを使い、LLM連携にはSambanovaが活躍する。例えば、次のようなインストールコマンドになる。
!pip install qdrant-client fastembed
!pip install langgraph langchain-community
!pip install langchain-sambanova
ステップ2:データ読み込み・加工
続いて、推薦システム向けとして映画関連データセットを下準備しておく。この例ではKaggleから入手できるIMDB Top 250 Movies データセットを利用し、映画ごとのname・genre・tagline・writers・castsなど幾つかのカラム情報を合成しsummaryというフィールドへと集約している。実際のコードは下記。kotlin
import pandas as pd
from langchain_community.document_loaders import DataFrameLoader
data = pd.read_csv("movies.csv")
data['summary'] = data[['name', 'genre', 'tagline', 'writers', 'casts']].astype(str).agg(' '.join, axis=1)
df = data[['summary','rank','name','certificate']]
loader = DataFrameLoader(df, page_content_column="summary")
docs = loader.load()
ステップ3:埋め込みモデル初期化
ここではFastEmbedを用い、個々の映画データからベクトル形式への変換処理を担う。FastEmbedはONNX Runtime上で軽快に動作し、大型PyTorch環境が不要でも高速且つ省リソースな計算が可能なのだ。ま、いいか。他にも実装方法はいろいろ存在するが、この組み合わせは設定負荷も低いので使いやすさもある気がするね。
データ処理でIMDB映画情報から多様なサマリー生成手順を試す
ローカル環境では、CPUとGPUのいずれにも対応した並列エンコードを使えます。Dense埋め込みが基本ですが、Hybrid Searchの場合はSparseも組み合わせて利用できますね。ちなみに、このような実装例として「Advanced Retrieval and Evaluation - Hybrid Search with miniCOIL using Qdrant and LangGraph」という記事があります。
### Step-4: Qdrantベクトルデータベースクライアント設定
まずはQdrant Cloudアカウントの作成が必要です。cloud.qdrant.ioで新規登録し、クラスターをひとつ立ち上げてください。その後、「Data Access Control」からAPIキーを発行できますので、クラスタのURL(末尾ポート6333)と一緒に取得しておきましょう。「recommendations」というコレクション名には映画ベクトルを保存する仕組みです。
python
from fastembed import TextEmbedding
embedding_model = TextEmbedding("BAAI/bge-base-en-v1.5")
### Step-4: Qdrantベクトルデータベースクライアント設定
まずはQdrant Cloudアカウントの作成が必要です。cloud.qdrant.ioで新規登録し、クラスターをひとつ立ち上げてください。その後、「Data Access Control」からAPIキーを発行できますので、クラスタのURL(末尾ポート6333)と一緒に取得しておきましょう。「recommendations」というコレクション名には映画ベクトルを保存する仕組みです。

FastEmbed活用でローカル高速ベクトル埋め込み生成を始めるには?
このサンプルコードは、QdrantClientおよびmodelsを活用し、「recommendations」というコレクションの新規作成から始まっている。まず、client.create_collection関数でvectors_configにはsize=768とdistance=models.Distance.COSINEが指定され、on_disk_payloadオプションもTrueに設定済みだ。
続いて、Langchainのローダーによる各ドキュメントのベクトル化、および対応するメタデータ付与の工程に移る。この際、とりわけsummary tableが主要なpage contentとしてVector変換され、一方でrankやname・certificate等の情報はpayload内のメタデータへ格納されることになる。そして、それぞれベクトル化されたこれらデータ群はQdrantへのupsert(追加または更新)処理を通じて送信され、インデックス生成プロセスとして位置づけられている。
具体的にはdocsオブジェクトをforループで一つずつ処理しながら、それぞれembedding_model.embed経由で内容を埋め込みベクトルとして取得する運びだ。それらポイントごとにid・vector・payload属性を持たせたPointStructインスタンスがpointsリストへ蓄積され、その全体が最終的にclient.upsertからrecommendationsコレクションにまとめて投入される流れ。
さらに、第4段階ではMMR(Maximal Marginal Relevance)の仕組みを利用した再ランク検索用ロジックにも触れている。通常型の近傍探索とは一線を画し、この手法では単なる類似性だけでなく結果集合の多様性にも配慮して選択が行われるため、極端に似過ぎたものばかりが集まらない点が特長となっている。また細かな注記として、「MMRの場合は各ポイント単位でランキング評価が進むため、Qdrant側で算出されるMMRスコアは基本的にクエリベクトルとの近接度となる」点についても解説していた。ま、いいか。
続いて、Langchainのローダーによる各ドキュメントのベクトル化、および対応するメタデータ付与の工程に移る。この際、とりわけsummary tableが主要なpage contentとしてVector変換され、一方でrankやname・certificate等の情報はpayload内のメタデータへ格納されることになる。そして、それぞれベクトル化されたこれらデータ群はQdrantへのupsert(追加または更新)処理を通じて送信され、インデックス生成プロセスとして位置づけられている。
具体的にはdocsオブジェクトをforループで一つずつ処理しながら、それぞれembedding_model.embed経由で内容を埋め込みベクトルとして取得する運びだ。それらポイントごとにid・vector・payload属性を持たせたPointStructインスタンスがpointsリストへ蓄積され、その全体が最終的にclient.upsertからrecommendationsコレクションにまとめて投入される流れ。
さらに、第4段階ではMMR(Maximal Marginal Relevance)の仕組みを利用した再ランク検索用ロジックにも触れている。通常型の近傍探索とは一線を画し、この手法では単なる類似性だけでなく結果集合の多様性にも配慮して選択が行われるため、極端に似過ぎたものばかりが集まらない点が特長となっている。また細かな注記として、「MMRの場合は各ポイント単位でランキング評価が進むため、Qdrant側で算出されるMMRスコアは基本的にクエリベクトルとの近接度となる」点についても解説していた。ま、いいか。
Qdrantクラウド上にベクトルコレクションをセットアップして映画情報を保存しよう
この部分では、レスポンスがスコア順で返されるのではなく、MMR(最大マージン再現)手法による優先順位に基づいて選択された項目が返されることを示しているんだよね。
python
def search_movies_by_text(query_text, limit=5):
query_vector = list(embedding_model.embed([query_text]))[0]</code></pre>
results = client.query_points(
collection_name=collection_name,
query=models.NearestQuery(
nearest=query_vector,
mmr=models.Mmr(
diversity=0.5,
candidates_limit=20,
)
),
limit=limit,
with_payload=True,
)
return results
ここでいう`candidates_limit`は、MMR処理の前段階としてピックアップされる上位結果件数の指定だ。一方、`limit`は最終的に返すアイテム数を表す。普通のベクトル検索(つまりMMR適用なし)との振舞いを比較できる関数も作成しておける。
python
def search_movies_by_text_without_mmr(query_text, limit=5):
query_vector = list(embedding_model.embed([query_text]))[0]
results = client.query_points(
collection_name=collection_name,
query=query_vector,
limit=limit,
with_payload=True,
)
return results
これで実際に以下のようなテストができる。
ini
result = search_movies_by_text("horror")
result_without_mmr = search_movies_by_text_without_mmr("horror")
python
【ステップ5:LangGraphとSambaNovaを活用したChatBotロジック構築】
LangGraphは状態管理と遷移設計を通じてチャットボットワークフロー構築を支援し、クエリや応答、中間結果など一連のやり取りを整理していける仕組みとなっている。
python
from typing import TypedDict, List
from langgraph.graph import StateGraph, START, END
python
class MovieSearchState(TypedDict):
query: str
search_results: List[dict]
response: str
SambaNova Cloudは高性能AI推論インフラサービスであり、オープンソースモデル利用時も自前GPU管理から解放される特徴がある。もし使用するならばSambaNova Cloudアカウント登録後にAPIキー入手し、環境変数`sambanova_api_key`へ設定して初めて操作可能になる形式なんだ。
javascript
import os
from langchain_sambanova import ChatSambaNovaCloud
os.environ["SAMBANOVA_API_KEY"] = "<replace-with-your-key>"
llm = ChatSambaNovaCloud(
model="Llama-4-Maverick-17B-128E-Instruct",
max_tokens=1024,
temperature=0.7,
top_p=0.01,
)
python
【ステップ6:LangGraph状態用サーチノードおよび生成ノード定義】
サーチノードではさっき述べた映画サーチロジックをそのまま継承しつつ、`results.points` から値を抽出・整形してリスト化する感じ。
python
def search_node(state: MovieSearchState):
"""映画検索処理"""
results = search_movies_by_text(state["query"], limit=5)
search_data = []
for result in results.points:
search_data.append({
"score": result.score,
"name": result.payload.get('name', 'Unknown'),
"rank": result.payload.get('rank', 'N/A'),
"certificate": result.payload.get('certificate', 'N/A')
})
return {"search_results": search_data}
生成ノード側は、それら結果情報にもとづいてプロンプト文面を生成し、大規模言語モデルへ推薦提案メッセージ要求を発行する仕様だと思う。ま、いいか。

MMR再ランキング付き検索ロジックで多様なおすすめ結果を取得する手順
### Step-7 グラフのコンパイルと実行
検索ノードおよび生成ノードを `add_node` でグラフに追加し、`add_edge` を利用して順序どおりに接続します。そして最終的には `compile` を呼び出すことでワークフロー全体が稼働できる状態になります。
workflow = StateGraph(MovieSearchState)
workflow.add_node("search", search_node)
workflow.add_node("generate", generate_node)
workflow.add_edge(START, "search")
workflow.add_edge("search", "generate")
workflow.add_edge("generate", END)
graph = workflow.compile()
検索ノードおよび生成ノードを `add_node` でグラフに追加し、`add_edge` を利用して順序どおりに接続します。そして最終的には `compile` を呼び出すことでワークフロー全体が稼働できる状態になります。
workflow = StateGraph(MovieSearchState)
workflow.add_node("search", search_node)
workflow.add_node("generate", generate_node)
workflow.add_edge(START, "search")
workflow.add_edge("search", "generate")
workflow.add_edge("generate", END)
graph = workflow.compile()
result = graph.invoke({"query":"horror"
LangGraphとSambaNova活用による会話型チャットボットワークフロー設計方法
5m月間におけるリーダーの具体的な人数については、確かな出典は示されていない模様です。コミュニティ内では資金的な支援を一切受けず、自主性を重んじて地道に取り組む姿勢が記載されています。実際の応援手段としては、LinkedInやTikTok、Instagramなどでフォローすることや、週ごとのニュースレターを購読するといった方法が紹介されていますね。さらに、離れる前には執筆者への「拍手」や「フォロー」を忘れず促す案内も添えられています。ま、いいか。全体を通して自然体なアプローチが感じ取れますし、とりたてて形式張った依頼は見当たりませんでした。