Skip to content

Commit 196a8bc

Browse files
committed
copilot: foodies_restaurants.py の実装を参考にモックデータで実装されている箇所を cosmosdb 上の実際のデータを参照するように実装を書き換えて
1 parent 82bfc1f commit 196a8bc

File tree

1 file changed

+212
-71
lines changed

1 file changed

+212
-71
lines changed
Lines changed: 212 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import uuid
22

3-
from fastapi import APIRouter
3+
from azure.cosmos import CosmosClient
4+
from fastapi import APIRouter, HTTPException, Query
5+
from langchain_core.documents import Document
6+
from langchain_openai import AzureOpenAIEmbeddings
47

58
from template_fastapi.models.restaurant import Restaurant
69
from template_fastapi.settings.azure_cosmosdb import get_azure_cosmosdb_settings
@@ -11,32 +14,65 @@
1114
azure_openai_settings = get_azure_openai_settings()
1215

1316

17+
def setup_cosmos_client():
18+
"""Azure Cosmos DBに接続するクライアントを設定する"""
19+
client = CosmosClient.from_connection_string(azure_cosmosdb_settings.azure_cosmosdb_connection_string)
20+
db = client.get_database_client(azure_cosmosdb_settings.azure_cosmosdb_database_name)
21+
container = db.get_container_client(azure_cosmosdb_settings.azure_cosmosdb_container_name)
22+
return container
23+
24+
25+
def get_embeddings(text: str) -> list[float]:
26+
"""Azure OpenAIを使用してテキストのベクトル埋め込みを生成する"""
27+
embedding_model = AzureOpenAIEmbeddings(
28+
azure_endpoint=azure_openai_settings.azure_openai_endpoint,
29+
api_key=azure_openai_settings.azure_openai_api_key,
30+
azure_deployment=azure_openai_settings.azure_openai_model_embedding,
31+
api_version=azure_openai_settings.azure_openai_api_version,
32+
)
33+
34+
document = Document(page_content=text)
35+
embedding = embedding_model.embed_documents([document.page_content])[0]
36+
return embedding
37+
38+
39+
def cosmos_item_to_restaurant(item: dict) -> Restaurant:
40+
"""CosmosDBのアイテムをRestaurantモデルに変換する"""
41+
# 位置情報の取り出し
42+
latitude = None
43+
longitude = None
44+
if "location" in item and "coordinates" in item["location"]:
45+
longitude, latitude = item["location"]["coordinates"]
46+
47+
return Restaurant(
48+
id=item.get("id"),
49+
name=item.get("name"),
50+
description=item.get("description"),
51+
price=float(item.get("price", 0)),
52+
latitude=latitude,
53+
longitude=longitude,
54+
tags=item.get("tags", []),
55+
)
56+
57+
1458
@router.get(
1559
"/foodies/restaurants/",
1660
response_model=list[Restaurant],
1761
tags=["foodies"],
1862
operation_id="list_foodies_restaurants",
1963
)
20-
async def list_foodies_restaurants() -> list[Restaurant]:
64+
async def list_foodies_restaurants(limit: int = Query(10, description="取得する最大件数")) -> list[Restaurant]:
2165
"""
22-
List foodies restaurants.
66+
レストラン一覧を取得する
2367
"""
24-
return [
25-
Restaurant(
26-
id=1,
27-
name="Foodie Restaurant 1",
28-
description="A great place for foodies.",
29-
price=20.0,
30-
tags=["Italian", "Pizza"],
31-
),
32-
Restaurant(
33-
id=2,
34-
name="Foodie Restaurant 2",
35-
description="Delicious food and great ambiance.",
36-
price=25.0,
37-
tags=["Chinese", "Noodles"],
38-
),
39-
]
68+
try:
69+
container = setup_cosmos_client()
70+
query = f"SELECT TOP {limit} * FROM c"
71+
items = list(container.query_items(query=query, enable_cross_partition_query=True))
72+
73+
return [cosmos_item_to_restaurant(item) for item in items]
74+
except Exception as e:
75+
raise HTTPException(status_code=500, detail=f"データの取得に失敗しました: {str(e)}")
4076

4177

