1
1
#!/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
3
3
4
4
import csv
5
5
6
6
import typer
7
- from azure .cosmos import CosmosClient , PartitionKey
8
- from langchain_core .documents import Document
9
- from langchain_openai import AzureOpenAIEmbeddings
10
7
from rich .console import Console
11
8
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
14
11
15
12
app = typer .Typer ()
16
13
console = Console ()
14
+ restaurant_repo = RestaurantRepository ()
17
15
18
16
19
- def read_csv_data (file_path : str ) -> list [dict ]:
17
+ def read_csv_data (file_path : str ) -> list [Restaurant ]:
20
18
"""CSVファイルからレストランデータを読み込む"""
21
19
restaurants = []
22
20
with open (file_path , encoding = "utf-8" ) as csvfile :
23
21
csv_reader = csv .DictReader (csvfile )
24
22
for row in csv_reader :
25
23
# タグをリストに変換
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
+ )
38
34
restaurants .append (restaurant )
39
35
return restaurants
40
36
41
37
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
-
89
38
@app .command ()
90
39
def import_data (
91
40
csv_file : str = typer .Option (..., "--csv-file" , "-f" , help = "CSVファイルのパス" ),
92
41
batch_size : int = typer .Option (100 , "--batch-size" , "-b" , help = "一度に処理するバッチサイズ" ),
93
42
):
94
- """CSVからデータをCosmosDBにインポートしてベクトル検索を設定する """
43
+ """CSVからデータをデータベースにインポートしてベクトル検索を設定する """
95
44
console .print (f"[bold green]CSVファイル[/bold green]: { csv_file } からデータを読み込みます" )
96
45
97
46
# CSVデータの読み込み
98
47
restaurants = read_csv_data (csv_file )
99
48
console .print (f"[bold blue]{ len (restaurants )} 件[/bold blue]のレストランデータを読み込みました" )
100
49
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
-
110
50
# データの登録
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]" )
115
52
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 } を登録しました" )
118
59
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]処理完了 " )
120
61
121
62
console .print ("[bold green]すべてのデータが正常に登録されました![/bold green]" )
122
63
@@ -129,32 +70,102 @@ def search(
129
70
"""説明文のベクトル検索を実行する"""
130
71
console .print (f"[bold green]クエリ[/bold green]: '{ query } 'で検索します" )
131
72
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 } " )
134
85
135
- # Cosmos DBに接続
136
- container = setup_cosmos_db ()
86
+ return results
137
87
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
- """
144
88
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圏内を検索します" )
146
98
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
+ )
148
103
149
104
# 結果の表示
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 )} " )
158
169
159
170
160
171
if __name__ == "__main__" :
0 commit comments