0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LangChainを学びたい(Step7:Chromaでメタ情報を絞り検索する)

Posted at

はじめに

前回までの続きです。前回までは意味が近い類似検索を実施しました。ただ、実務の中ではメタ情報で絞り込みをかけながら類似検索をかける事が多いと思います!今回はそんなケースを実装に落として見ます!!

1. データ取得

今回は初代の151匹のポケモンの情報を登録しておき、その中で意味検索とメタ検索で絞り込みを実施していきます!!

fetch_pokemon_151_with_image.py
import requests
import csv
import time

OUTPUT_CSV = "pokemon_151_with_image.csv"


def get_species_description(species_id: int) -> str:
    """pokemon-species から日本語の説明文を1つ取得"""
    url = f"https://pokeapi.co/api/v2/pokemon-species/{species_id}"
    r = requests.get(url)
    r.raise_for_status()
    data = r.json()

    # 日本語の説明文を探す(ja-Hrkt → ひらがな・カタカナ、日本語優先)
    for entry in data.get("flavor_text_entries", []):
        if entry["language"]["name"] in ["ja", "ja-Hrkt"]:
            text = entry["flavor_text"]
            # 改行や全角スペースを整形
            text = text.replace("\n", " ").replace("\u3000", " ")
            return text

    return ""


def get_pokemon_data(poke_id: int) -> dict:
    """ポケモンIDから、名前/タイプ/説明/画像URLをまとめて取得"""
    url = f"https://pokeapi.co/api/v2/pokemon/{poke_id}"
    r = requests.get(url)
    r.raise_for_status()
    data = r.json()

    # 英語名
    name_en = data["name"]  # 例: "bulbasaur"

    # 種族情報(日本語名など)を取得
    species_url = data["species"]["url"]
    species_res = requests.get(species_url)
    species_res.raise_for_status()
    species = species_res.json()

    # 日本語名
    name_jp = ""
    for n in species.get("names", []):
        if n["language"]["name"] in ["ja", "ja-Hrkt"]:
            name_jp = n["name"]
            break

    # タイプ(複数の場合あり)
    types = [t["type"]["name"] for t in data.get("types", [])]
    type1 = types[0] if len(types) > 0 else ""
    type2 = types[1] if len(types) > 1 else ""

    # 説明文(日本語)
    description = get_species_description(poke_id)

    # 画像URL(公式イラスト)
    # other → official-artwork → front_default がキレイな公式絵
    sprites = data.get("sprites", {})
    other = sprites.get("other", {})
    official = other.get("official-artwork", {})
    image_url = official.get("front_default") or sprites.get("front_default") or ""

    return {
        "id": poke_id,
        "name_jp": name_jp,
        "name_en": name_en,
        "type1": type1,
        "type2": type2,
        "description": description,
        "image_url": image_url,
    }


