Сравнить коммиты
	
		
			2 Коммитов 
		
	
	
		
			d94e804592
			...
			6ae9ebe361
		
	
	| Автор | SHA1 | Дата | 
|---|---|---|
| 
							
							
								 | 
						6ae9ebe361 | 11 месяцев назад | 
| 
							
							
								 | 
						5bc2f0c7b7 | 11 месяцев назад | 
@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					*.parquet
 | 
				
			||||||
 | 
					*__pycache__*
 | 
				
			||||||
@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					pandas
 | 
				
			||||||
 | 
					matplotlib
 | 
				
			||||||
 | 
					seaborn
 | 
				
			||||||
 | 
					pyarrow==13.0.0
 | 
				
			||||||
 | 
					mlflow==2.7.1 
 | 
				
			||||||
 | 
					implicit==0.7.2
 | 
				
			||||||
 | 
					catboost
 | 
				
			||||||
 | 
					fastapi
 | 
				
			||||||
 | 
					uvicorn
 | 
				
			||||||
											
												
													Различия файлов скрыты, потому что одна или несколько строк слишком длинны
												
											
										
									
								@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					FROM python:3.11-slim
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					COPY . /events_app
 | 
				
			||||||
 | 
					WORKDIR /events_app
 | 
				
			||||||
 | 
					RUN pip install -r requirements.txt
 | 
				
			||||||
 | 
					EXPOSE 8020
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CMD ["uvicorn", "events_service:app", "--port", "8020", "--host", "0.0.0.0"] 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -0,0 +1,54 @@
 | 
				
			|||||||
 | 
					from fastapi import FastAPI
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class EventStore:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, max_events_per_user=10):
 | 
				
			||||||
 | 
					        self.events = {}
 | 
				
			||||||
 | 
					        self.max_events_per_user = max_events_per_user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get(self, user_id):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Возвращает события для пользователя
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if user_id in self.events:
 | 
				
			||||||
 | 
					            user_events =  self.events[user_id]
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            user_events = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return user_events
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def put(self, user_id, item_id):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Сохраняет событие для пользователя
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        user_events = self.get(user_id)
 | 
				
			||||||
 | 
					        self.events[user_id] = [item_id] + user_events[: self.max_events_per_user]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					events_store = EventStore()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app = FastAPI(title="events")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.post("/put")
 | 
				
			||||||
 | 
					async def put(user_id: int, item_id: int):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Сохраняет событие для user_id, item_id
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    events_store.put(user_id, item_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {"result": "ok"}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.get("/get")
 | 
				
			||||||
 | 
					async def get(user_id: int, k: int = 10):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Возвращает список последних k событий для пользователя user_id
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    events = events_store.get(user_id)[:k]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {"events": events}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					fastapi
 | 
				
			||||||
 | 
					uvicorn
 | 
				
			||||||
@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					FROM python:3.11-slim
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					COPY . /features_app
 | 
				
			||||||
 | 
					WORKDIR /features_app
 | 
				
			||||||
 | 
					RUN pip install -r requirements.txt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					EXPOSE 8010
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CMD ["uvicorn", "feature_service:app", "--port", "8010", "--host", "0.0.0.0"] 
 | 
				
			||||||
@ -0,0 +1,57 @@
 | 
				
			|||||||
 | 
					import logging
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import pandas as pd
 | 
				
			||||||
 | 
					from fastapi import FastAPI
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					PATH_TO_SIMILAR_ITEMS = '../../recommendations/similar_items.parquet'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					logger = logging.getLogger("uvicorn.error")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SimilarItems:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, path, **kwargs):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Загружаем данные из файла
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        logger.info(f"Loading data")
 | 
				
			||||||
 | 
					        self._similar_items = pd.read_parquet(path)
 | 
				
			||||||
 | 
					        self._similar_items = self._similar_items[kwargs['columns']]
 | 
				
			||||||
 | 
					        self._similar_items = self._similar_items.set_index('item_id')
 | 
				
			||||||
 | 
					        logger.info(f"Loaded")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get(self, item_id: int, k: int = 10):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Возвращает список k похожих объектов
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            i2i = self._similar_items.loc[item_id].head(k)
 | 
				
			||||||
 | 
					            i2i = {"item_id_2": i2i["sim_item_id"].tolist(), "score": i2i['score'].tolist()}
 | 
				
			||||||
 | 
					        except KeyError:
 | 
				
			||||||
 | 
					            logger.error("No recommendations found")
 | 
				
			||||||
 | 
					            i2i = {"item_id_2": [], "score": []}
 | 
				
			||||||
 | 
					        except: 
 | 
				
			||||||
 | 
					            logger.error("problem with similar recomendations")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return i2i
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					sim_items_store = SimilarItems( 
 | 
				
			||||||
 | 
					    PATH_TO_SIMILAR_ITEMS,
 | 
				
			||||||
 | 
					    columns=["item_id", "sim_item_id", "score"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					logger.info("Ready!")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# создаём приложение FastAPI
 | 
				
			||||||
 | 
					app = FastAPI(title="features")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.get("/similar_items")
 | 
				
			||||||
 | 
					async def recommendations(item_id: int, k: int = 10):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Возвращает список похожих объектов длиной k для item_id
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    i2i = sim_items_store.get(item_id, k)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return i2i
 | 
				
			||||||
@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					pandas
 | 
				
			||||||
 | 
					matplotlib
 | 
				
			||||||
 | 
					pyarrow
 | 
				
			||||||
 | 
					implicit==0.7.2
 | 
				
			||||||
 | 
					fastapi
 | 
				
			||||||
 | 
					uvicorn
 | 
				
			||||||
@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					FROM python:3.11-slim
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					COPY . /recs_app
 | 
				
			||||||
 | 
					WORKDIR /recs_app
 | 
				
			||||||
 | 
					RUN pip install -r requirements.txt
 | 
				
			||||||
 | 
					RUN export $(cat ./.env | xargs)
 | 
				
			||||||
 | 
					EXPOSE 8000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CMD ["uvicorn", "recommendation_service:app", "--port", "8000", "--host", "0.0.0.0"]
 | 
				
			||||||
@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					import logging as logger
 | 
				
			||||||
 | 
					import pandas as pd
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Recommendations:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self._recs = {"personal": None, "default": None}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def load(self, rec_type, path, **kwargs):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Загружает рекомендации из файла
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        logger.info(f"Loading recommendations, type: {rec_type}")
 | 
				
			||||||
 | 
					        self._recs[rec_type] = pd.read_parquet(path, **kwargs)
 | 
				
			||||||
 | 
					        if rec_type == "personal":
 | 
				
			||||||
 | 
					            self._recs[rec_type] = self._recs[rec_type].set_index("user_id")
 | 
				
			||||||
 | 
					        logger.info(f"Loaded")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get(self, user_id: int, k: int=100):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Возвращает список рекомендаций для пользователя
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            recs = self._recs["personal"].loc[user_id]
 | 
				
			||||||
 | 
					            recs = recs["item_id"].to_list()[:k]
 | 
				
			||||||
 | 
					        except KeyError:
 | 
				
			||||||
 | 
					            recs = self._recs["default"]
 | 
				
			||||||
 | 
					            recs = recs["item_id"].to_list()[:k]
 | 
				
			||||||
 | 
					        except:
 | 
				
			||||||
 | 
					            logger.error("No recommendations found")
 | 
				
			||||||
 | 
					            recs = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not recs:
 | 
				
			||||||
 | 
					            logger.warning(f"No default recommendations available for user {user_id}")
 | 
				
			||||||
 | 
					            recs = []
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            logger.info(f'recs: {recs}')
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        return recs
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -0,0 +1,116 @@
 | 
				
			|||||||
 | 
					import logging
 | 
				
			||||||
 | 
					import requests
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					import pandas as pd
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from fastapi import FastAPI
 | 
				
			||||||
 | 
					from rec_handler import Recommendations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					PATH_TO_RECOMENDATIONS = '../../recommendations/'
 | 
				
			||||||
 | 
					FILENAME_PERS_RECOMENDATIONS = 'personal_als.parquet'
 | 
				
			||||||
 | 
					FILENAME_TOP_POPULAR = 'top_popular.parquet'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					logger = logging.getLogger("uvicorn.error")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					rec_store = Recommendations()
 | 
				
			||||||
 | 
					features_store_url = "http://localhost:8010"
 | 
				
			||||||
 | 
					events_store_url = "http://localhost:8020" 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					logger.info("Starting")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					rec_store.load(
 | 
				
			||||||
 | 
					"personal",
 | 
				
			||||||
 | 
					PATH_TO_RECOMENDATIONS+FILENAME_PERS_RECOMENDATIONS,
 | 
				
			||||||
 | 
					columns=["user_id", "item_id", "score"],
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					rec_store.load(
 | 
				
			||||||
 | 
					    "default",
 | 
				
			||||||
 | 
					    PATH_TO_RECOMENDATIONS+FILENAME_TOP_POPULAR,
 | 
				
			||||||
 | 
					    columns=["item_id", "rank"],
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# создаём приложение FastAPI
 | 
				
			||||||
 | 
					app = FastAPI(title="recommendations")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.post("/recommendations_offline")
 | 
				
			||||||
 | 
					async def recommendations_offline(user_id: int, k: int = 100):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Возвращает список рекомендаций длиной k для пользователя user_id
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    recs = rec_store.get(user_id=user_id, k=k)
 | 
				
			||||||
 | 
					    return {"recs": recs}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.post("/recommendations_online")
 | 
				
			||||||
 | 
					async def recommendations_online(user_id: int, k: int = 100):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Возвращает список онлайн-рекомендаций длиной k для пользователя user_id
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    headers = {"Content-type": "application/json", "Accept": "text/plain"}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # получаем последние события пользователя
 | 
				
			||||||
 | 
					    params = {"user_id": user_id, "k": 3}
 | 
				
			||||||
 | 
					    resp = requests.get(events_store_url + "/get", headers=headers, params=params)
 | 
				
			||||||
 | 
					    events = resp.json()
 | 
				
			||||||
 | 
					    events = events["events"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # получаем список похожих объектов
 | 
				
			||||||
 | 
					    if len(events) > 0:
 | 
				
			||||||
 | 
					        items = []
 | 
				
			||||||
 | 
					        scores = []
 | 
				
			||||||
 | 
					        for item_id in events:
 | 
				
			||||||
 | 
					            params = {"item_id": item_id, "k": k}
 | 
				
			||||||
 | 
					            headers = {"Content-type": "application/json", "Accept": "text/plain"}
 | 
				
			||||||
 | 
					            resp = requests.get(features_store_url + "/similar_items", headers=headers, params=params)
 | 
				
			||||||
 | 
					            if resp.status_code == 200:
 | 
				
			||||||
 | 
					                similar_items = resp.json()
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                similar_items = None
 | 
				
			||||||
 | 
					                print(f"status code: {resp.status_code}")
 | 
				
			||||||
 | 
					            items += similar_items["item_id_2"]
 | 
				
			||||||
 | 
					            scores += similar_items["score"]
 | 
				
			||||||
 | 
					        combined = list(zip(items, scores))
 | 
				
			||||||
 | 
					        combined = sorted(combined, key=lambda x: x[1], reverse=True)
 | 
				
			||||||
 | 
					        combined = [item for item, _ in combined]
 | 
				
			||||||
 | 
					        recs = combined[:k]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        recs = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {"recs": recs} 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.post("/recommendations")
 | 
				
			||||||
 | 
					async def recommendations(user_id: int, k: int = 100):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Возвращает список рекомендаций длиной k для пользователя user_id
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    recs_offline = await recommendations_offline(user_id, k)
 | 
				
			||||||
 | 
					    recs_online = await recommendations_online(user_id, k)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    recs_offline = recs_offline["recs"]
 | 
				
			||||||
 | 
					    recs_online = recs_online["recs"]
 | 
				
			||||||
 | 
					    recs_blended = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    min_length = min(len(recs_offline), len(recs_online))
 | 
				
			||||||
 | 
					    # чередуем элементы из списков, пока позволяет минимальная длина
 | 
				
			||||||
 | 
					    for i in range(min_length):
 | 
				
			||||||
 | 
					        recs_blended.append(recs_online[i])
 | 
				
			||||||
 | 
					        recs_blended.append(recs_offline[i])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # добавляем оставшиеся элементы в конец
 | 
				
			||||||
 | 
					    recs_blended += recs_online[min_length:]
 | 
				
			||||||
 | 
					    recs_blended += recs_offline[min_length:]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # оставляем только первые k рекомендаций
 | 
				
			||||||
 | 
					    recs_blended = recs_blended[:k]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {"recs": recs_blended}
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					pandas
 | 
				
			||||||
 | 
					matplotlib
 | 
				
			||||||
 | 
					pyarrow==13.0.0
 | 
				
			||||||
 | 
					mlflow==2.7.1 
 | 
				
			||||||
 | 
					psycopg==3.1.12
 | 
				
			||||||
 | 
					psycopg[binary,pool]
 | 
				
			||||||
 | 
					boto3==1.34.78
 | 
				
			||||||
 | 
					implicit==0.7.2
 | 
				
			||||||
 | 
					fastapi
 | 
				
			||||||
 | 
					uvicorn
 | 
				
			||||||
 | 
					prometheus-fastapi-instrumentator 
 | 
				
			||||||
 | 
					python-dotenv
 | 
				
			||||||
											
												Двоичный файл не отображается.
											
										
									
								
											
												Двоичный файл не отображается.
											
										
									
								
					Загрузка…
					
					
				
		Ссылка в новой задаче