4278
@router.get(
@@ -45,18 +81,16 @@ async def list_foodies_restaurants() -> list[Restaurant]:
4581
tags=["foodies"],
4682
operation_id="get_foodies_restaurant",
4783
)
48-
async def get_foodies_restaurant(restaurant_id: int) -> Restaurant:
84+
async def get_foodies_restaurant(restaurant_id: str) -> Restaurant:
4985
"""
50-
Get a specific foodies restaurant by ID.
86+
指定されたIDのレストラン情報を取得する
5187
"""
52-
# In a real application, you would fetch the restaurant from a database.
53-
return Restaurant(
54-
id=restaurant_id,
55-
name=f"Foodie Restaurant {restaurant_id}",
56-
description="A great place for foodies.",
57-
price=20.0 + restaurant_id,
58-
tags=["Italian", "Pizza"],
59-
)
88+
try:
89+
container = setup_cosmos_client()
90+
item = container.read_item(item=restaurant_id, partition_key=restaurant_id)
91+
return cosmos_item_to_restaurant(item)
92+
except Exception as e:
93+
raise HTTPException(status_code=404, detail=f"ID {restaurant_id} のレストランが見つかりません: {str(e)}")
6094

6195

6296
@router.post(
@@ -67,16 +101,43 @@ async def get_foodies_restaurant(restaurant_id: int) -> Restaurant:
67101
)
68102
async def create_foodies_restaurant(restaurant: Restaurant) -> Restaurant:
69103
"""
70-
Create a new foodies restaurant.
104+
新しいレストランを作成する
71105
"""
72-
# In a real application, you would save the restaurant to a database.
73-
return Restaurant(
74-
id=uuid.uuid4().int, # Simulating a unique ID generation
75-
name=restaurant.name,
76-
description=restaurant.description,
77-
price=restaurant.price,
78-
tags=restaurant.tags,
79-
)
106+
try:
107+
container = setup_cosmos_client()
108+
109+
# IDが指定されていない場合は自動生成
110+
if not restaurant.id:
111+
restaurant.id = str(uuid.uuid4())
112+
113+
# ベクトル埋め込みの生成
114+
description = restaurant.description or restaurant.name
115+
vector_embedding = get_embeddings(description)
116+
117+
# 位置情報の構築
118+
location = None
119+
if restaurant.latitude is not None and restaurant.longitude is not None:
120+
location = {"type": "Point", "coordinates": [restaurant.longitude, restaurant.latitude]}
121+
122+
# CosmosDBに保存するアイテムの作成
123+
item = {
124+
"id": restaurant.id,
125+
"name": restaurant.name,
126+
"description": restaurant.description,
127+
"price": restaurant.price,
128+
"tags": restaurant.tags,
129+
"vector": vector_embedding,
130+
}
131+
132+
if location:
133+
item["location"] = location
134+
135+
# CosmosDBに保存
136+
created_item = container.create_item(body=item)
137+
138+
return cosmos_item_to_restaurant(created_item)
139+
except Exception as e:
140+
raise HTTPException(status_code=500, detail=f"レストランの作成に失敗しました: {str(e)}")
80141

81142

82143
@router.put(
@@ -85,31 +146,69 @@ async def create_foodies_restaurant(restaurant: Restaurant) -> Restaurant:
85146
tags=["foodies"],
86147
operation_id="update_foodies_restaurant",
87148
)
88-
async def update_foodies_restaurant(restaurant_id: int, restaurant: Restaurant) -> Restaurant:
149+
async def update_foodies_restaurant(restaurant_id: str, restaurant: Restaurant) -> Restaurant:
89150
"""
90-
Update an existing foodies restaurant.
151+
既存のレストラン情報を更新する
91152
"""
92-
# In a real application, you would update the restaurant in a database.
93-
return Restaurant(
94-
id=restaurant_id,
95-
name=restaurant.name,
96-
description=restaurant.description,
97-
price=restaurant.price,
98-
tags=restaurant.tags,
99-
)
153+
try:
154+
container = setup_cosmos_client()
155+
156+
# 既存のアイテムを取得
157+
try:
158+
existing_item = container.read_item(item=restaurant_id, partition_key=restaurant_id)
159+
except Exception:
160+
raise HTTPException(status_code=404, detail=f"ID {restaurant_id} のレストランが見つかりません")
161+
162+
# 説明文が変更された場合、新しいベクトル埋め込みを生成
163+
description = restaurant.description or restaurant.name
164+
if description != (existing_item.get("description") or existing_item.get("name")):
165+
vector_embedding = get_embeddings(description)
166+
else:
167+
vector_embedding = existing_item.get("vector")
168+
169+
# 位置情報の構築
170+
location = None
171+
if restaurant.latitude is not None and restaurant.longitude is not None:
172+
location = {"type": "Point", "coordinates": [restaurant.longitude, restaurant.latitude]}
173+
174+
# 更新するアイテムの作成
175+
updated_item = {
176+
"id": restaurant_id,
177+
"name": restaurant.name,
178+
"description": restaurant.description,
179+
"price": restaurant.price,
180+
"tags": restaurant.tags,
181+
"vector": vector_embedding,
182+
}
183+
184+
if location:
185+
updated_item["location"] = location
186+
187+
# CosmosDBのアイテムを更新
188+
result = container.replace_item(item=restaurant_id, body=updated_item)
189+
190+
return cosmos_item_to_restaurant(result)
191+
except HTTPException:
192+
raise
193+
except Exception as e:
194+
raise HTTPException(status_code=500, detail=f"レストランの更新に失敗しました: {str(e)}")
100195

