1
1
import uuid
2
2
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
4
7
5
8
from template_fastapi .models .restaurant import Restaurant
6
9
from template_fastapi .settings .azure_cosmosdb import get_azure_cosmosdb_settings
11
14
azure_openai_settings = get_azure_openai_settings ()
12
15
13
16
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
+
14
58
@router .get (
15
59
"/foodies/restaurants/" ,
16
60
response_model = list [Restaurant ],
17
61
tags = ["foodies" ],
18
62
operation_id = "list_foodies_restaurants" ,
19
63
)
20
- async def list_foodies_restaurants () -> list [Restaurant ]:
64
+ async def list_foodies_restaurants (limit : int = Query ( 10 , description = "取得する最大件数" ) ) -> list [Restaurant ]:
21
65
"""
22
- List foodies restaurants.
66
+ レストラン一覧を取得する
23
67
"""
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 )} " )
40
76
41
77
42
78
@router .get (
@@ -45,18 +81,16 @@ async def list_foodies_restaurants() -> list[Restaurant]:
45
81
tags = ["foodies" ],
46
82
operation_id = "get_foodies_restaurant" ,
47
83
)
48
- async def get_foodies_restaurant (restaurant_id : int ) -> Restaurant :
84
+ async def get_foodies_restaurant (restaurant_id : str ) -> Restaurant :
49
85
"""
50
- Get a specific foodies restaurant by ID.
86
+ 指定されたIDのレストラン情報を取得する
51
87
"""
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 )} " )
60
94
61
95
62
96
@router .post (
@@ -67,16 +101,43 @@ async def get_foodies_restaurant(restaurant_id: int) -> Restaurant:
67
101
)
68
102
async def create_foodies_restaurant (restaurant : Restaurant ) -> Restaurant :
69
103
"""
70
- Create a new foodies restaurant.
104
+ 新しいレストランを作成する
71
105
"""
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 )} " )
80
141
81
142
82
143
@router .put (
@@ -85,31 +146,69 @@ async def create_foodies_restaurant(restaurant: Restaurant) -> Restaurant:
85
146
tags = ["foodies" ],
86
147
operation_id = "update_foodies_restaurant" ,
87
148
)
88
- async def update_foodies_restaurant (restaurant_id : int , restaurant : Restaurant ) -> Restaurant :
149
+ async def update_foodies_restaurant (restaurant_id : str , restaurant : Restaurant ) -> Restaurant :
89
150
"""
90
- Update an existing foodies restaurant.
151
+ 既存のレストラン情報を更新する
91
152
"""
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 )} " )
100
195
101
196
102
197
@router .delete (
103
198
"/foodies/restaurants/{restaurant_id}" ,
104
199
tags = ["foodies" ],
105
200
operation_id = "delete_foodies_restaurant" ,
106
201
)
107
- async def delete_foodies_restaurant (restaurant_id : int ) -> dict :
202
+ async def delete_foodies_restaurant (restaurant_id : str ) -> dict :
108
203
"""
109
- Delete a foodies restaurant by ID.
204
+ 指定されたIDのレストランを削除する
110
205
"""
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 )} " )
113
212
114
213
115
214
@router .get (
@@ -118,24 +217,66 @@ async def delete_foodies_restaurant(restaurant_id: int) -> dict:
118
217
tags = ["foodies" ],
119
218
operation_id = "search_foodies_restaurants" ,
120
219
)
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 ]:
122
261
"""
123
- Search for foodies restaurants by name.
262
+ 指定した位置の近くにあるレストランを検索する
124
263
"""
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