def main() -> None:
    all_data: list[dict] = []

    for i in range(1, 152):  # 初代 1〜151
        print(f"Fetching {i} ...")
        try:
            info = get_pokemon_data(i)
            all_data.append(info)
        except Exception as e:
            print(f"Error on ID={i}: {e}")
        # APIへの負荷を下げるために少し待つ
        time.sleep(0.2)

    # CSV に書き出し
    with open(OUTPUT_CSV, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(
            f,
            fieldnames=[
                "id",
                "name_jp",
                "name_en",
                "type1",
                "type2",
                "description",
                "image_url",
            ],
        )
        writer.writeheader()
        writer.writerows(all_data)

    print(f"\n🎉 完了! → {OUTPUT_CSV} を生成しました")


if __name__ == "__main__":
    main()

2. Chromaに登録

151匹の情報を登録します!

pokemon_chroma_store_151.py
# pokemon_chroma_store_151.py
import csv
import os
from dotenv import load_dotenv

from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma  # 新しい Chroma パッケージを使用

# 1. .env 読み込み
load_dotenv()
api_key = os.getenv("OPENROUTER_API_KEY")
if not api_key:
    raise RuntimeError("OPENROUTER_API_KEY が設定されていません")

# 2. Embedding モデル(OpenRouter 経由の OpenAI Embedding)
emb = OpenAIEmbeddings(
    model="text-embedding-3-small",
    openai_api_key=api_key,
    base_url="https://openrouter.ai/api/v1",
)

# 3. CSV 読み込み
CSV_PATH = "pokemon_151_with_image.csv"
texts: list[str] = []
metadatas: list[dict] = []

with open(CSV_PATH, encoding="utf-8") as f:
    reader = csv.DictReader(f)
    for row in reader:
        # ベクトル化に使うテキスト(意味検索用)
        text = (
            f"{row['name_jp']}{row['name_en']}): "
            f"タイプ={row['type1']} {row['type2']}"
            f"説明: {row['description']}"
        )
        texts.append(text)

        # メタデータ(タイプ・名前・画像URLなど)
        metadatas.append(
            {
                "id": int(row["id"]),
                "name_jp": row["name_jp"],
                "name_en": row["name_en"],
                "type1": row["type1"],
                "type2": row["type2"],
                "image_url": row["image_url"],
            }
        )

print(f"CSV 読み込み完了: {len(texts)}")

# 4. Chroma に保存(永続化)
PERSIST_DIR = "chroma_pokemon_151"

db = Chroma.from_texts(
    texts=texts,
    embedding=emb,
    metadatas=metadatas,
    collection_name="pokemon_151",
    persist_directory=PERSIST_DIR,
)

print("🔥 Chroma にポケモン151匹を保存しました!")
実行結果
(venv) ~/develop/langchain_study  (main)$ python3 pokemon_chroma_store_151.py
CSV 読み込み完了: 150 件
🔥 Chroma にポケモン151匹を保存しました!
# pokemon_search_pokemon_151.py
import os
from dotenv import load_dotenv

from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma

# 1. .env 読み込み
load_dotenv()
api_key = os.getenv("OPENROUTER_API_KEY")
if not api_key:
    raise RuntimeError("OPENROUTER_API_KEY が設定されていません")

# 2. Embedding モデル
emb = OpenAIEmbeddings(
    model="text-embedding-3-small",
    openai_api_key=api_key,
    base_url="https://openrouter.ai/api/v1",
)

# 3. 永続化された Chroma をロード
PERSIST_DIR = "chroma_pokemon_151"

db = Chroma(
    persist_directory=PERSIST_DIR,
    collection_name="pokemon_151",
    embedding_function=emb,
)

print("📁 Chroma ポケモン151 データベース読み込み完了")


def semantic_search_with_filters() -> None:
    """
    意味検索 + メタ情報フィルタをまとめて行う関数。

    - query: 自然文(例: 「砂の中に潜って丸くなって身を守るポケモン」)
    - type1/type2: 必要なら英語タイプで絞り込み(例: ground, fire, water)
    """
    print("\n=== 意味検索 + メタ情報フィルタ ===")
    query = input("どんなポケモンを探したい? > ").strip()
    if not query:
        print("クエリが空です。")
        return

    print("\nタイプで絞り込む場合は、英語タイプ名を入力してください。")
    print("例: grass, fire, water, electric, ground, rock, psychic, ice, dragon, normal, poison, bug, flying, steel, fairy ...")
    type1 = input("type1 で絞り込み(空なら指定なし)> ").strip()
    type2 = input("type2 で絞り込み(空なら指定なし)> ").strip()

    filter_dict: dict | None = None
    if type1 or type2:
        filter_dict = {}
        if type1:
            filter_dict["type1"] = type1
        if type2:
            filter_dict["type2"] = type2

    # filter_dict が None なら全体から意味検索、あればその条件内で意味検索
    docs = db.similarity_search(
        query,
        k=5,
        filter=filter_dict,
    )

    if not docs:
        print("該当するポケモンが見つかりませんでした。")
        return

    print("\n🔎 検索結果 Top5:")
    for i, doc in enumerate(docs, start=1):
        meta = doc.metadata
        print(f"\n[{i}] #{meta['id']} {meta['name_jp']}{meta['name_en']}")
        print(f"  タイプ: {meta['type1']}, {meta['type2']}")
        print(f"  画像: {meta['image_url']}")
        print(f"  内容: {doc.page_content[:80]}...")  # 説明長いので先頭だけ表示


def metadata_only_search() -> None:
    """
    メタ情報だけでの絞り込み(おまけ)。
    例: type1 = 'ground' のポケモン一覧を見たいだけ、など。
    """
    print("\n=== メタ情報だけで検索(一覧用) ===")
    print("type1 / type2 は英語表記です(例: fire, water, ground, electric ...)")
    key = input("どのキーで絞り込みますか?(type1 / type2) > ").strip()
    value = input("値を入力してください > ").strip()

    if key not in ("type1", "type2"):
        print("type1 / type2 以外は未対応です。")
        return

    result = db.get(where={key: value})
    ids = result.get("ids", [])
    metadatas = result.get("metadatas", [])

    if not ids:
        print("該当するポケモンがいませんでした。")
        return

    print(f"\n🔎 {key} = {value} のポケモン: {len(ids)}")
    for meta in metadatas:
        print(
            f"- #{meta['id']:>3} {meta['name_jp']}{meta['name_en']}"
            f"[{meta['type1']}, {meta['type2']}]"
        )


def main() -> None:
    while True:
        print("\n==============================")
        print("1: 意味検索 + メタ情報フィルタ")
        print("2: メタ情報だけで一覧を出す(type1 / type2)")
        print("Enter: 終了")
        print("==============================")
        choice = input("モードを選んでください > ").strip()

        if choice == "":
            print("終了します。")
            break
        elif choice == "1":
            semantic_search_with_filters()
        elif choice == "2":
            metadata_only_search()
        else:
            print("1 / 2 / Enter のいずれかを選んでください。")


if __name__ == "__main__":
    main()
実行結果
(venv) ~/develop/langchain_study  (main)$ python3 pokemon_chroma_151_query.py
📁 Chroma ポケモン151 データベース読み込み完了

==============================
1: 意味検索 + メタ情報フィルタ
2: メタ情報だけで一覧を出す(type1 / type2)
Enter: 終了
==============================
モードを選んでください > 1

=== 意味検索 + メタ情報フィルタ ===
どんなポケモンを探したい? > まえば

タイプで絞り込む場合は、英語タイプ名を入力してください。
例: grass, fire, water, electric, ground, rock, psychic, ice, dragon, normal, poison, bug, flying, steel, fairy ...
type1 で絞り込み(空なら指定なし)> normal
type2 で絞り込み(空なら指定なし)> 

🔎 検索結果 Top5:

[1] #20 ラッタ(raticate)
  タイプ: normal, 
  画像: https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/20.png
  内容: ラッタ(raticate): タイプ=normal 。説明: のびつづける まえばを けずるため かたい ものを かじる しゅうせい。 ブロックべいも かじって...

[2] #132 メタモン(ditto)
  タイプ: normal, 
  画像: https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/132.png
  内容: メタモン(ditto): タイプ=normal 。説明: ぜんしんの さいぼうを くみかえて みたものの かたち そっくりに へんしんする のうりょくを もつ。...

[3] #16 ポッポ(pidgey)
  タイプ: normal, flying
  画像: https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/16.png
  内容: ポッポ(pidgey): タイプ=normal flying。説明: もりや はやしに おおく ぶんぷ。 ちじょうでも はげしく はばたいて すなを かけたりす...

[4] #113 ラッキー(chansey)
  タイプ: normal, 
  画像: https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/113.png
  内容: ラッキー(chansey): タイプ=normal 。説明: しあわせを はこぶと いわれている。 きずついた ひとに タマゴを わけてあげる やさしい ポケモ...

[5] #19 コラッタ(rattata)
  タイプ: normal, 
  画像: https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/19.png
  内容: コラッタ(rattata): タイプ=normal 。説明: キバが2つ。とにかく なんでも かじってみる。1ぴき みつけたら 40ぴきは そこに すんでるはず...

==============================
1: 意味検索 + メタ情報フィルタ
2: メタ情報だけで一覧を出す(type1 / type2)
Enter: 終了

最後に

色々学んできましたが、短期間でRAGを組む事ができました。
もちろんスケールするにつれて難易度は増加していくと思いますが、基礎を学ぶ事ができました!!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?