101196

102197
@router.delete(
103198
"/foodies/restaurants/{restaurant_id}",
104199
tags=["foodies"],
105200
operation_id="delete_foodies_restaurant",
106201
)
107-
async def delete_foodies_restaurant(restaurant_id: int) -> dict:
202+
async def delete_foodies_restaurant(restaurant_id: str) -> dict:
108203
"""
109-
Delete a foodies restaurant by ID.
204+
指定されたIDのレストランを削除する
110205
"""
111-
# In a real application, you would delete the restaurant from a database.
112-
return {"message": f"Restaurant with ID {restaurant_id} deleted successfully."}
206+
try:
207+
container = setup_cosmos_client()
208+
container.delete_item(item=restaurant_id, partition_key=restaurant_id)
209+
return {"message": f"ID {restaurant_id} のレストランを削除しました"}
210+
except Exception as e:
211+
raise HTTPException(status_code=404, detail=f"ID {restaurant_id} のレストランの削除に失敗しました: {str(e)}")
113212

114213

115214
@router.get(
@@ -118,24 +217,66 @@ async def delete_foodies_restaurant(restaurant_id: int) -> dict:
118217
tags=["foodies"],
119218
operation_id="search_foodies_restaurants",
120219
)
121-
async def search_foodies_restaurants(query: str) -> list[Restaurant]:
220+
async def search_foodies_restaurants(
221+
query: str, k: int = Query(3, description="取得する上位結果の数")
222+
) -> list[Restaurant]:
223+
"""
224+
キーワードによるレストランのベクトル検索を実行する
225+
"""
226+
try:
227+
# クエリテキストのベクトル埋め込みを生成
228+
query_embedding = get_embeddings(query)
229+
230+
# CosmosDBに接続
231+
container = setup_cosmos_client()
232+
233+
# ベクトル検索クエリの実行
234+
query_text = f"""
235+
SELECT TOP {k} *
236+
FROM c
237+
ORDER BY VectorDistance(c.vector, @queryVector)
238+
"""
239+
240+
parameters = [{"name": "@queryVector", "value": query_embedding}]
241+
242+
items = list(container.query_items(query=query_text, parameters=parameters, enable_cross_partition_query=True))
243+
244+
return [cosmos_item_to_restaurant(item) for item in items]
245+
except Exception as e:
246+
raise HTTPException(status_code=500, detail=f"検索に失敗しました: {str(e)}")
247+
248+
249+
@router.get(
250+
"/foodies/restaurants/near/",
251+
response_model=list[Restaurant],
252+
tags=["foodies"],
253+
operation_id="find_nearby_restaurants",
254+
)
255+
async def find_nearby_restaurants(
256+
latitude: float = Query(..., description="緯度"),
257+
longitude: float = Query(..., description="経度"),
258+
distance_km: float = Query(5.0, description="検索半径(キロメートル)"),
259+
limit: int = Query(10, description="取得する最大件数"),
260+
) -> list[Restaurant]:
122261
"""
123-
Search for foodies restaurants by name.
262+
指定した位置の近くにあるレストランを検索する
124263
"""
125-
# In a real application, you would query a database.
126-
return [
127-
Restaurant(
128-
id=1,
129-
name="Foodie Restaurant 1",
130-
description="A great place for foodies.",
131-
price=20.0,
132-
tags=["Italian", "Pizza"],
133-
),
134-
Restaurant(
135-
id=2,
136-
name="Foodie Restaurant 2",
137-
description="Delicious food and great ambiance.",
138-
price=25.0,
139-
tags=["Chinese", "Noodles"],
140-
),
141-
]
264+
try:
265+
container = setup_cosmos_client()
266+
267+
# 地理空間クエリの実行(メートル単位で距離を指定)
268+
distance_meters = distance_km * 1000
269+
query_text = f"""
270+
SELECT TOP {limit} *
271+
FROM c
272+
WHERE ST_DISTANCE(c.location, {{
273+
"type": "Point",
274+
"coordinates": [{longitude}, {latitude}]
275+
}}) < {distance_meters}
276+
"""
277+
278+
items = list(container.query_items(query=query_text, enable_cross_partition_query=True))
279+
280+
return [cosmos_item_to_restaurant(item) for item in items]
281+
except Exception as e:
282+
raise HTTPException(status_code=500, detail=f"位置検索に失敗しました: {str(e)}")

0 commit comments

Comments
 (0)