Skip to content

Commit 4f09ca5

Browse files
committed
copilot: #file:foodies_restaurants.py の実装で repositories モジュールを利用するように実装修正して。
1 parent 786d3bd commit 4f09ca5

File tree

3 files changed

+117
-104
lines changed

3 files changed

+117
-104
lines changed

docs/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
### Azure Cosmos DB
2828

2929
- [VectorDistance() を使用したクエリによるベクトル検索を実行する](https://learn.microsoft.com/ja-jp/azure/cosmos-db/nosql/vector-search#perform-vector-search-with-queries-using-vectordistance)
30+
- [Azure Cosmos DB for NoSQL での改ページ](https://learn.microsoft.com/ja-jp/azure/cosmos-db/nosql/query/pagination)
31+
- [OFFSET LIMIT (NoSQL クエリ)](https://learn.microsoft.com/ja-jp/azure/cosmos-db/nosql/query/offset-limit)
3032
- [LangChain / Azure Cosmos DB No SQL](https://python.langchain.com/docs/integrations/vectorstores/azure_cosmos_db_no_sql/)
3133

3234
```shell

scripts/foodies_restaurants.py

Lines changed: 114 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,122 +1,63 @@
11
#!/usr/bin/env python
2-
# filepath: /Users/ks6088ts/src/github.com/ks6088ts-labs/template-fastapi/scripts/foodies_restaurant.py
2+
# filepath: /Users/ks6088ts/src/github.com/ks6088ts-labs/template-fastapi/scripts/foodies_restaurants.py
33

44
import csv
55

66
import typer
7-
from azure.cosmos import CosmosClient, PartitionKey
8-
from langchain_core.documents import Document
9-
from langchain_openai import AzureOpenAIEmbeddings
107
from rich.console import Console
118

12-
from template_fastapi.settings.azure_cosmosdb import get_azure_cosmosdb_settings
13-
from template_fastapi.settings.azure_openai import get_azure_openai_settings
9+
from template_fastapi.models.restaurant import Restaurant
10+
from template_fastapi.repositories.restaurants import RestaurantRepository
1411

1512
app = typer.Typer()
1613
console = Console()
14+
restaurant_repo = RestaurantRepository()
1715

1816

19-
def read_csv_data(file_path: str) -> list[dict]:
17+
def read_csv_data(file_path: str) -> list[Restaurant]:
2018
"""CSVファイルからレストランデータを読み込む"""
2119
restaurants = []
2220
with open(file_path, encoding="utf-8") as csvfile:
2321
csv_reader = csv.DictReader(csvfile)
2422
for row in csv_reader:
2523
# タグをリストに変換
26-
tags = row["tags"].strip('"').split(",")
27-
restaurant = {
28-
"id": row["id"],
29-
"name": row["name"],
30-
"description": row["description"],
31-
"price": int(row["price"]),
32-
"location": {
33-
"type": "Point",
34-
"coordinates": [float(row["longitude"]), float(row["latitude"])],
35-
},
36-
"tags": tags,
37-
}
24+
tags = row["tags"].strip('"').split(",") if row["tags"] else []
25+
restaurant = Restaurant(
26+
id=row["id"],
27+
name=row["name"],
28+
description=row["description"],
29+
price=float(row["price"]),
30+
latitude=float(row["latitude"]) if row["latitude"] else None,
31+
longitude=float(row["longitude"]) if row["longitude"] else None,
32+
tags=tags,
33+
)
3834
restaurants.append(restaurant)
3935
return restaurants
4036

4137

42-
def get_embeddings(texts: list[str]) -> list[list[float]]:
43-
"""Azure OpenAIを使用してテキストのベクトル埋め込みを生成する"""
44-
settings = get_azure_openai_settings()
45-
embedding_model = AzureOpenAIEmbeddings(
46-
azure_endpoint=settings.azure_openai_endpoint,
47-
api_key=settings.azure_openai_api_key,
48-
azure_deployment=settings.azure_openai_model_embedding,
49-
api_version=settings.azure_openai_api_version,
50-
)
51-
52-
documents = [Document(page_content=text) for text in texts]
53-
embeddings = embedding_model.embed_documents([doc.page_content for doc in documents])
54-
return embeddings
55-
56-
57-
def setup_cosmos_db():
58-
"""Azure Cosmos DBのセットアップと接続"""
59-
settings = get_azure_cosmosdb_settings()
60-
client = CosmosClient.from_connection_string(settings.azure_cosmosdb_connection_string)
61-
62-
# データベースが存在しなければ作成
63-
db = client.create_database_if_not_exists(id=settings.azure_cosmosdb_database_name)
64-
65-
# コンテナが存在しなければ作成(ベクトル検索用インデックスポリシー付き)
66-
indexing_policy = {
67-
"indexingMode": "consistent",
68-
"includedPaths": [{"path": "/*"}],
69-
"excludedPaths": [{"path": "/vector/*"}],
70-
"vectorIndexes": [
71-
{
72-
"path": "/vector",
73-
"type": "cosine",
74-
"numDimensions": 1536, # OpenAI Embedding modelのデフォルトサイズ
75-
"vectorSearchConfiguration": "vectorConfig",
76-
}
77-
],
78-
}
79-
80-
container = db.create_container_if_not_exists(
81-
id=settings.azure_cosmosdb_container_name,
82-
partition_key=PartitionKey(path="/id"),
83-
indexing_policy=indexing_policy,
84-
)
85-
86-
return container
87-
88-
8938
@app.command()
9039
def import_data(
9140
csv_file: str = typer.Option(..., "--csv-file", "-f", help="CSVファイルのパス"),
9241
batch_size: int = typer.Option(100, "--batch-size", "-b", help="一度に処理するバッチサイズ"),
9342
):
94-
"""CSVからデータをCosmosDBにインポートしてベクトル検索を設定する"""
43+
"""CSVからデータをデータベースにインポートしてベクトル検索を設定する"""
9544
console.print(f"[bold green]CSVファイル[/bold green]: {csv_file}からデータを読み込みます")
9645

9746
# CSVデータの読み込み
9847
restaurants = read_csv_data(csv_file)
9948
console.print(f"[bold blue]{len(restaurants)}件[/bold blue]のレストランデータを読み込みました")
10049

101-
# 説明文を抽出してベクトル埋め込みを生成
102-
descriptions = [restaurant["description"] for restaurant in restaurants]
103-
console.print("[bold yellow]説明文のベクトル埋め込みを生成しています...[/bold yellow]")
104-
embeddings = get_embeddings(descriptions)
105-
106-
# Cosmos DBのセットアップ
107-
container = setup_cosmos_db()
108-
console.print(f"[bold green]Cosmos DBのコンテナ[/bold green]: {container.id}に接続しました")
109-
11050
# データの登録
111-
console.print("[bold yellow]レストランデータをCosmosDBに登録しています...[/bold yellow]")
112-
for i, restaurant in enumerate(restaurants):
113-
# ベクトルデータを追加
114-
restaurant["vector"] = embeddings[i]
51+
console.print("[bold yellow]レストランデータをデータベースに登録しています...[/bold yellow]")
11552

116-
# UpsertによりCosmosDBに登録
117-
container.upsert_item(body=restaurant)
53+
# バッチサイズごとに処理
54+
for i in range(0, len(restaurants), batch_size):
55+
batch = restaurants[i : i + batch_size]
56+
for restaurant in batch:
57+
created_restaurant = restaurant_repo.create_restaurant(restaurant)
58+
console.print(f"ID: {created_restaurant.id} - {created_restaurant.name}を登録しました")
11859

119-
console.print(f"ID: {restaurant['id']} - {restaurant['name']}を登録しました")
60+
console.print(f"[bold blue]{min(i + batch_size, len(restaurants))}/{len(restaurants)}件[/bold blue]処理完了")
12061

12162
console.print("[bold green]すべてのデータが正常に登録されました![/bold green]")
12263

@@ -129,32 +70,102 @@ def search(
12970
"""説明文のベクトル検索を実行する"""
13071
console.print(f"[bold green]クエリ[/bold green]: '{query}'で検索します")
13172

132-
# クエリテキストのベクトル埋め込みを生成
133-
query_embedding = get_embeddings([query])[0]
73+
# レポジトリを使用してベクトル検索を実行
74+
results = restaurant_repo.search_restaurants(query=query, k=k)
75+
76+
# 結果の表示
77+
console.print(f"\n[bold blue]{len(results)}件[/bold blue]の検索結果:")
78+
for i, restaurant in enumerate(results):
79+
console.print(f"\n[bold]{i + 1}. {restaurant.name}[/bold]")
80+
console.print(f" 説明: {restaurant.description or '説明なし'}")
81+
console.print(f" 価格: ¥{restaurant.price}")
82+
console.print(f" タグ: {', '.join(restaurant.tags)}")
83+
if restaurant.latitude and restaurant.longitude:
84+
console.print(f" 位置: 緯度 {restaurant.latitude}, 経度 {restaurant.longitude}")
13485

135-
# Cosmos DBに接続
136-
container = setup_cosmos_db()
86+
return results
13787

138-
# ベクトル検索クエリの実行
139-
query_text = f"""
140-
SELECT TOP {k} r.id, r.name, r.description, r.price, r.tags
141-
FROM restaurants r
142-
ORDER BY VectorDistance(r.vector, @queryVector)
143-
"""
14488

145-
parameters = [{"name": "@queryVector", "value": query_embedding}]
89+
@app.command()
90+
def find_nearby(
91+
latitude: float = typer.Option(..., "--latitude", "-lat", help="緯度"),
92+
longitude: float = typer.Option(..., "--longitude", "-lon", help="経度"),
93+
distance_km: float = typer.Option(5.0, "--distance", "-d", help="検索半径(キロメートル)"),
94+
limit: int = typer.Option(10, "--limit", "-l", help="取得する最大件数"),
95+
):
96+
"""位置情報に基づいて近くのレストランを検索する"""
97+
console.print(f"[bold green]位置[/bold green]: 緯度 {latitude}, 経度 {longitude}{distance_km}km圏内を検索します")
14698

147-
items = list(container.query_items(query=query_text, parameters=parameters, enable_cross_partition_query=True))
99+
# レポジトリを使用して位置検索を実行
100+
results = restaurant_repo.find_nearby_restaurants(
101+
latitude=latitude, longitude=longitude, distance_km=distance_km, limit=limit
102+
)
148103

149104
# 結果の表示
150-
console.print(f"\n[bold blue]{len(items)}件[/bold blue]の検索結果:")
151-
for i, item in enumerate(items):
152-
console.print(f"\n[bold]{i + 1}. {item['name']}[/bold]")
153-
console.print(f" 説明: {item['description']}")
154-
console.print(f" 価格: ¥{item['price']}")
155-
console.print(f" タグ: {', '.join(item['tags'])}")
156-
157-
return items
105+
console.print(f"\n[bold blue]{len(results)}件[/bold blue]の検索結果:")
106+
for i, restaurant in enumerate(results):
107+
console.print(f"\n[bold]{i + 1}. {restaurant.name}[/bold]")
108+
console.print(f" 説明: {restaurant.description or '説明なし'}")
109+
console.print(f" 価格: ¥{restaurant.price}")
110+
console.print(f" タグ: {', '.join(restaurant.tags)}")
111+
if restaurant.latitude and restaurant.longitude:
112+
console.print(f" 位置: 緯度 {restaurant.latitude}, 経度 {restaurant.longitude}")
113+
114+
return results
115+
116+
117+
@app.command()
118+
def get_restaurant(
119+
restaurant_id: str = typer.Option(..., "--id", "-i", help="レストランID"),
120+
):
121+
"""指定されたIDのレストラン情報を取得する"""
122+
console.print(f"[bold green]レストランID[/bold green]: {restaurant_id}の情報を取得します")
123+
124+
try:
125+
# レポジトリを使用してレストラン情報を取得
126+
restaurant = restaurant_repo.get_restaurant(restaurant_id)
127+
128+
# 結果の表示
129+
console.print("\n[bold blue]レストラン情報[/bold blue]:")
130+
console.print(f"ID: {restaurant.id}")
131+
console.print(f"名前: {restaurant.name}")
132+
console.print(f"説明: {restaurant.description or '説明なし'}")
133+
console.print(f"価格: ¥{restaurant.price}")
134+
console.print(f"タグ: {', '.join(restaurant.tags)}")
135+
if restaurant.latitude and restaurant.longitude:
136+
console.print(f"位置: 緯度 {restaurant.latitude}, 経度 {restaurant.longitude}")
137+
138+
return restaurant
139+
except Exception as e:
140+
console.print(f"[bold red]エラー[/bold red]: {str(e)}")
141+
return None
142+
143+
144+
@app.command()
145+
def delete_restaurant(
146+
restaurant_id: str = typer.Option(..., "--id", "-i", help="削除するレストランID"),
147+
force: bool = typer.Option(False, "--force", "-f", help="確認なしで削除する"),
148+
):
149+
"""指定されたIDのレストランを削除する"""
150+
console.print(f"[bold green]レストランID[/bold green]: {restaurant_id}を削除します")
151+
152+
try:
153+
# 削除前に確認
154+
if not force:
155+
restaurant = restaurant_repo.get_restaurant(restaurant_id)
156+
console.print("以下のレストランを削除しようとしています:")
157+
console.print(f"ID: {restaurant.id}")
158+
console.print(f"名前: {restaurant.name}")
159+
160+
if not typer.confirm("削除してもよろしいですか?"):
161+
console.print("[yellow]削除をキャンセルしました[/yellow]")
162+
return
163+
164+
# レポジトリを使用してレストランを削除
165+
restaurant_repo.delete_restaurant(restaurant_id)
166+
console.print(f"[bold green]レストランID: {restaurant_id}を正常に削除しました[/bold green]")
167+
except Exception as e:
168+
console.print(f"[bold red]エラー[/bold red]: {str(e)}")
158169

159170

160171
if __name__ == "__main__":

template_fastapi/models/restaurant.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33

44
class Restaurant(BaseModel):
5-
id: int
5+
id: str
66
name: str
77
description: str | None = None
88
price: float

0 commit comments

Comments
 (0)